diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index 9eebed53..86ca268d 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -576,7 +576,8 @@ static REQUESTED_CJK_LOCALE: RwLock> = RwLock::new(None); /// environment, so a Chinese interface is drawn with a Chinese face on a machine whose locale is /// Japanese (and the other way round). The interface sets this from its language setting; an empty /// tag, `c` or `posix` clears the request and returns to the environment. Process-global, like the -/// environment it replaces. +/// environment it replaces: every layout in the process follows it, so composition text is drawn +/// with the same language's Han forms as the interface, not only the interface chrome. pub fn set_cjk_locale(tag: &str) { let tag = tag.trim().to_ascii_lowercase(); let requested = if tag.is_empty() || tag == "c" || tag == "posix" { None } else { Some(tag) }; diff --git a/crates/ui-egui/src/header.rs b/crates/ui-egui/src/header.rs index e43f7cba..c3c0c08a 100644 --- a/crates/ui-egui/src/header.rs +++ b/crates/ui-egui/src/header.rs @@ -615,7 +615,7 @@ mod tests { fn long_press_type_keeps_the_tool_until_vertical_is_selected() { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); let ctx = egui::Context::default(); - crate::theme::install(&ctx, &app.tokens); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); frame(&mut app, &ctx, 0.0, vec![]); frame(&mut app, &ctx, 0.1, vec![]); let center = |element: &crate::automation::Element| { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index b615af32..667879b6 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2244,7 +2244,7 @@ mod tests { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); let ctx = egui::Context::default(); let tokens = crate::theme::Tokens::for_kind(crate::theme::ThemeKind::Dark); - crate::theme::install(&ctx, &tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &tokens, language(app)); crate::panels::settings::open(&mut app, "general"); let mut out = ctx.run_ui(egui::RawInput::default(), |_| { crate::panels::settings::show(&mut app, &ctx, &tokens); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9fe8f6e3..a4f0170b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -212,6 +212,8 @@ pub struct EffectcraftApp { last_time: f64, styled: bool, fonts_ready: bool, + /// The interface language the fonts were installed for; `""` until the first frame. + styled_language: &'static str, pub(crate) control_rx: Option>, pub(crate) deferred: Vec<(ControlRequest, f64)>, pub(crate) synthetic: Vec, @@ -298,6 +300,7 @@ impl EffectcraftApp { last_time: 0.0, styled: false, fonts_ready: false, + styled_language: "", control_rx: None, deferred: vec![], synthetic: vec![], @@ -1568,10 +1571,16 @@ impl eframe::App for EffectcraftApp { fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.apply_gpu_failure(ctx); - if !self.styled { - theme::install(ctx, &self.tokens, &self.session.prefs.general.language); - fit_window(ctx); - self.styled = true; + // The CJK fallback fonts depend on the interface language, so they follow a change of + // Settings ▸ General ▸ Language instead of only the first frame. + let language = crate::i18n::language(self); + if !self.styled || self.styled_language != language { + theme::install(ctx, &self.tokens, language); + self.styled_language = language; + if !self.styled { + fit_window(ctx); + self.styled = true; + } ctx.request_repaint(); } else { self.fonts_ready = true; diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 55dd5876..14984658 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -548,7 +548,7 @@ mod tests { fn app() -> (EffectcraftApp, egui::Context) { let ctx = egui::Context::default(); let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); app.session.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360})).unwrap(); (app, ctx) } diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index 9444757e..cb93f114 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -844,7 +844,7 @@ mod tests { fn premiere_export_dialog() { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); let ctx = egui::Context::default(); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); app.session.execute("comp.new", json!({"name": "Spot", "width": 64, "height": 36, "frameRate": 25, "duration": 1})).unwrap(); app.session.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap(); crate::menus::invoke(&mut app, &ctx, "file.exportTimeline", json!({})).unwrap(); diff --git a/crates/ui-egui/src/panels/home.rs b/crates/ui-egui/src/panels/home.rs index 8bb7e36c..d14e7031 100644 --- a/crates/ui-egui/src/panels/home.rs +++ b/crates/ui-egui/src/panels/home.rs @@ -540,7 +540,7 @@ mod tests { s.prefs.push_recent("/projects/Alpha.ecproj"); let mut app = EffectcraftApp::new(s); let ctx = egui::Context::default(); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); ctx.run_ui(Default::default(), |_| {}).textures_delta.clear(); ctx.run_ui(Default::default(), |ui| { app.auto.begin_frame(); @@ -573,7 +573,7 @@ mod tests { app.ui.start_screen = true; app.ui.home_learn = true; let ctx = egui::Context::default(); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); let frame = |app: &mut EffectcraftApp| { ctx.run_ui(Default::default(), |ui| { app.auto.begin_frame(); diff --git a/crates/ui-egui/src/panels/templates.rs b/crates/ui-egui/src/panels/templates.rs index aac942d2..ee739621 100644 --- a/crates/ui-egui/src/panels/templates.rs +++ b/crates/ui-egui/src/panels/templates.rs @@ -194,7 +194,7 @@ mod tests { fn gallery_lists_renders_and_opens_templates() { let mut app = app_with_store(); let ctx = egui::Context::default(); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); // File ▸ New ▸ New Project from Template… opens the Templates tab. let ev = app.session.execute("file.newFromTemplate", json!({})).unwrap(); assert_eq!(ev["frontend"], "file.newFromTemplate"); @@ -250,7 +250,7 @@ mod tests { fn my_custom_rgb_dialog() { let mut app = app_with_store(); let ctx = egui::Context::default(); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); // Menu check marks follow the active comp's viewer. app.session.execute("comp.new", json!({"name": "C", "width": 64, "height": 48})).unwrap(); let item = crate::menus::menu_items(&app).into_iter().find(|m| m.id == "view.customRgb").expect("menu item"); diff --git a/crates/ui-egui/src/panels/unsaved.rs b/crates/ui-egui/src/panels/unsaved.rs index 9e0e7067..047674f7 100644 --- a/crates/ui-egui/src/panels/unsaved.rs +++ b/crates/ui-egui/src/panels/unsaved.rs @@ -170,7 +170,7 @@ mod tests { fn app() -> (EffectcraftApp, egui::Context) { let ctx = egui::Context::default(); let app = EffectcraftApp::new(effectcraft_engine::Session::default()); - crate::theme::install(&ctx, &app.tokens, &app.session.prefs.general.language); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(app)); (app, ctx) } diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index e635ee51..18e0e1ed 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -274,9 +274,12 @@ static INTER_SEMIBOLD: &[u8] = include_bytes!("../../../assets/fonts/Inter-SemiB static JETBRAINS_MONO: &[u8] = include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"); /// Install fonts (Inter, Inter Medium/SemiBold, JetBrains Mono) and egui visuals. `language` is the -/// stored `general.language` value (`en`, `ja`, `zh-hans`, `zh-hant` or `system`): the CJK fallback -/// that draws the interface's own script is registered first, so a Chinese interface is drawn by a -/// Chinese face and a Japanese one by a Japanese face. +/// resolved interface language from [`crate::i18n::language`] (`en`, `ja`, `zh-hans` or `zh-hant`), +/// not the stored `general.language` preference: `system` is already resolved there, so a Chinese +/// system draws Chinese with the default setting. The CJK fallback that draws the interface's own +/// script is registered first, so a Chinese interface is drawn by a Chinese face and a Japanese one +/// by a Japanese face. `install` is called again when the language changes (see the `EffectcraftApp` +/// frame loop), so the fonts do not need a restart. pub fn install(ctx: &egui::Context, t: &Tokens, language: &str) { let mut fonts = FontDefinitions::default(); fonts.font_data.insert("inter".into(), Arc::new(FontData::from_static(INTER_REGULAR))); @@ -425,7 +428,7 @@ mod japanese_font_tests { return; } let ctx = egui::Context::default(); - super::install(&ctx, &super::Tokens::for_kind(super::ThemeKind::Dark)); + super::install(&ctx, &super::Tokens::for_kind(super::ThemeKind::Dark), "ja"); let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); // No renderer here: drop the frame's texture uploads (egui asserts on unhandled ones in debug). out.textures_delta.clear(); diff --git a/docs/preferences.md b/docs/preferences.md index a51ac97d..86cc7799 100644 --- a/docs/preferences.md +++ b/docs/preferences.md @@ -29,7 +29,7 @@ Commands (CLI, MCP, control channel): ### Settings that change behaviour -- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant`), 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`; the browser build stays in English with it. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. +- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant`), 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`; the browser build stays in English with it. 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. - `general.undoLevels`: Levels of Undo - `general.pathPointSize`: Path Point and Handle Size - `general.recentItems`: Recent Projects Shown