diff --git a/pwsp-gui/locales/app.toml b/pwsp-gui/locales/app.toml index ccea440..e11c2fb 100644 --- a/pwsp-gui/locales/app.toml +++ b/pwsp-gui/locales/app.toml @@ -561,3 +561,25 @@ ar = "مستوى صوت الميكروفون" kz = "Микрофон дыбысы" he = "עוצמת מיקרופון" pt-BR = "Volume do microfone" + +[gui.settings.language.label] +en = "Language" +ru = "Язык" +es = "Idioma" +fr = "Langue" +zh = "语言" +ar = "اللغة" +kz = "Тіл" +he = "שפה" +pt-BR = "Idioma" + +[gui.settings.language.system] +en = "System" +ru = "Системный" +es = "Sistema" +fr = "Système" +zh = "系统" +ar = "النظام" +kz = "Жүйе" +he = "מערכת" +pt-BR = "Sistema" diff --git a/pwsp-gui/src/gui/views/settings.rs b/pwsp-gui/src/gui/views/settings.rs index 45c1513..600df40 100644 --- a/pwsp-gui/src/gui/views/settings.rs +++ b/pwsp-gui/src/gui/views/settings.rs @@ -1,4 +1,4 @@ -use crate::gui::SoundpadGui; +use crate::{gui::SoundpadGui, locale}; use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Slider, Ui}; use egui_material_icons::icons::ICON_ARROW_BACK; use pwsp_lib::types::config::PreferredTheme; @@ -64,6 +64,7 @@ impl SoundpadGui { // ---------- Selectors ----------- self.draw_mic_selection(ui); self.draw_output_selection(ui); + self.draw_language_selection(ui); let mut selected_theme = self.config.preferred_theme.clone(); ComboBox::from_label(t!("gui.settings.theme.label")) @@ -184,4 +185,32 @@ impl SoundpadGui { self.set_output(selected_output); } } + + fn draw_language_selection(&mut self, ui: &mut Ui) { + let mut selected = self.config.forced_lang.clone(); + let previous = selected.clone(); + + let selected_text = match &selected { + Some(code) => locale::display_name(code).to_string(), + None => t!("gui.settings.language.system").to_string(), + }; + + ComboBox::from_label(t!("gui.settings.language.label")) + .height(30.0) + .selected_text(selected_text) + .show_ui(ui, |ui| { + // Listed first so picking a language stays undoable. + ui.selectable_value(&mut selected, None, t!("gui.settings.language.system")); + for code in locale::available() { + let name = locale::display_name(&code); + ui.selectable_value(&mut selected, Some(code.clone()), name); + } + }); + + if selected != previous { + self.config.forced_lang = selected; + self.config.save_to_file().ok(); + locale::apply(&self.config); + } + } } diff --git a/pwsp-gui/src/locale.rs b/pwsp-gui/src/locale.rs new file mode 100644 index 0000000..1db077a --- /dev/null +++ b/pwsp-gui/src/locale.rs @@ -0,0 +1,58 @@ +use pwsp_lib::types::config::GuiConfig; + +/// Applies the UI language: the one pinned in the config, or the system's when none is. +/// +/// Safe to call at any time — `t!` reads the locale on every lookup, so the next frame +/// already renders in the new language. +pub fn apply(config: &GuiConfig) { + let locale = config.forced_lang.clone().unwrap_or_else(system_locale); + rust_i18n::set_locale(&locale); +} + +fn system_locale() -> String { + sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")) +} + +/// Locales shipped under `locales/`, in a stable order for the menu. +pub fn available() -> Vec { + let mut locales: Vec = rust_i18n::available_locales!() + .into_iter() + .map(|locale| locale.to_string()) + .collect(); + locales.sort_unstable(); + locales +} + +/// How a language names itself, which is how it should appear in a language picker +/// regardless of the language the rest of the UI is in. An unnamed locale falls back to +/// its code, so a newly shipped translation is still selectable. +pub fn display_name(code: &str) -> &str { + match code { + "en" => "English", + "ru" => "Русский", + "es" => "Español", + "fr" => "Français", + "zh" => "中文", + "ar" => "العربية", + "kz" => "Қазақша", + "he" => "עברית", + "pt-BR" => "Português (Brasil)", + other => other, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_shipped_locale_has_a_native_name() { + for code in available() { + assert_ne!( + display_name(&code), + code, + "locale {code} falls back to its code in the picker" + ); + } + } +} diff --git a/pwsp-gui/src/main.rs b/pwsp-gui/src/main.rs index bab97ad..7c0cf56 100644 --- a/pwsp-gui/src/main.rs +++ b/pwsp-gui/src/main.rs @@ -1,7 +1,8 @@ mod gui; +mod locale; use anyhow::{Context, Result, anyhow}; -use pwsp_lib::utils::gui::ensure_pwsp_audio_dir; +use pwsp_lib::utils::gui::{ensure_pwsp_audio_dir, get_gui_config}; use rust_i18n::i18n; use std::{ env, @@ -13,8 +14,7 @@ i18n!("locales", fallback = "en"); #[tokio::main] async fn main() -> Result<()> { - let locale = sys_locale::get_locale().unwrap_or(String::from("en-US")); - rust_i18n::set_locale(&locale); + locale::apply(&get_gui_config()); let args = env::args().skip(1).collect::>(); diff --git a/pwsp-lib/src/types/config.rs b/pwsp-lib/src/types/config.rs index 72b3100..3bbdd11 100644 --- a/pwsp-lib/src/types/config.rs +++ b/pwsp-lib/src/types/config.rs @@ -85,6 +85,8 @@ pub struct GuiConfig { pub dirs_settings: HashMap, pub preferred_theme: PreferredTheme, + /// UI language code, or `None` to follow the system locale. + pub forced_lang: Option, } impl SortOrder { @@ -130,6 +132,7 @@ impl Default for GuiConfig { preferred_theme: PreferredTheme::System, dirs_settings: HashMap::new(), + forced_lang: None, } } }