M7: localization layer (#128)

UI strings come from per-language TOML catalogs compiled in (English text as the key and fallback, named placeholders, one/other plurals, context suffixes); command labels are translated at display time while ids, CLI, MCP and control output stay English; accessibility names follow. Preferences > Language (System / English / Español, live; the system language from the OS or the browser), pref.language for scripts. A key check test guards the catalogs. The Spanish catalog is a machine draft awaiting native review; docs/translating.md explains adding a language.
This commit is contained in:
BFlat
2026-10-11 16:21:16 +07:00
committed by GitHub
parent 76df2d4136
commit a2e2012fdc
56 changed files with 5566 additions and 1461 deletions
+1 -1
View File
@@ -25,4 +25,4 @@ log = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "Location", "MediaQueryList"] }
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "Location", "MediaQueryList", "Navigator"] }
+4
View File
@@ -86,4 +86,8 @@ impl Services for WebServices {
fn reduce_motion(&self) -> bool {
self.reduce_motion.as_ref().is_some_and(web_sys::MediaQueryList::matches)
}
/// The browser's language (`navigator.language`).
fn system_language(&self) -> Option<String> {
web_sys::window().and_then(|w| w.navigator().language())
}
}
+3 -2
View File
@@ -25,9 +25,10 @@ webbrowser = { workspace = true }
image = { workspace = true }
env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] }
# The system's reduce-motion setting (UISettings.AnimationsEnabled).
# The system's reduce-motion setting (UISettings.AnimationsEnabled) and interface language
# (GlobalizationPreferences.Languages).
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", default-features = false, features = ["UI_ViewManagement"] }
windows = { version = "0.62", default-features = false, features = ["UI_ViewManagement", "System_UserProfile", "Foundation_Collections"] }
[build-dependencies]
winresource = "0.1"
+33 -1
View File
@@ -13,6 +13,8 @@ pub struct Desktop {
pub prefs: Option<Prefs>,
/// The system's reduce-motion setting (read in the background at startup).
reduce_motion: Arc<AtomicBool>,
/// The system's interface language (read at startup).
language: Option<String>,
}
fn config_dir() -> Option<PathBuf> {
@@ -44,7 +46,34 @@ impl Desktop {
let reduce_motion = Arc::new(AtomicBool::new(false));
let flag = reduce_motion.clone();
std::thread::spawn(move || flag.store(system_reduce_motion(), Ordering::Relaxed));
Self { clipboard: arboard::Clipboard::new().ok(), prefs, reduce_motion }
Self { clipboard: arboard::Clipboard::new().ok(), prefs, reduce_motion, language: system_language() }
}
}
/// The system's preferred interface language as a tag ("es-ES", "es_ES.UTF-8"): Windows' language
/// list, macOS's AppleLanguages, the locale variables elsewhere.
fn system_language() -> Option<String> {
#[cfg(target_os = "windows")]
{
windows::System::UserProfile::GlobalizationPreferences::Languages()
.and_then(|l| l.GetAt(0))
.map(|s| s.to_string())
.inspect_err(|e| log::info!("language: can't read the system languages: {e}"))
.ok()
}
// `defaults` prints a list: ( "es-ES", en ).
#[cfg(target_os = "macos")]
{
let out = std::process::Command::new("defaults").args(["read", "-g", "AppleLanguages"]).output().ok()?;
String::from_utf8_lossy(&out.stdout).split(['(', ',', ')']).map(|s| s.trim().trim_matches('"').to_string()).find(|s| !s.is_empty())
}
#[cfg(all(unix, not(target_os = "macos")))]
{
["LC_ALL", "LC_MESSAGES", "LANG"].iter().filter_map(|k| std::env::var(k).ok()).find(|v| !v.is_empty() && v != "C" && v != "POSIX")
}
#[cfg(not(any(windows, unix)))]
{
None
}
}
@@ -153,6 +182,9 @@ impl Services for Desktop {
fn reduce_motion(&self) -> bool {
self.reduce_motion.load(Ordering::Relaxed)
}
fn system_language(&self) -> Option<String> {
self.language.clone()
}
}
/// The window icon from the bundled app icon (if present).