mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-07-26 21:57:05 +00:00
feat(gui): let the UI language be chosen manually (#179)
The interface followed the system locale with no way to override it. Settings now carry a language picker, stored as forced_lang in gui.json, with "System" listed first so the choice stays undoable. rust-i18n keeps the current locale in a process-wide atomic and t! reads it on every lookup, so switching takes effect on the next frame without a restart. Languages are listed by their own name rather than a translated one, since that is what a reader looking for their language recognises. A test asserts every shipped locale has such a name, so adding a translation without naming it fails rather than silently showing a bare language code. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7b87dc02dd
commit
389640ee0a
@@ -561,3 +561,25 @@ ar = "مستوى صوت الميكروفون"
|
|||||||
kz = "Микрофон дыбысы"
|
kz = "Микрофон дыбысы"
|
||||||
he = "עוצמת מיקרופון"
|
he = "עוצמת מיקרופון"
|
||||||
pt-BR = "Volume do microfone"
|
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"
|
||||||
|
|||||||
@@ -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::{Align, Button, Color32, ComboBox, Layout, RichText, Slider, Ui};
|
||||||
use egui_material_icons::icons::ICON_ARROW_BACK;
|
use egui_material_icons::icons::ICON_ARROW_BACK;
|
||||||
use pwsp_lib::types::config::PreferredTheme;
|
use pwsp_lib::types::config::PreferredTheme;
|
||||||
@@ -64,6 +64,7 @@ impl SoundpadGui {
|
|||||||
// ---------- Selectors -----------
|
// ---------- Selectors -----------
|
||||||
self.draw_mic_selection(ui);
|
self.draw_mic_selection(ui);
|
||||||
self.draw_output_selection(ui);
|
self.draw_output_selection(ui);
|
||||||
|
self.draw_language_selection(ui);
|
||||||
|
|
||||||
let mut selected_theme = self.config.preferred_theme.clone();
|
let mut selected_theme = self.config.preferred_theme.clone();
|
||||||
ComboBox::from_label(t!("gui.settings.theme.label"))
|
ComboBox::from_label(t!("gui.settings.theme.label"))
|
||||||
@@ -184,4 +185,32 @@ impl SoundpadGui {
|
|||||||
self.set_output(selected_output);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String> {
|
||||||
|
let mut locales: Vec<String> = 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
mod gui;
|
mod gui;
|
||||||
|
mod locale;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
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 rust_i18n::i18n;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
@@ -13,8 +14,7 @@ i18n!("locales", fallback = "en");
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let locale = sys_locale::get_locale().unwrap_or(String::from("en-US"));
|
locale::apply(&get_gui_config());
|
||||||
rust_i18n::set_locale(&locale);
|
|
||||||
|
|
||||||
let args = env::args().skip(1).collect::<Vec<String>>();
|
let args = env::args().skip(1).collect::<Vec<String>>();
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ pub struct GuiConfig {
|
|||||||
pub dirs_settings: HashMap<PathBuf, DirSettings>,
|
pub dirs_settings: HashMap<PathBuf, DirSettings>,
|
||||||
|
|
||||||
pub preferred_theme: PreferredTheme,
|
pub preferred_theme: PreferredTheme,
|
||||||
|
/// UI language code, or `None` to follow the system locale.
|
||||||
|
pub forced_lang: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SortOrder {
|
impl SortOrder {
|
||||||
@@ -130,6 +132,7 @@ impl Default for GuiConfig {
|
|||||||
|
|
||||||
preferred_theme: PreferredTheme::System,
|
preferred_theme: PreferredTheme::System,
|
||||||
dirs_settings: HashMap::new(),
|
dirs_settings: HashMap::new(),
|
||||||
|
forced_lang: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user