web: Match System follows ?lang= and the browser's languages

Match System (the default) stayed English in the browser build, which has
no system-locale probe (#229). The page now passes ?lang= from the address,
then navigator.languages, and Match System takes the first of them with a
catalog (an English tag counts), else English. A language chosen in
Settings still wins; native builds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
(cherry picked from commit 73c064e3858068ed19fbfafbdd0b3c8cc229a42e)
This commit is contained in:
Davit Matchakhelidze
2026-10-14 22:50:03 +04:00
parent a1a1a36a41
commit 5539bb4a95
4 changed files with 39 additions and 4 deletions
+1
View File
@@ -50,6 +50,7 @@ web-sys = { version = "0.3", features = [
"HtmlElement",
"HtmlInputElement",
"Location",
"Navigator",
"Url",
"UrlSearchParams",
"Window",
+5
View File
@@ -184,6 +184,11 @@ fn install_hooks(app: &mut EffectcraftApp) {
#[wasm_bindgen]
pub async fn start(canvas_id: String) -> Result<(), JsValue> {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
// Match System follows `?lang=` in the address, then the browser's languages; a language
// chosen in Settings still wins.
let navigator = web_sys::window().map(|w| w.navigator());
let browser = navigator.iter().flat_map(|n| n.languages().iter().filter_map(|v| v.as_string()).collect::<Vec<_>>());
effectcraft_ui_egui::i18n::set_system_locales(query_value("lang").into_iter().chain(browser).collect());
let doc = web_sys::window().and_then(|w| w.document()).ok_or("no document")?;
let canvas: web_sys::HtmlCanvasElement = doc.get_element_by_id(&canvas_id).ok_or("no canvas")?.dyn_into()?;
let empty = query_flag("empty");
+32 -3
View File
@@ -66,16 +66,35 @@ pub(crate) fn czech(app: &EffectcraftApp) -> bool {
/// Settings ▸ General ▸ Language ▸ Match System: the operating system's interface language
/// where EffectCraft has it, else English (#229), as After Effects installs in the system's
/// language. The browser build stays in English because it has no system-locale probe.
/// language. The browser build follows the locales its page passes in ([`set_system_locales`]).
fn system_language() -> &'static str {
#[cfg(not(target_arch = "wasm32"))]
{
static LANGUAGE: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
LANGUAGE.get_or_init(|| supported(sys_locale::get_locale().as_deref()))
}
// No system locale in the browser build: English.
#[cfg(target_arch = "wasm32")]
supported(None)
first_supported(SYSTEM_LOCALES.get().map(Vec::as_slice).unwrap_or_default())
}
/// The browser's preferred languages, most preferred first (`?lang=` in the address, then
/// `navigator.languages`), for Match System in the web build.
static SYSTEM_LOCALES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
/// Tell Match System the system's preferred languages (the web build has no locale probe of its
/// own). Set before the first frame; only the first call counts.
pub fn set_system_locales(tags: Vec<String>) {
let _ = SYSTEM_LOCALES.set(tags);
}
/// The first of `tags` EffectCraft has a catalog for (English counts), else English: someone who
/// prefers Dutch, then Czech, gets Czech.
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
fn first_supported(tags: &[String]) -> &'static str {
tags.iter()
.map(|t| (t, supported(Some(t))))
.find(|(t, code)| *code != "en" || t.split(['-', '_']).next().is_some_and(|p| p.eq_ignore_ascii_case("en")))
.map_or("en", |(_, code)| code)
}
/// The language EffectCraft shows for a BCP 47 locale (`ja-JP` → `ja`, `zh-Hans-CN` → `zh-hans`,
@@ -6643,6 +6662,16 @@ mod tests {
assert_eq!(command_label("window.panel", "Project"), "Project");
}
#[test]
fn match_system_takes_the_first_browser_language_with_a_catalog() {
let tags = |t: &[&str]| t.iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert_eq!(super::first_supported(&tags(&["cs-CZ", "cs", "en"])), "cs");
assert_eq!(super::first_supported(&tags(&["nl-NL", "cs"])), "cs");
assert_eq!(super::first_supported(&tags(&["en-US", "cs"])), "en");
assert_eq!(super::first_supported(&tags(&["nl-NL"])), "en");
assert_eq!(super::first_supported(&tags(&[])), "en");
}
#[test]
fn czech_translates_menus_without_changing_bindings_or_user_names() {
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
+1 -1
View File
@@ -30,7 +30,7 @@ Commands (CLI, MCP, control channel):
### Settings that change behaviour
- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant` / `uk` / `es` / `pt-br`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`, a Ukrainian one (`uk`, `uk-UA`, `uk_UA.UTF-8`) selects `uk`, a Spanish one (`es`, `es-ES`, `es_MX.UTF-8`) selects `es`, a Portuguese one (`pt`, `pt-BR`, `pt_PT.UTF-8`) selects `pt-br`; the browser build stays in English with it. Choose **Español** or **Português (Brasil)** for Spanish or Brazilian Portuguese menus (panels stay in English for now); choose **Українська** for Ukrainian menus and cataloged panel, dialog, button and tooltip text; Ukrainian uses the bundled UI fonts. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. The same setting picks the CJK fallback for the interface *and* the Han preference of the text engine, so the composition's own text is drawn with the same language's glyph forms (the text engine's preference is process-global); swapping the language reinstalls the fonts, so it applies without a restart. Two catalogs back the text: the menus are keyed by `(command, English label)` (`crates/ui-egui/src/i18n.rs`), and the rest of the interface - panels, dialogs, buttons, tooltips - is keyed by its English source string (`crates/ui-egui/src/i18n/ui.rs`, with rows for Japanese, Simplified and Traditional Chinese and Ukrainian), where a panel writes `tr("Rename")`, or `tr_args("Proxy Use: {}", &[&quality])` when the row carries a value. A row that is missing falls back to English, so a new string shows up untranslated rather than empty; the tests in that module fail on an unused row and on a hard-coded string in a converted panel.
- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant` / `uk` / `es` / `pt-br`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`, a Ukrainian one (`uk`, `uk-UA`, `uk_UA.UTF-8`) selects `uk`, a Spanish one (`es`, `es-ES`, `es_MX.UTF-8`) selects `es`, a Portuguese one (`pt`, `pt-BR`, `pt_PT.UTF-8`) selects `pt-br`; the browser build follows `?lang=` in the address, then the browser's languages (`navigator.languages`). Choose **Español** or **Português (Brasil)** for Spanish or Brazilian Portuguese menus (panels stay in English for now); choose **Українська** for Ukrainian menus and cataloged panel, dialog, button and tooltip text; Ukrainian uses the bundled UI fonts. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. The same setting picks the CJK fallback for the interface *and* the Han preference of the text engine, so the composition's own text is drawn with the same language's glyph forms (the text engine's preference is process-global); swapping the language reinstalls the fonts, so it applies without a restart. Two catalogs back the text: the menus are keyed by `(command, English label)` (`crates/ui-egui/src/i18n.rs`), and the rest of the interface - panels, dialogs, buttons, tooltips - is keyed by its English source string (`crates/ui-egui/src/i18n/ui.rs`, with rows for Japanese, Simplified and Traditional Chinese and Ukrainian), where a panel writes `tr("Rename")`, or `tr_args("Proxy Use: {}", &[&quality])` when the row carries a value. A row that is missing falls back to English, so a new string shows up untranslated rather than empty; the tests in that module fail on an unused row and on a hard-coded string in a converted panel.
- `general.undoLevels`: Levels of Undo
- `general.pathPointSize`: Path Point and Handle Size
- `general.recentItems`: Recent Projects Shown