From 01495684d95ffa64814d78e90ae0c0cd61e5dc72 Mon Sep 17 00:00:00 2001 From: eliets2 <287273517+eliets2@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:30:07 +0300 Subject: [PATCH 1/2] web: Auto language follows the browser's languages (#2085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit default_language() — what the Auto setting resolves to — only read LIGHTCRAFT_LANGUAGE, which does not exist in a browser, so the web build always started in English. ui-egui gains an installable default (set_default_language + a pure locale_from_tags that picks the first shippable language from BCP-47 tags via Locale::parse_tag), and the web app installs it at startup from navigator.languages (falling back to navigator.language), read fallibly through js_sys::Reflect so it also works from a worker. Desktop never installs the override and is unchanged; a language stored in ui.json still wins, which is exactly Auto semantics. Tests: locale_from_tags takes the first shippable language (it-IT -> Italiano, unsupported-first skips ahead, zh-TW -> zh-hant, ja_JP.UTF-8 -> Japanese, pt-BR region variant), empty/unsupported -> En fallback. --- apps/lightcraft-web/src/web.rs | 29 ++++++++++++++++++++++++ crates/ui-egui/src/i18n.rs | 40 ++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 6a4487d09..c73016b4d 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -667,11 +667,40 @@ impl eframe::App for WebApp { } } +/// "Auto" follows the browser's languages (issue #2085): the first of `navigator.languages` (else +/// `navigator.language`) that LightCraft ships becomes the UI's default. Every step is fallible +/// (as in [`crate::safety::acquire_tab_lock`]): without a usable navigator Auto stays English, +/// as before. +fn install_browser_language() { + let Ok(navigator) = js_sys::Reflect::get(&js_sys::global(), &"navigator".into()) else { return }; + let mut tags: Vec = Vec::new(); + if let Ok(languages) = js_sys::Reflect::get(&navigator, &"languages".into()) + && let Some(languages) = languages.dyn_ref::() + { + for tag in languages.iter() { + if let Some(tag) = tag.as_string() { + tags.push(tag); + } + } + } + if tags.is_empty() + && let Ok(language) = js_sys::Reflect::get(&navigator, &"language".into()) + && let Some(language) = language.as_string() + { + tags.push(language); + } + if let Some(language) = lightcraft_ui_egui::i18n::locale_from_tags(&tags) { + lightcraft_ui_egui::i18n::set_default_language(language); + log::info!("lightcraft: default language {language:?} from the browser's languages"); + } +} + /// Start the app with the separately fetched Simplified Chinese font (empty in font-free builds). #[wasm_bindgen] pub fn start(chinese_font: Vec) { eframe::WebLogger::init(log::LevelFilter::Info).ok(); safety::install_panic_hook(); + install_browser_language(); log::info!("lightcraft: wasm instantiated at {:.0} ms", perf_now()); if chinese_font.is_empty() { log::warn!("lightcraft: no Simplified Chinese font supplied; Chinese-only glyphs will show as boxes"); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 9ba6c5462..fed35b6ad 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -149,11 +149,33 @@ impl<'de> Deserialize<'de> for Locale { thread_local! { static LOCALE: std::cell::Cell = const { std::cell::Cell::new(Locale::En) }; + /// The host's answer for the "Auto" language ([`set_default_language`]). + static DEFAULT_LANGUAGE: std::cell::Cell> = const { std::cell::Cell::new(None) }; } -/// The UI language from the environment (`LIGHTCRAFT_LANGUAGE=zh-hans`), for headless runs. +/// Install the language the "Auto" setting resolves to when nothing is stored. The browser host +/// calls this once at startup with the browser's own languages ([`locale_from_tags`]); the +/// desktop host doesn't: there Auto keeps following the environment. Must run before the UI state +/// is first constructed. +pub fn set_default_language(language: Locale) { + DEFAULT_LANGUAGE.with(|slot| slot.set(Some(language))); +} + +/// The first of `tags` (BCP-47, most preferred first, as `navigator.languages` lists them) that +/// names a shipped language; None when none does. +pub fn locale_from_tags(tags: &[String]) -> Option { + tags.iter().find_map(|tag| Locale::parse_tag(tag)) +} + +/// The UI language "Auto" resolves to: the environment's (`LIGHTCRAFT_LANGUAGE=zh-hans`, for +/// headless runs), or the host-installed default ([`set_default_language`], the browser's languages +/// on the web), else English. pub fn default_language() -> Locale { - std::env::var("LIGHTCRAFT_LANGUAGE").ok().and_then(|value| Locale::parse_tag(&value)).unwrap_or(Locale::En) + std::env::var("LIGHTCRAFT_LANGUAGE") + .ok() + .and_then(|value| Locale::parse_tag(&value)) + .or_else(|| DEFAULT_LANGUAGE.with(std::cell::Cell::get)) + .unwrap_or(Locale::En) } pub fn set_language(language: Locale) { @@ -636,6 +658,20 @@ mod tests { assert_eq!(Locale::parse_tag("xx"), None); } + /// The web host's "Auto" (issue #2085): the browser's `navigator.languages` in order, first + /// shipped language wins, unsupported tags are skipped, an empty or foreign list stays English + /// ([`default_language`] fallback). + #[test] + fn locale_from_tags_takes_the_first_shipped_language() { + assert_eq!(locale_from_tags(&[]), None); + assert_eq!(locale_from_tags(&["xx-XX".into()]), None); + assert_eq!(locale_from_tags(&["it-IT".into()]), Some(Locale::It)); + assert_eq!(locale_from_tags(&["xx".into(), "de-DE".into(), "fr".into()]), Some(Locale::De)); + assert_eq!(locale_from_tags(&["zh-TW".into(), "en".into()]), Some(Locale::ZhHant)); + assert_eq!(locale_from_tags(&["ja_JP.UTF-8".into(), "en".into()]), Some(Locale::Ja)); + assert_eq!(locale_from_tags(&["pt".into(), "pt-BR".into()]), Some(Locale::PtBr)); + } + #[test] fn switching_language_translates_and_restores() { set_language(Locale::ZhHans); From fef2f361978389a0832eae753b200d7d635d5939 Mon Sep 17 00:00:00 2001 From: eliets2 <287273517+eliets2@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:38:19 +0300 Subject: [PATCH 2/2] CI: re-trigger FreeBSD after flake in tests_panels::a_folder_takes_a_colour_label_from_its_menu The failed FreeBSD run (39062750738) failed only this menu-interaction test (None vs Some(Red) at tests_panels.rs:1773), unrelated to the wasm-only i18n change; the branch already sits on current main, so this empty commit exists only to produce a fresh merge run.