diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/Cargo.toml b/Cargo.toml index 7b957cd..4347ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "manifold" -version = "5.0.1" +version = "6.0.0" authors = ["Lucy Bladen "] edition = "2021" @@ -27,6 +27,19 @@ r2d2 = "0.8.9" rand = "0.8.5" regex = "1.5.4" reqwest = "0.11.9" +rust-i18n = "3.1.2" serde = "1.0.171" serde_json = "1.0.105" tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] } + +[package.metadata.i18n] +# The available locales for your application, default: ["en"]. +available-locales = ["en", "de"] + +# The default locale, default: "en". +default-locale = "en" + +# Path for your translations YAML file, default: "locales". +# This config for let `cargo i18n` command line tool know where to find your translations. +# You must keep this path same as the one you pass to method `rust_i18n::i18n!`. +load-path = "config/locales" diff --git a/config/locales/manifold.yml b/config/locales/manifold.yml new file mode 100644 index 0000000..bc559fe --- /dev/null +++ b/config/locales/manifold.yml @@ -0,0 +1,201 @@ +_version: 2 +global: + not_available: + de: Nicht verfügbar + en: Not Available + not_available_with_id: + de: "Nicht verfügbar (%{id})" + en: "Not Available (%{id})" + +logging: + bot_start: + de: Bot hat sich mit Discord verbunden und ist bereit für Befehlseingabe. + en: Manifold bot connected to discord and ready to begin broadcast operations. + logged_command: + de: "Befehl %{command} von %{name} erhalten" + en: "Received command %{command} from %{name}" + user_banned: + de: "%{user} wurde gesperrt" + en: "%{user} was banned" + user_unbanned: + de: "Die Sperre wurde für %{user} aufgehoben" + en: "Ban was lifted for %{user}" + user_joined: + de: "%{name} ist dem Server mit dem Spitznamen %{nickname} beigetreten (%{uid})" + en: "%{name} joined the server with nickname %{nickname} (%{uid})" + user_left: + de: "%{name} hat den Server verlassen" + en: "%{name} left the server" + role_created: + de: "Neue Rolle %{name} (%{role_id}) wurde erstellt" + en: "New role %{name} (%{role_id}) was created" + permissions: + de: Berechtigungen für Rollen + en: Role Permissions + hoist: + de: Hochziehen + en: Hoist + icon: + de: Ikone + en: Icon + mentionable: + de: Erwähnenswert + en: Mentionable + role_deleted: + de: "Rolle %{name} (%{role_id}) wurde gelöscht" + en: "Role %{name} (%{role_id}) was deleted" + permissions: + de: Berechtigungen für Rollen + en: Role Permissions + hoist: + de: Hochziehen + en: Hoist + icon: + de: Symbol + en: Icon + mentionable: + de: Erwähnenswert + en: Mentionable + not_cached: + de: "Rolle %{role_id} wurde gelöscht (Rolle nicht zwischengespeichert, keine Rolleninformationen verfügbar)" + en: "Role %{role_id} was deleted (Role not cached, no role info available)" + message_deleted: + de: "Nachricht in #%{channel} (%{channel_id}) entfernt" + en: "Message removed in #%{channel} (%{channel_id})" + content: + de: Inhalt der Nachricht + en: Message Content + attachments: + de: Nachrichten-Anhänge + en: Message Attachments + not_cached: + de: "Nachricht entfernt in #%{channel_name} (%{channel_id}) (Nicht zwischengespeichert)" + en: "Message removed in #%{channel_name} (%{channel_id}) (Not cached)" + channel_not_cached: + de: "Nicht verfügbar / Nicht zwischengespeichert (%{channel_id})" + en: "Not Available / Not Cached (%{channel_id})" + message_edited: + de: "Nachricht aktualisiert in #%{channel_name} (%{channel_id})" + en: "Message updated in #%{channel_name} (%{channel_id})" + original_content: + de: Ursprünglicher Inhalt + en: Original Content + new_content: + de: Neue Inhalte + en: New Content + created_at: + de: Nachricht erstellt am + en: Message created at + author: + de: Verfasser der Nachricht + en: Message author + +commands: + admin: + config_dump: + de: "Konfigurations-Deponie; %{Dump}" + en: "Config dump; %{dump}" + start_watching: + de: "Okay, ich beobachte nun, %{target}." + en: "Okay, I'll start watching %{target}" + saving_the_world: + de: Speicher-Befehl erhalten, speichere Welt... + en: Save call received, saving the world... + save_complete: + de: Speichern abgeschlossen; Speicherte %{user_count} Benutzer. + en: "Save call complete; Saved %{user_count} users." + animal: + no_frog_tips: + de: Es konnten keine Hinweise gefunden werden. + en: No tips could be located. Try rubbing it. + no_animal_pic: + de: Die Suche schien kein Erfolg zu haben. Vielleicht hilft etwas Speck? + en: No frens here at the moment. Perhaps some bacon would help? + convert: + specify: + de: Geben Sie eine Konvertierung an. Weitere Informationen finden Sie in der Hilfe. + en: Specify a conversion. See help for more info. + converted: + de: "%{input_value}%{input_unit} ist %{output_value}%{output_unit}" + en: "%{input_value}%{input_unit} is %{output_value}%{output_unit}" + core: + set_timezone: + reject_invalid_timezone: + de: Ungültige Zeitzone + en: Are you having a laugh? What kind of timezone is that? + accept_timezone: + de: "Ich habe Ihre Zeitzone auf %{timezone} eingestellt, aber wir alle wissen, dass UTC die einzig wahre Zeitzone ist" + en: "I have set your timezone to %{timezone}, but we all know that UTC is the One True Timezone" + time: + unknown_timezone: + de: Ve vill tell ze Zeit! + en: This user hasn't given me their timezone yet. Shame on them! + unknown_user: + de: Wer ist das? Ich kenne sie nicht. + en: Who's that? I don't know them. + time_output: + de: "Die Zeit in %{name}'s Zeitzone von %{timezone} ist %{time}" + en: "Time in %{name}'s current timezone of %{timezone} is %{time}" + dad_joke: + no_jokes: + de: Ich kann keine Dad-Jokes finden. Vielleicht ist dein Vater nicht lustig. + en: Can't find any dad jokes. Maybe your dad isn't funny. + nasa_apod: + no_picture: + de: Das NASA Astronomy Photo of the Day fehlte. Versuchen Sie stattdessen, nach oben zu schauen. + en: The NASA Astronomy Photo of the Day was missing. Try looking up instead. + weather: + no_location: + de: Ich weiß nicht, wo du wohnst, und du hast es mir nicht gesagt, also kann ich nicht helfen. Schauen Sie aus dem Fenster. + en: I don't know where you live, and you didn't tell me, so I can't help. Look out of the window. + no_weather: + de: "Irgendetwas ist schief gelaufen. Vielleicht gibt es dort kein Wetter. %{error_output}" + en: "Something went wrong. Maybe there is no weather there. %{error_output}" + retrieve_wait: + de: Wetter abrufen, Geduld haben + en: Retrieving weather, be patient + current_weather: + de: "Aktuelles Wetter bei %{location_name}, %{location_region}, %{location_country}" + en: "Current weather at %{location_name}, %{location_region}, %{location_country}" + forecast_time: + de: "Prognose für %{forecast_time} erstellt um %{forecast_updated}" + en: "Forecast for %{forecast_time} made at %{forecast_updated}" + temperature_dewpoint: + de: Temperatur (Taupunkt) + en: Temperature (Dewpoint) + feels_like: + de: Fühlt sich wie + en: Feels like + condition: + de: Zustand + en: Condition + pressure: + de: Druck + en: Pressure + precipitation: + de: Niederschlag + en: Precipitation + humidity: + de: Feuchtigkeit + en: Humidity + cloud_coverage: + de: Wolkenbedeckung + en: Cloud coverage + coordinates: + de: Koordinaten + en: Coordinates + wind: + de: Wind + en: Wind + wind_value: + de: "%{wind_mph}mph/%{wind_kph}km/h von den %{wind_dir} (%{wind_degree} Grad), Böen auf %{gust_mph}mph/%{gust_kph}km/h" + en: "%{wind_mph}mph/%{wind_kph}kph from the %{wind_dir} (%{wind_degree} degrees), gusting to %{gust_mph}mph/%{gust_kph}kph" + weather_location: + location_save_confirm: + de: "Okay. Jetzt weiß ich, dass du in %{location} lebst. Ob das so weise war?" + en: "Okay. Now I know that you live in %{location}. Are you sure that was wise?" + +misc: + version_string: + de: "%{caller} (Mannigfaltigkeits-Framework-Version %{mfold_ver}, erstellt um %{mfold_time} aus der Revision %{mfold_rev})" + en: "%{caller} (Manifold framework version %{mfold_ver} built at %{mfold_time} from revision %{mfold_rev})" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..3db25b7 --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +{ pkgs ? import {}}: +pkgs.mkShell { + buildInputs = with pkgs; [ + openssl + postgresql + pkg-config + ]; +} \ No newline at end of file diff --git a/src/commands/admin.rs b/src/commands/admin.rs index 19dcdb9..665563f 100644 --- a/src/commands/admin.rs +++ b/src/commands/admin.rs @@ -1,10 +1,11 @@ use poise::serenity_prelude::Activity; + use crate::{ManifoldContext, ManifoldData, ManifoldResult}; use crate::error::ManifoldError; #[poise::command(slash_command, prefix_command, owners_only)] async fn dump_config(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { - ctx.say(format!("Config dump; {:?}", &ctx.data().bot_config)).await?; + ctx.say(t!("commands.admin.config_dump", dump = format!("{:?}", &ctx.data().bot_config))).await?; Ok(()) } @@ -20,13 +21,13 @@ async fn register_commands(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { async fn set_activity(ctx: ManifoldContext<'_>, #[rest] #[description="Who to watch"] target: String) -> ManifoldResult<()> { ctx.serenity_context().set_activity(Activity::watching(&target)).await; - ctx.say(format!("Okay, I'll start watching {}", target)).await?; + ctx.say(t!("commands.admin.start_watching", target = target)).await?; Ok(()) } #[poise::command(slash_command, prefix_command, owners_only)] async fn save_world(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { - let my_message = ctx.send(|f| f.content("Save call received, saving the world...").reply(true)).await?; + let my_message = ctx.send(|f| f.content(t!("commands.admin.saving_the_world")).reply(true)).await?; let mut user_count = 0; @@ -47,7 +48,7 @@ async fn save_world(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { user_count = userinfo.len(); } - my_message.edit(ctx, |f| f.content(format!("Save call complete; Saved {} users.", user_count))).await?; + my_message.edit(ctx, |f| f.content(t!("commands.admin.save_complete", user_count = user_count))).await?; Ok(()) } diff --git a/src/commands/animal.rs b/src/commands/animal.rs index 866eddc..562fb5d 100644 --- a/src/commands/animal.rs +++ b/src/commands/animal.rs @@ -16,7 +16,7 @@ async fn frog_tip(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { ctx.send(|f| f.content(format!("{}", tip)).reply(true)).await?; } else { error!("No such fuel tank frog_tips"); - ctx.send(|f| f.content("No tips could be located. Try rubbing it.".to_string()).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.animal.no_frog_tips").to_string()).reply(true)).await?; } Ok(()) @@ -39,7 +39,7 @@ async fn animal_pic(ctx: ManifoldContext<'_>, tank_name: &str) -> ManifoldResult ctx.send(|f| f.content(format!("{}", pic)).reply(true)).await?; } else { error!("No such fuel tank {}", tank_name); - ctx.send(|f| f.content("No frens here at the moment. Perhaps some bacon would help?".to_string()).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.animal.no_animal_pic").to_string()).reply(true)).await?; } Ok(()) diff --git a/src/commands/convert.rs b/src/commands/convert.rs index 6bceea7..932e2e1 100644 --- a/src/commands/convert.rs +++ b/src/commands/convert.rs @@ -4,35 +4,35 @@ use crate::{ManifoldContext, ManifoldData}; #[poise::command(slash_command, prefix_command, aliases("c"), subcommands("ftoc", "ftok", "ftor", "ctof", "ctok", "ctor", "rtof", "rtoc", "rtok", "ktoc", "ktof", "ktor"))] pub async fn convert(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { - ctx.send(|f| f.content("Specify a conversion. See help for more info").reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.specify")).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ftoc(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°F is {}°C", value, ((value - 32.0)/1.8))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_value = value, input_unit = "°F", output_value = ((value - 32.0)/1.8), output_unit = "°C")).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ftok(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°F is {}°K", value, (((value - 32.0)/1.8) + 273.0))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°F", input_value = value, output_unit = "K", output_value = (((value - 32.0)/1.8) + 273.0))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ftor(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°F is {}°R", value, (value + 459.67))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°F", input_value = value, output_unit = "°Ra", output_value = (value + 459.67))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ctof(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°C is {}°F", value, ((value * 1.8)+32.0))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°C", input_value = value, output_unit = "°F", output_value = ((value * 1.8)+32.0))).reply(true)).await?; Ok(()) } @@ -46,49 +46,49 @@ pub async fn ctok(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { #[poise::command(slash_command, prefix_command)] pub async fn ctor(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°C is {}°R", value, ((value + 273.15) * (9.0/5.0)))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°C", input_value = value, output_unit = "°Ra", output_value = ((value + 273.15) * (9.0/5.0)))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ktoc(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°K is {}°C", value, (value - 273.15))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "K", input_value = value, output_unit = "°C", output_value = (value - 273.15))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ktof(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°K is {}°F", value, ((value * (9.0/5.0)) - 459.67))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "K", input_value = value, output_unit = "°F", output_value = ((value * (9.0/5.0)) - 459.67))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn ktor(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°K is {}°R", value, (value * (9.0/5.0)))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "K", input_value = value, output_unit = "°Ra", output_value = (value * (9.0/5.0)))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn rtok(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°R is {}°K", value, (value * (5.0/9.0)))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°Ra", input_value = value, output_unit = "K", output_value = (value * (5.0/9.0)))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn rtoc(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°R is {}°C", value, ((value - 491.67) * (5.0/9.0)))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°Ra", input_value = value, output_unit = "°C", output_value = ((value - 491.67) * (5.0/9.0)))).reply(true)).await?; Ok(()) } #[poise::command(slash_command, prefix_command)] pub async fn rtof(ctx: ManifoldContext<'_>, value: f32) -> ManifoldResult<()> { - ctx.send(|f| f.content(format!("{}°R is {}°F", value, (value - 459.67))).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.convert.converted", input_unit = "°Ra", input_value = value, output_unit = "°F", output_value = (value - 459.67))).reply(true)).await?; Ok(()) } diff --git a/src/commands/core.rs b/src/commands/core.rs index 775f779..49be130 100644 --- a/src/commands/core.rs +++ b/src/commands/core.rs @@ -55,14 +55,14 @@ async fn set_timezone(ctx: ManifoldContext<'_>, input_timezone: String) -> Manif Ok(r) => r, Err(e) => { error!("Problem parsing timezone input: {:?}", e); - ctx.send(|f| f.content(format!("Are you having a laugh? What kind of timezone is that? {:?}", e)).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.core.set_timezone.reject_invalid_timezone")).reply(true)).await?; Err(e)? } }; if let Some(user) = userinfo.get_mut(ctx.author().id.as_u64()) { user.timezone = Some(validated_timezone.to_string()); - ctx.send(|f| f.content(format!("I have set your timezone to {}, but we all know that UTC is the One True Timezone.", validated_timezone.to_string())).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.core.set_timezone.accept_timezone", timezone = validated_timezone.to_string())).reply(true)).await?; } else { let new_user = UserInfo { user_id: ctx.author().id.as_u64().to_owned() as i64, @@ -73,7 +73,7 @@ async fn set_timezone(ctx: ManifoldContext<'_>, input_timezone: String) -> Manif last_seen: Some(chrono::Utc::now().timestamp()) }; userinfo.insert(ctx.author().id.as_u64().clone(), new_user); - ctx.send(|f| f.content(format!("I have set your timezone to {}, but we all know that UTC is the One True Timezone.", validated_timezone.to_string())).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.core.set_timezone.accept_timezone", timezone = validated_timezone.to_string())).reply(true)).await?; } Ok(()) @@ -86,16 +86,16 @@ pub async fn time(ctx: ManifoldContext<'_>, target: u64) -> ManifoldResult<()> { let user = match userinfo.get(&target) { Some(u) => match u.timezone.to_owned() { Some(_) => u, - None => {ctx.send(|f| f.content(format!("This user hasn't given me their timezone yet. Shame on them!")).reply(true)).await?; Err("NoTZ")?} + None => {ctx.send(|f| f.content(t!("commands.core.time.unknown_timezone")).reply(true)).await?; Err("NoTZ")?} }, - None => { ctx.send(|f| f.content(format!("Who's that? I don't know them.")).reply(true)).await?; Err("User not found")?} + None => { ctx.send(|f| f.content(t!("commands.core.time.unknown_user")).reply(true)).await?; Err("User not found")?} }; let current_time_utc = chrono::Utc::now(); let user_timezone: Tz = user.timezone.as_ref().unwrap().parse::().unwrap(); let adjusted_time = user_timezone.from_utc_datetime(¤t_time_utc.naive_utc()); - ctx.send(|f| f.content(format!("Time in {}'s current timezone of {} is {}", user.username, user_timezone.name().to_string(), adjusted_time.time().to_string())).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.core.time.time_output", name = user.username, timezone = user_timezone.name().to_string(), time = adjusted_time.time().to_string())).reply(true)).await?; Ok(()) } diff --git a/src/commands/joke.rs b/src/commands/joke.rs index 3adaa95..1559bd2 100644 --- a/src/commands/joke.rs +++ b/src/commands/joke.rs @@ -15,7 +15,7 @@ async fn dad_joke(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { ctx.send(|f| f.content(format!("{}", tip)).reply(true)).await?; } else { error!("No such fuel tank dad_jokes"); - ctx.send(|f| f.content("Can't find any dad jokes. Maybe your dad isn't funny.".to_string()).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.dad_joke.no_jokes")).reply(true)).await?; } Ok(()) diff --git a/src/commands/nasa.rs b/src/commands/nasa.rs index b030716..480dafa 100644 --- a/src/commands/nasa.rs +++ b/src/commands/nasa.rs @@ -24,7 +24,7 @@ async fn nasa_apod(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { }).await?; } else { error!("No such fuel tank nasa_apod"); - ctx.send(|f| f.content("The NASA Astronomy Photo of the Day was missing. Try looking up instead.".to_string()).reply(true)).await?; + ctx.send(|f| f.content(t!("commands.nasa_apod.no_picture")).reply(true)).await?; } Ok(()) diff --git a/src/commands/weather.rs b/src/commands/weather.rs index dc994e9..4bafa41 100644 --- a/src/commands/weather.rs +++ b/src/commands/weather.rs @@ -12,7 +12,7 @@ use crate::helpers::paginate; #[poise::command(slash_command, prefix_command, aliases("w"))] async fn weather(ctx: ManifoldContext<'_>, #[rest] #[description="Location to look up weather for"] location: Option) -> ManifoldResult<()> { - let my_message = ctx.say("Retrieving weather, be patient").await?; + let my_message = ctx.say(t!("commands.weather.retrieve_wait")).await?; let weather_forecast = _get_weather(ctx, &my_message, location).await?; @@ -26,20 +26,20 @@ async fn weather(ctx: ManifoldContext<'_>, #[rest] #[description="Location to lo d.hour.iter().for_each(|f| { pages.push(CreateEmbed::default() .colour(card_colour) - .title(format!("Current weather at {}, {}, {}", weather_forecast.location.name, weather_forecast.location.region, weather_forecast.location.country)) - .description(format!("Forecast for {} made at {}.", f.time, weather_forecast.current.last_updated)) + .title(t!("commands.weather.current_weather", location_name = weather_forecast.location.name, location_region = weather_forecast.location.region, location_country = weather_forecast.location.country)) + .description(t!("commands.weather.forecast_time", forecast_time = f.time, forecast_updated = weather_forecast.current.last_updated)) .image(format!("https:{}", f.condition.icon)) .fields(vec![ - ("Temperature (Dewpoint)", format!("{}°C/{}°F ({:.1}°C/{:.1}°F)", f.temp_c, f.temp_f, f.dewpoint_c, f.dewpoint_f), true), - ("Feels like", format!("{}°C/{}°F", f.feelslike_c, f.feelslike_f), true), - ("Condition", format!("{}", f.condition.text), true), - ("Pressure", format!("{}mb/{}in", f.pressure_mb, f.pressure_in), true), - ("Precipitation", format!("{}mm/{}in", f.precip_mm, f.precip_in), true), - ("Humidity", format!("{}%", f.humidity), true), - ("Cloud coverage", format!("{}%", f.cloud), true), - ("Coordinates", format!("Lat: {} Lon: {}", weather_forecast.location.lat, weather_forecast.location.lon), true), + (t!("commands.weather.temperature_dewpoint"), format!("{}°C/{}°F ({:.1}°C/{:.1}°F)", f.temp_c, f.temp_f, f.dewpoint_c, f.dewpoint_f), true), + (t!("commands.weather.feels_like"), format!("{}°C/{}°F", f.feelslike_c, f.feelslike_f), true), + (t!("commands.weather.condition"), format!("{}", f.condition.text), true), + (t!("commands.weather.pressure"), format!("{}mb/{}in", f.pressure_mb, f.pressure_in), true), + (t!("commands.weather.precipitation"), format!("{}mm/{}in", f.precip_mm, f.precip_in), true), + (t!("commands.weather.humidity"), format!("{}%", f.humidity), true), + (t!("commands.weather.cloud_coverage"), format!("{}%", f.cloud), true), + (t!("commands.weather.coordinates"), format!("Lat: {} Lon: {}", weather_forecast.location.lat, weather_forecast.location.lon), true), ]) - .field("Wind", format!("{}mph/{}kph from the {} ({} degrees), gusting to {}mph/{}kph", f.wind_mph, f.wind_kph, f.wind_dir, f.wind_degree, f.gust_mph, f.gust_kph), false) + .field(t!("commands.weather.wind"), t!("commands.weather.wind_value", wind_mph = f.wind_mph, wind_kph = f.wind_kph, wind_dir = f.wind_dir, wind_degree = f.wind_degree, gust_mph = f.gust_mph, gust_kph = f.gust_kph), false) .footer(|f| { f.text(format!("{}", responses.get_response(&"weather card footer".to_string()).unwrap_or(&"Weather Powered By Deez Nutz".to_string()))); f @@ -76,7 +76,7 @@ pub async fn save_weather_location(ctx: ManifoldContext<'_>, #[rest] #[descripti userinfo.insert(ctx.author().id.as_u64().clone(), new_user); } - ctx.say(format!("Okay. Now I know that you live in {}. Are you sure that was wise?", &location)).await?; + ctx.say(t!("commands.weather_location.location_save_confirm", location = &location)).await?; Ok(()) } @@ -85,7 +85,6 @@ pub async fn _get_weather(ctx: ManifoldContext<'_>, message_handle: &ReplyHandle let mut weather_location: String = String::default(); - let responses = &ctx.data().responses; let config = &ctx.data().bot_config; let userinfo = &mut ctx.data().user_info.lock().await; @@ -105,13 +104,13 @@ pub async fn _get_weather(ctx: ManifoldContext<'_>, message_handle: &ReplyHandle }, None => { message_handle.edit(ctx, |m| { - m.content = responses.get_response(&"weather noloc".to_string()).cloned(); m + m.content = Some(t!("commands.weather.no_location").to_string()); m }).await?; Err("No location provided")?; } }; } else { - ctx.say(responses.get_response(&"weather noloc".to_string()).unwrap_or(&"B I don't know where you live, and you didn't tell me, so I can't help. Look out of the window.".to_string())).await?; + ctx.say(t!("commands.weather.no_location")).await?; Err("No location provided")?; } } @@ -121,7 +120,7 @@ pub async fn _get_weather(ctx: ManifoldContext<'_>, message_handle: &ReplyHandle let weather_forecast: WeatherForecastRequestResponse = match weather_client.get_weather_forecast(weather_location).await { Ok(w) => w, Err(e) => { - ctx.say(format!("Something went wrong. Maybe there is no weather there. {:?}", e)).await?; + ctx.say(t!("commands.weather.no_weather", error_output = format!("{:?}", e))).await?; Err("unknown")? } }; diff --git a/src/config.rs b/src/config.rs index d182a4c..c1d1c86 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,6 +27,7 @@ pub struct ManifoldConfig { pub services: HashMap, pub database: DatabaseConfig, pub responses_file_path: PathBuf, + pub locale: String, } impl ManifoldConfig { diff --git a/src/events.rs b/src/events.rs index c174fc3..b25912c 100644 --- a/src/events.rs +++ b/src/events.rs @@ -54,7 +54,7 @@ impl Handler { let greeting = match responses.get_response(&"bot startup".to_string()) { Some(g) => g.to_owned(), - None => "Manifold bot connected to discord and ready to begin broadcast operations.".to_string(), + None => t!("logging.bot_start").to_string(), }; for guild in &data_about_bot.guilds { @@ -79,7 +79,7 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("{} was banned", banned_user.name)) + .title(t!("logging.user_banned", user = banned_user.name)) .colour(Colour::from_rgb(255, 0, 0)) .timestamp(Timestamp::now()) }) @@ -96,7 +96,7 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("Ban was lifted for {}", unbanned_user.name)) + .title(t!("logging.user_unbanned", user = unbanned_user.name)) .colour(Colour::from_rgb(0, 255, 0)) .timestamp(Timestamp::now()) }) @@ -113,7 +113,7 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("{} joined the server with nickname {} ({})", new_member.user.name, new_member.nick.as_ref().unwrap_or(&new_member.user.name), new_member.user.id)) + .title(t!("logging.user_joined", name = new_member.user.name, nickname = new_member.nick.as_ref().unwrap_or(&new_member.user.name), uid = new_member.user.id)) .colour(Colour::from_rgb(0, 255, 0)) .timestamp(new_member.joined_at.unwrap_or(Timestamp::now())) .field("Account creation date", new_member.user.created_at(), false) @@ -131,7 +131,7 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("{} left the server", user.name)) + .title(t!("logging.user_left", name = user.name)) .colour(Colour::from_rgb(255, 0, 0)) .timestamp(Timestamp::now()) }) @@ -153,13 +153,13 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("New role {} ({}) was created", &new.name, &new.id)) + .title(t!("logging.role_created", name = &new.name, role_id = &new.id)) .colour(Colour::from_rgb(0, 255, 0)) .timestamp(Timestamp::now()) - .field("Role Permissions", &new.permissions, false) - .field("Hoist", &new.hoist, true) - .field("Icon", &new.icon.clone().unwrap_or("None set".to_string()), true) - .field("Mentionable", &new.mentionable, true) + .field(t!("logging.role_created.permissions"), &new.permissions, false) + .field(t!("logging.role_created.hoist"), &new.hoist, true) + .field(t!("logging.role_created.icon"), &new.icon.clone().unwrap_or("None set".to_string()), true) + .field(t!("logging.role_created.mentionable"), &new.mentionable, true) }) }).await?; @@ -179,17 +179,17 @@ impl Handler { match removed_role_data_if_available { Some(role) => { return e - .title(format!("Role {} ({}) was deleted", &role.name, &role.id)) + .title(t!("logging.role_deleted", name = &role.name, role_id = &role.id)) .colour(Colour::from_rgb(255, 0, 0)) .timestamp(Timestamp::now()) - .field("Role Permissions", &role.permissions, false) - .field("Hoist", &role.hoist, true) - .field("Icon", &role.icon.clone().unwrap_or("None set".to_string()), true) - .field("Mentionable", &role.mentionable, true) + .field(t!("logging.role_deleted.permissions"), &role.permissions, false) + .field(t!("logging.role_deleted.hoist"), &role.hoist, true) + .field(t!("logging.role_deleted.icon"), &role.icon.clone().unwrap_or("None set".to_string()), true) + .field(t!("logging.role_deleted.mentionable"), &role.mentionable, true) }, None => { return e - .title(format!("Role {} was deleted (Role was not cached)", removed_role_id)) + .title(t!("logging.role_deleted.not_cached", role_id = removed_role_id)) .colour(Colour::from_rgb(255, 0, 0)) .timestamp(Timestamp::now()) } @@ -235,7 +235,7 @@ impl Handler { let channel = match ctx.cache.guild_channel(channel_id) { Some(c) => c.name, - None => format!("Not Available / Not Cached ({})", channel_id) + None => t!("global.not_available_with_id", id = channel_id).to_string() }; match ctx.cache.message(channel_id, deleted_message_id) { @@ -251,11 +251,11 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("Message removed in #{} ({})", channel, channel_id)) + .title(t!("logging.message_deleted", channel = channel, channel_id = channel_id)) .colour(Colour::from_rgb(255, 0, 0)) .author(|a| a.name(&msg.author.name)) - .field("Message Content", msg.content_safe(&ctx.cache), false) - .field("Message Attachments", attachment_urls.join(", "), false) + .field(t!("logging.message_deleted.content"), msg.content_safe(&ctx.cache), false) + .field(t!("logging.message_deleted.attachments"), attachment_urls.join(", "), false) .timestamp(Timestamp::now()) .footer(|f| f.text(&msg.author.id)); for attachment in &msg.attachments { @@ -271,7 +271,7 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("Message removed in #{} ({}) (Not cached)", channel, channel_id)) + .title(t!("logging.message_deleted.not_cached", channel_name = channel, channel_id = channel_id)) .colour(Colour::from_rgb(255, 0, 0)) .timestamp(Timestamp::now()) }) @@ -292,17 +292,17 @@ impl Handler { .content("") .embed(|e| { e - .title(format!("Message updated in #{} ({})", message_channel, new.channel_id)) + .title(t!("logging.message_edited", channel_name = message_channel, channel_id = new.channel_id)) .colour(Colour::from_rgb(255, 153, 0)) .author(|a| a.name(new.author.name.clone())) .timestamp(Timestamp::now()) - .field("Original Content", match original.as_ref() { + .field(t!("logging.message_edited.original_content"), match original.as_ref() { Some(m) => m.content.clone(), - None => "Not available".to_string(), + None => t!("global.not_available").to_string(), }, false) - .field("New Content", new.content.clone(), false) - .field("Message created at", new.timestamp, true) - .field("Message author", &new.author.id, true) + .field(t!("logging.message_edited.new_content"), new.content.clone(), false) + .field(t!("logging.message_edited.created_at"), new.timestamp, true) + .field(t!("logging.message_edited.author"), &new.author.id, true) }) }).await?; } diff --git a/src/lib.rs b/src/lib.rs index 9225eba..ef6c81a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #[macro_use] extern crate log; +#[macro_use] extern crate rust_i18n; #[macro_use] extern crate serde; use std::env; @@ -71,6 +72,8 @@ pub struct ManifoldDataInner { pub type ManifoldContext<'a> = poise::Context<'a, ManifoldData, ManifoldError>; pub type ManifoldCommand = poise::Command; +i18n!("config/locales"); + pub async fn prepare_client(arguments: ArgMatches, intents: GatewayIntents, injected_commands: Vec, caller_version_string: String, caller_database_migrations: EmbeddedMigrations) -> ManifoldResult> { let bot_environment = arguments.get_one("environment").unwrap(); let config_file = arguments.get_one::("config-file").unwrap(); @@ -79,6 +82,8 @@ pub async fn prepare_client(arguments: ArgMatches, intents: Gat debug!("Configuration file path: {}", &config_file); let bot_config = ManifoldConfig::new(&config_file, bot_environment).expect(&*format!("Could not read configuration file {}", &config_file)); + rust_i18n::set_locale(bot_config.locale.as_str()); + let manager = ConnectionManager::::new(format!("postgresql://{user}:{pass}@{host}:{port}/{database}", user=&bot_config.database.user, pass=&bot_config.database.pass, host=&bot_config.database.host, port=&bot_config.database.port, database=&bot_config.database.database_name)); let pool = r2d2::Pool::builder() .max_size(1) @@ -100,7 +105,7 @@ pub async fn prepare_client(arguments: ArgMatches, intents: Gat }), pre_command: |ctx: ManifoldContext<'_>| Box::pin(async move { info!("Received command {} from {}", ctx.command().name, ctx.author().name); - let _ = ctx.data().bot_config.channels.log.say(ctx, format!("Received command {} from {}", ctx.command().name, ctx.author().name)).await; + let _ = ctx.data().bot_config.channels.log.say(ctx, t!("logging.logged_command", command = ctx.command().name, name = ctx.author().name)).await; }), commands: commands::collect_commands(injected_commands), prefix_options: poise::PrefixFrameworkOptions { @@ -126,7 +131,7 @@ pub async fn prepare_client(arguments: ArgMatches, intents: Gat database: db, responses, user_info: Mutex::new(user_info), - version_string: format!("{caller} (Manifold framework version {mfold_ver} built at {mfold_time} from revision {mfold_rev})", caller=caller_version_string, mfold_ver=built_info::PKG_VERSION, mfold_time=built_info::BUILT_TIME_UTC, mfold_rev=git_info), + version_string: t!("misc.version_string", caller=caller_version_string, mfold_ver=built_info::PKG_VERSION, mfold_time=built_info::BUILT_TIME_UTC, mfold_rev=git_info).to_string(), }))) }) });