Merge PR #2181: web: Auto language follows the browser's languages (#2085)

#2089 proposed the same fix for #2085 earlier.
This commit is contained in:
aiongg
2026-10-21 14:53:26 +00:00
2 changed files with 67 additions and 2 deletions
+29
View File
@@ -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<String> = Vec::new();
if let Ok(languages) = js_sys::Reflect::get(&navigator, &"languages".into())
&& let Some(languages) = languages.dyn_ref::<js_sys::Array>()
{
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<u8>) {
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");
+38 -2
View File
@@ -149,11 +149,33 @@ impl<'de> Deserialize<'de> for Locale {
thread_local! {
static LOCALE: std::cell::Cell<Locale> = const { std::cell::Cell::new(Locale::En) };
/// The host's answer for the "Auto" language ([`set_default_language`]).
static DEFAULT_LANGUAGE: std::cell::Cell<Option<Locale>> = 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<Locale> {
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);