From 1ab6eb963f9bde78942ea9ff016dc56ed98895fc Mon Sep 17 00:00:00 2001 From: WaWu Date: Sat, 10 Oct 2026 02:50:47 +0800 Subject: [PATCH 1/2] i18n: the viewer toolbar and the solid dialog ask the catalog for the text they draw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text a helper draws for a panel was handed to that helper as a bare literal, so the catalog lookup never happened and parts of the interface stayed English with a Chinese language chosen: - the Composition panel's toolbar tooltips - Choose grid and guide options, Toggle Transparency Grid, Toggle Mask and Shape Path Visibility, Region of Interest, Show Channel and Color Management Settings, Reset Exposure, Take Snapshot, Show Snapshot (hold); - the items of its four popups (grid and guides, resolution, magnification, channel) - Grid, Guides, Rulers, Proportional Grid, 3D Reference Axes, Full, Half, Third, Quarter, Custom..., Fit, Fit up to 100%, Colorize; - the solid dialog's "Affect all layers that use this solid" checkbox and its disabled hint "Only this layer uses this solid.". `tog` and `popup` now look the label up themselves, so every current and future call site is covered by one change each; the automation ids and labels stay English, as the rest of the panels keep them for scripts and tests. `resolution_label` composes its words from the catalog, so the closed resolution dropdown reads Chinese too, and the viewer's item now reuses the panel's existing "Title/action safe" row instead of a second spelling of the same concept. 21 rows join the Simplified catalog. The same keys join the Traditional one with the English source as their value: the tests require the two lists to match, and the Traditional wording is the translator's call rather than this change's. Tests: the row-completeness check now also accepts a row that a helper draws (its English source appears as a literal there), and a new check refuses a bare `Checkbox::new` / `RadioButton::new` label - the shape that hid the solid dialog, and the one the literal scan cannot see because the label is the call's second argument. This also corrects two Simplified rows #398 left wrong (`Channel` read 色版 in a Simplified catalog, `Motion Blur: {}` read 动态模糊 while the menu catalog's own Motion Blur is 运动模糊). --- crates/ui-egui/src/i18n/ui.rs | 81 +++++++++++++++++++++-- crates/ui-egui/src/panels/dialogs.rs | 4 +- crates/ui-egui/src/panels/viewer_tools.rs | 16 +++-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 4afb43b17..84d4d3f99 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -129,7 +129,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Casts Shadows", "投射阴影"), ("Change Renderer…", "更改渲染器…"), ("Changing a shortcut creates a custom preset; the default stays as it is.", "修改快捷键会创建自定义预设;默认预设保持不变。"), - ("Channel", "色版"), + ("Channel", "通道"), ("Channel:", "通道:"), ("Chapter:", "章节:"), ("Choose...", "选择..."), @@ -285,7 +285,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("More ArtCraft apps", "更多 ArtCraft 应用"), ("More tutorials", "更多教程"), ("Motion Blur", "运动模糊"), - ("Motion Blur: {}", "动态模糊:{}"), + ("Motion Blur: {}", "运动模糊:{}"), ("Motion Sketch: drag to record", "动态草图:拖动以记录"), ("Move Down", "下移"), ("Move Up", "上移"), @@ -490,6 +490,30 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl 拖动定位点可记录其运动"), ("▣ {}", "▣ {}"), ("⚠ {} is used by:", "⚠ {} 已被以下项目使用:"), + // Strings a panel used to hand to a helper as a bare literal: the helper draws them (the + // viewer's toolbar tooltips and popup items, the solid dialog's checkbox), so the lookup has + // to reach the catalog from inside the helper. Kept at the end so the catalogs stay parallel. + ("3D Reference Axes", "3D 参考轴"), + ("Affect all layers that use this solid", "影响使用该纯色的所有图层"), + ("Choose grid and guide options", "选择网格和参考线选项"), + ("Colorize", "着色"), + ("Custom", "自定义"), + ("Custom...", "自定义…"), + ("Fit", "适合"), + ("Fit up to 100%", "适合(最大 100%)"), + ("Full", "完整"), + ("Half", "一半"), + ("Only this layer uses this solid.", "仅此图层使用该纯色。"), + ("Proportional Grid", "比例网格"), + ("Quarter", "四分之一"), + ("Region of Interest", "目标区域"), + ("Reset Exposure", "重置曝光"), + ("Show Channel and Color Management Settings", "显示通道和色彩管理设置"), + ("Show Snapshot (hold)", "显示快照(按住)"), + ("Take Snapshot", "拍摄快照"), + ("Third", "三分之一"), + ("Toggle Mask and Shape Path Visibility", "切换蒙版与形状路径可见性"), + ("Toggle Transparency Grid", "切换透明网格"), ]; const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ @@ -907,6 +931,29 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl 拖曳釘選點可記錄其運動"), ("▣ {}", "▣ {}"), ("⚠ {} is used by:", "⚠ {} 已被以下專案使用:"), + // The same rows as the Simplified catalog (the tests require both lists to match). The + // Traditional wording is the translator's call and is left as the English source for now. + ("3D Reference Axes", "3D Reference Axes"), + ("Affect all layers that use this solid", "Affect all layers that use this solid"), + ("Choose grid and guide options", "Choose grid and guide options"), + ("Colorize", "Colorize"), + ("Custom", "Custom"), + ("Custom...", "Custom..."), + ("Fit", "Fit"), + ("Fit up to 100%", "Fit up to 100%"), + ("Full", "Full"), + ("Half", "Half"), + ("Only this layer uses this solid.", "Only this layer uses this solid."), + ("Proportional Grid", "Proportional Grid"), + ("Quarter", "Quarter"), + ("Region of Interest", "Region of Interest"), + ("Reset Exposure", "Reset Exposure"), + ("Show Channel and Color Management Settings", "Show Channel and Color Management Settings"), + ("Show Snapshot (hold)", "Show Snapshot (hold)"), + ("Take Snapshot", "Take Snapshot"), + ("Third", "Third"), + ("Toggle Mask and Shape Path Visibility", "Toggle Mask and Shape Path Visibility"), + ("Toggle Transparency Grid", "Toggle Transparency Grid"), ]; #[cfg(test)] @@ -976,6 +1023,7 @@ mod tests { ".heading(\"", ".monospace(\"", ".on_hover_text(\"", + ".on_disabled_hover_text(\"", ".hint_text(\"", ".text(\"", ".prefix(\"", @@ -1039,19 +1087,44 @@ mod tests { } /// Every row is asked for by a converted panel: a leftover row means the panel went back to its - /// English literal. + /// English literal. A string a helper draws for the panel (a toolbar tooltip, a popup item, a + /// checkbox label) is looked up inside that helper, so its English source only appears as a + /// literal there - `helper_labels_are_translated` keeps those helpers honest. #[test] fn every_row_is_used_by_a_converted_panel() { for (en, _) in catalogs() { let plain = format!("tr({en:?})"); let formatted = format!("tr_args({en:?}"); + let bare = format!("{en:?}"); assert!( - CONVERTED.iter().any(|(_, src)| src.contains(&plain) || src.contains(&formatted)), + CONVERTED.iter().any(|(_, src)| src.contains(&plain) || src.contains(&formatted) || src.contains(&bare)), "{en:?} is in the catalog but no converted panel asks for it" ); } } + /// A label that a helper takes as an argument is drawn by the helper, so the helper has to ask + /// the catalog: `Checkbox::new` and `RadioButton::new` are not among the scanned call shapes + /// (the label is their second argument), which let the solid dialog's checkbox ship in English. + #[test] + fn helper_labels_are_translated() { + for (path, source) in CONVERTED { + for marker in ["Checkbox::new(", "RadioButton::new("] { + let mut from = 0; + while let Some(at) = source[from..].find(marker) { + let at = from + at; + let rest = &source[at + marker.len()..]; + let line = &rest[..rest.find('\n').unwrap_or(rest.len())]; + if let Some(q) = line.find('"') { + let before = line[..q].trim_end(); + assert!(before.ends_with("tr("), "{path}: the `{marker}` label `{}` is not in the catalog", &line[q..]); + } + from = at + marker.len(); + } + } + } + } + /// A newly hard-coded interface string fails here instead of shipping in English to every /// language. It is a heuristic - it looks at the call shapes the panels use - and it ignores /// one-character literals, which are punctuation or a key name rather than a sentence. diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index f123906bb..eea50f073 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -418,8 +418,8 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { if editing.is_some() { ui.add_space(8.0); let r = ui - .add_enabled(shared, egui::Checkbox::new(&mut affect_all, "Affect all layers that use this solid")) - .on_disabled_hover_text("Only this layer uses this solid."); + .add_enabled(shared, egui::Checkbox::new(&mut affect_all, tr("Affect all layers that use this solid"))) + .on_disabled_hover_text(tr("Only this layer uses this solid.")); auto.add("dialog.solid.affectAll", r.rect, "Affect all layers that use this solid"); if shared && !affect_all { ui.label(egui::RichText::new(tr("This change will create a new solid for this layer.")).color(t.text_dim).size(11.0)); diff --git a/crates/ui-egui/src/panels/viewer_tools.rs b/crates/ui-egui/src/panels/viewer_tools.rs index 8d41eb738..2161a0303 100644 --- a/crates/ui-egui/src/panels/viewer_tools.rs +++ b/crates/ui-egui/src/panels/viewer_tools.rs @@ -486,7 +486,9 @@ fn popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: &str, anchor: Rect, it ui.separator(); continue; } - let r = ui.selectable_label(*on, label.as_str()); + // The item is the English source: the drawn text is looked up in the catalog, while + // the automation label stays English for scripts and tests. + let r = ui.selectable_label(*on, crate::i18n::tr(label)); app.auto.add(&format!("viewer.{auto}.{i}"), r.rect, label); if r.clicked() { chosen = Some(i); @@ -520,10 +522,10 @@ pub(crate) fn resolution_label(res: Resolution, scale: f64) -> String { } else { "Quarter" }; - format!("({r})") + format!("({})", crate::i18n::tr(r)) } - Resolution::Custom(n) => format!("Custom ({n})"), - r => r.label().to_string(), + Resolution::Custom(n) => format!("{} ({n})", crate::i18n::tr("Custom")), + r => crate::i18n::tr(r.label()).to_string(), } } @@ -602,7 +604,9 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, let tog = |ui: &mut egui::Ui, auto: &mut crate::automation::Registry, x: &mut f32, icon: Icon, on: bool, id: &str, tip: &str| -> (bool, Rect) { let r = Rect::from_min_size(pos2(*x, cy - 11.0), vec2(22.0, 22.0)); - let resp = widgets::icon_button(ui, r, icon, on, &t, egui::Id::new(("vw-btn", id))).on_hover_text(tip); + // The tip is the English source: the tooltip is looked up in the catalog, while the + // automation label stays English for scripts and tests. + let resp = widgets::icon_button(ui, r, icon, on, &t, egui::Id::new(("vw-btn", id))).on_hover_text(crate::i18n::tr(tip)); auto.add(&format!("viewer.{id}"), r, tip); *x += 24.0; (resp.clicked(), r) @@ -632,7 +636,7 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, } let v = &app.ui.viewer; let items = vec![ - ("Title/Action Safe".to_string(), v.safe_margins), + ("Title/action safe".to_string(), v.safe_margins), ("Proportional Grid".to_string(), v.proportional_grid), ("Grid".to_string(), v.grid), ("Guides".to_string(), v.guides), From 9b7add404687b4916235703eb8a3c34abfb40fe2 Mon Sep 17 00:00:00 2001 From: WaWu Date: Sat, 10 Oct 2026 14:51:16 +0800 Subject: [PATCH 2/2] i18n: the forms, the settings pages and the viewer menus draw their technical terms in Chinese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second pass over the interface below the menu bar: the strings that are technical terms or proper nouns - `Mercury GPU Acceleration (…)`, `8/16/32 bits per channel`, `OCIO (built-in ACES config)`, `sRGB IEC61966-2.1 (default)`, `Import Photoshop File`, `Current Settings`, the viewer's channel and Fast Previews items. Each wording was checked against the installed After Effects 26.5.0 Simplified Chinese, the way the menu bar was in #353. Terms Adobe keeps in English (`Display P3`, `HDTV (Rec. 709)`, `RGB`, `Alpha`, `H.264`, `ProRes`) need no row: a missing row falls back to the English source, which is the decision. The text reaches the catalog through the funnels that draw it, so one change each covers every call site: - `forms.rs`: field labels, notes, choice items and info bodies, plus `dialogs::modal`'s title - the modal id stays keyed on the untranslated source, so changing the language cannot move it; - `settings.rs`: page titles, sections, notes, buttons, setting labels and choice items (the page table itself is engine data and cannot reach the catalog); - `comp_settings.rs` (3D renderer), `key_dialogs.rs::combo` (Current Settings and the roving items), `properties.rs` + `timeline.rs` (the text animator menu), `dialogs.rs` (About links); - `render_queue.rs`: the Opus bitrate goes through `tr_args`, so the value is substituted after the lookup - worth calling out, because building the string with `format!` and translating the result looks up the assembled text and silently stays English (the same trap as the project settings' GPU label, which this commit also fixes). 30 rows join the Simplified catalog. The same keys join the Traditional one with the English source as their value: the tests require the two lists to match, and the Traditional wording is the translator's call. Tests: `CONVERTED` gains the three engine files whose strings the interface draws (`prefs.rs`, `viewer.rs`, `commands/viewer_cmds.rs`); each holds no drawn literals, so the literal scan stays green and the new rows have a source to be "used" from. `cargo test -p effectcraft-ui-egui --lib i18n`: 16 passed. --- crates/ui-egui/src/i18n/ui.rs | 72 ++++++++++++++++++++++ crates/ui-egui/src/panels/comp_settings.rs | 8 ++- crates/ui-egui/src/panels/dialogs.rs | 5 +- crates/ui-egui/src/panels/forms.rs | 18 +++--- crates/ui-egui/src/panels/key_dialogs.rs | 5 +- crates/ui-egui/src/panels/properties.rs | 4 +- crates/ui-egui/src/panels/render_queue.rs | 2 +- crates/ui-egui/src/panels/settings.rs | 18 +++--- crates/ui-egui/src/panels/timeline.rs | 3 +- 9 files changed, 108 insertions(+), 27 deletions(-) diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 84d4d3f99..1a655bc42 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -514,6 +514,40 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Third", "三分之一"), ("Toggle Mask and Shape Path Visibility", "切换蒙版与形状路径可见性"), ("Toggle Transparency Grid", "切换透明网格"), + // Technical terms and proper nouns: the project settings, the import and export forms, the + // viewer's channel and Fast Previews popups, the keyframe dialogs and the About window. The + // terms that are the same in Chinese (Display P3, HDTV (Rec. 709), RGB, Alpha, H.264, ProRes…) + // need no row: a missing row falls back to the English source, which is the decision. + ("16 bits per channel", "每通道 16 位"), + ("32 bits per channel (float)", "每通道 32 位(浮点)"), + ("8 bits per channel", "每通道 8 位"), + ("Adaptive Resolution", "自适应分辨率"), + ("Adobe-style built-in", "Adobe 风格内置"), + ("Blue", "蓝色"), + ("Current Settings", "当前设置"), + ("Disable Per-character 3D", "停用逐字 3D 化"), + ("Draft", "草图"), + ("Enable Per-character 3D", "启用逐字 3D 化"), + ("Fast Draft", "快速草图"), + ("Fast Previews (Draft 3D, Faster Effects)", "快速预览(3D 草图、更快效果)"), + ("Green", "绿色"), + ("Import Image Sequence", "导入图像序列"), + ("Import PDF / Illustrator File", "导入 PDF / Illustrator 文件"), + ("Import Photoshop File", "导入 Photoshop 文件"), + ("Interpret Unlabeled Alpha As", "将未标记的 Alpha 解释为"), + ("Join the ArtCraft Discord", "加入 ArtCraft Discord"), + ("Mercury GPU Acceleration ({})", "Mercury GPU 加速({})"), + ("Mercury Software Only", "仅 Mercury 软件"), + ("OCIO (built-in ACES config)", "OCIO(内置 ACES 配置)"), + ("Off (Final Quality)", "关闭(最终质量)"), + ("OpenGL (compatibility)", "OpenGL(兼容)"), + ("Opus Bitrate: {} kbps", "Opus 比特率:{} kbps"), + ("RGB Straight", "RGB 直接"), + ("Red", "红色"), + ("Tone map", "色调映射"), + ("Use In-Window Menu Bar on macOS", "在 macOS 上使用窗口内菜单栏"), + ("Wireframe", "线框"), + ("sRGB IEC61966-2.1 (default)", "sRGB IEC61966-2.1(默认)"), ]; const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ @@ -954,6 +988,38 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Third", "Third"), ("Toggle Mask and Shape Path Visibility", "Toggle Mask and Shape Path Visibility"), ("Toggle Transparency Grid", "Toggle Transparency Grid"), + // The same rows as the Simplified catalog (the tests require both lists to match). The + // Traditional wording is the translator's call and stays the English source until then. + ("16 bits per channel", "16 bits per channel"), + ("32 bits per channel (float)", "32 bits per channel (float)"), + ("8 bits per channel", "8 bits per channel"), + ("Adaptive Resolution", "Adaptive Resolution"), + ("Adobe-style built-in", "Adobe-style built-in"), + ("Blue", "Blue"), + ("Current Settings", "Current Settings"), + ("Disable Per-character 3D", "Disable Per-character 3D"), + ("Draft", "Draft"), + ("Enable Per-character 3D", "Enable Per-character 3D"), + ("Fast Draft", "Fast Draft"), + ("Fast Previews (Draft 3D, Faster Effects)", "Fast Previews (Draft 3D, Faster Effects)"), + ("Green", "Green"), + ("Import Image Sequence", "Import Image Sequence"), + ("Import PDF / Illustrator File", "Import PDF / Illustrator File"), + ("Import Photoshop File", "Import Photoshop File"), + ("Interpret Unlabeled Alpha As", "Interpret Unlabeled Alpha As"), + ("Join the ArtCraft Discord", "Join the ArtCraft Discord"), + ("Mercury GPU Acceleration ({})", "Mercury GPU Acceleration ({})"), + ("Mercury Software Only", "Mercury Software Only"), + ("OCIO (built-in ACES config)", "OCIO (built-in ACES config)"), + ("Off (Final Quality)", "Off (Final Quality)"), + ("OpenGL (compatibility)", "OpenGL (compatibility)"), + ("Opus Bitrate: {} kbps", "Opus Bitrate: {} kbps"), + ("RGB Straight", "RGB Straight"), + ("Red", "Red"), + ("Tone map", "Tone map"), + ("Use In-Window Menu Bar on macOS", "Use In-Window Menu Bar on macOS"), + ("Wireframe", "Wireframe"), + ("sRGB IEC61966-2.1 (default)", "sRGB IEC61966-2.1 (default)"), ]; #[cfg(test)] @@ -1012,6 +1078,12 @@ mod tests { ("panels/viewer.rs", include_str!("../panels/viewer.rs")), ("panels/viewer_tools.rs", include_str!("../panels/viewer_tools.rs")), ("panels/viewers.rs", include_str!("../panels/viewers.rs")), + // Text the interface draws but does not own: the settings pages and the viewer's channel + // and Fast Previews lists come from the engine as data, so their source strings live here. + // Both files pass the literal scan below unchanged (they hold no drawn literals). + ("engine settings pages", include_str!("../../../engine/src/prefs.rs")), + ("engine viewer channels", include_str!("../../../engine/src/viewer.rs")), + ("engine viewer commands", include_str!("../../../engine/src/commands/viewer_cmds.rs")), ]; /// Call shapes that hand a string literal straight to the user. diff --git a/crates/ui-egui/src/panels/comp_settings.rs b/crates/ui-egui/src/panels/comp_settings.rs index 39b7580ea..b18e4c8df 100644 --- a/crates/ui-egui/src/panels/comp_settings.rs +++ b/crates/ui-egui/src/panels/comp_settings.rs @@ -340,11 +340,13 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr Tab::Renderer => { ui.horizontal(|ui| { ui.label(tr("Renderer:")); - let r = - egui::ComboBox::from_id_salt("comp-renderer").selected_text(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" }).show_ui(ui, |ui| { + let r = egui::ComboBox::from_id_salt("comp-renderer").selected_text(tr(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" })).show_ui( + ui, + |ui| { ui.selectable_value(&mut d.advanced_3d, false, tr("Classic 3D")); ui.selectable_value(&mut d.advanced_3d, true, tr("Advanced 3D")); - }); + }, + ); auto.add("dialog.comp.renderer", r.response.rect, "Renderer"); }); } diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index eea50f073..08e7c302e 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -158,7 +158,8 @@ pub(crate) fn modal(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Toke .show(ctx, |ui| { ui.set_width(size.x - 36.0); ui.set_min_height(size.y - 36.0); - ui.label(egui::RichText::new(title).font(Tokens::semibold(15.0)).color(t.tab_text_active)); + // The id stays keyed on the untranslated source, so switching language cannot move it. + ui.label(egui::RichText::new(tr(title)).font(Tokens::semibold(15.0)).color(t.tab_text_active)); ui.add_space(10.0); egui::ScrollArea::both().max_height((available.y - 84.0).max(40.0)).auto_shrink([false, true]).show(ui, body); }); @@ -291,7 +292,7 @@ fn about_main(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, cmd: &mut ); let foreground = if discord { Color32::WHITE } else { t.text }; icons::paint(p, Rect::from_center_size(pos2(r.min.x + 18.0, r.center().y), vec2(15.0, 15.0)), icon, foreground); - p.text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, label, Tokens::medium(12.5), foreground); + p.text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, tr(label), Tokens::medium(12.5), foreground); p.text( pos2(r.max.x - 12.0, r.center().y), Align2::RIGHT_CENTER, diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index a9468451d..a2905659e 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -4,7 +4,7 @@ //! Keyboard Shortcuts: `shortcut_editor`). Every form runs an engine command with the collected parameters, so whatever a //! dialog does an agent can do with one `engine.execute`. -use crate::i18n::tr; +use crate::i18n::{tr, tr_args}; use egui::{Color32, vec2}; use serde_json::{Map, Value, json}; @@ -487,9 +487,11 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool { } "file.projectSettings" if p.as_object().is_none_or(|m| m.is_empty()) => { let st = &s.project.settings; + // The catalogue row is `Mercury GPU Acceleration ({})`, so the value goes through + // tr_args: wrapping the assembled string in tr() would look up the backend's name. let gpu_label = match &s.accel { - Some(a) => format!("Mercury GPU Acceleration ({})", a.name()), - None => "Mercury GPU Acceleration (no GPU: software)".to_string(), + Some(a) => tr_args("Mercury GPU Acceleration ({})", &[&a.name()]), + None => tr("Mercury GPU Acceleration (no GPU: software)").to_string(), }; let depth = match st.bit_depth.label() { l if l.starts_with("16") => 1, @@ -765,7 +767,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { super::dialogs::modal(ctx, &f.title.clone(), vec2(480.0, h), t, |ui| { egui::Grid::new("form-grid").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| { for (i, fl) in f.fields.iter_mut().enumerate() { - ui.label(&fl.label); + ui.label(tr(&fl.label)); let r = match &mut fl.kind { FieldKind::Number { value, speed } => { let dec = if *speed < 0.01 { 5 } else { 3 }; @@ -801,7 +803,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { FieldKind::Note(text) => { ui.scope(|ui| { ui.set_width(220.0); - ui.add(egui::Label::new(egui::RichText::new(text.as_str()).small()).wrap()) + ui.add(egui::Label::new(egui::RichText::new(tr(text.as_str())).small()).wrap()) }) .inner .rect @@ -810,11 +812,11 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { FieldKind::Choice { options, sel } => { let cur = options.get(*sel).map(|o| o.0.clone()).unwrap_or_default(); egui::ComboBox::from_id_salt(("form", fl.key.as_str())) - .selected_text(cur) + .selected_text(tr(&cur)) .width(240.0) .show_ui(ui, |ui| { for (i, (l, _)) in options.iter().enumerate() { - ui.selectable_value(sel, i, l); + ui.selectable_value(sel, i, tr(l)); } }) .response @@ -883,7 +885,7 @@ pub fn show_info(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let (title, body) = app.dialog_state.info.clone(); let mut close = false; super::dialogs::modal(ctx, &title, vec2(440.0, 160.0), t, |ui| { - ui.label(body); + ui.label(tr(&body)); ui.add_space(16.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() { diff --git a/crates/ui-egui/src/panels/key_dialogs.rs b/crates/ui-egui/src/panels/key_dialogs.rs index 3e170c12c..6e1c5d4ce 100644 --- a/crates/ui-egui/src/panels/key_dialogs.rs +++ b/crates/ui-egui/src/panels/key_dialogs.rs @@ -190,9 +190,10 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { fn combo(ui: &mut egui::Ui, app: &mut EffectcraftApp, id: &str, sel: &mut usize, opts: &[&str], enabled: bool) { ui.add_enabled_ui(enabled, |ui| { - let r = egui::ComboBox::from_id_salt(id).width(200.0).selected_text(opts[*sel]).show_ui(ui, |ui| { + // The options are the English source strings; the automation id and label stay English. + let r = egui::ComboBox::from_id_salt(id).width(200.0).selected_text(tr(opts[*sel])).show_ui(ui, |ui| { for (i, o) in opts.iter().enumerate() { - ui.selectable_value(sel, i, *o); + ui.selectable_value(sel, i, tr(o)); } }); app.auto.add(id, r.response.rect, id); diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 2dcecc1d4..9e78ffbc0 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -15,6 +15,7 @@ use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2}; use serde_json::json; use crate::dock::PanelKind; +use crate::i18n::tr; use crate::icons::{self, Icon}; use crate::theme::Tokens; use crate::{EffectcraftApp, widgets}; @@ -120,7 +121,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } let per_char = layer.props.prop("text/perChar3d").is_some_and(|q| q.value.as_bool()); let entries = animator_entries(per_char); - let labels: Vec = entries.iter().map(|(l, _)| l.clone()).collect(); + // The entries are the English source strings; the menu draws them translated. + let labels: Vec = entries.iter().map(|(l, _)| tr(l).to_string()).collect(); if let Some(i) = widgets::popup_menu(ui, pop, br.left_bottom(), &labels, None) { match entries[i].1 { "" => {} diff --git a/crates/ui-egui/src/panels/render_queue.rs b/crates/ui-egui/src/panels/render_queue.rs index 583c800bd..502b84102 100644 --- a/crates/ui-egui/src/panels/render_queue.rs +++ b/crates/ui-egui/src/panels/render_queue.rs @@ -183,7 +183,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren } v.push(("-".into(), Value::Null)); for kbps in [24u32, 32, 64, 96, 128, 192, 256] { - v.push((mark(o.opus_bitrate_kbps == kbps, &format!("Opus Bitrate: {kbps} kbps")), json!({"audioBitrate": kbps}))); + v.push((mark(o.opus_bitrate_kbps == kbps, &tr_args("Opus Bitrate: {} kbps", &[&kbps])), json!({"audioBitrate": kbps}))); } for (a, k) in [(OpusApplication::Audio, "audio"), (OpusApplication::Voip, "voice")] { v.push((mark(o.opus_application == a, &tr_args("Opus Tuning: {}", &[&(a.label())])), json!({"opusApplication": k}))); diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index ac79d8f08..6e3335b53 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -50,14 +50,14 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value match *item { Item::Section(title) => { ui.add_space(6.0); - ui.label(RichText::new(title).font(Tokens::semibold(12.5)).color(t.tab_text_active)); + ui.label(RichText::new(tr(title)).font(Tokens::semibold(12.5)).color(t.tab_text_active)); ui.separator(); } Item::Note(text) => { - ui.label(RichText::new(text).color(t.text_dim)); + ui.label(RichText::new(tr(text)).color(t.text_dim)); } Item::Button { label, command, params } => { - let r = ui.button(label); + let r = ui.button(tr(label)); reg(app, &format!("settings.button.{}", label.trim_end_matches('.')), &r, label); if r.clicked() { acts.push(Act::Run(command.into(), serde_json::from_str(params).unwrap_or(json!({})))); @@ -96,7 +96,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value match kind { Kind::Bool => { let mut b = v.as_bool().unwrap_or(false); - let r = ui.checkbox(&mut b, label); + let r = ui.checkbox(&mut b, tr(label)); if r.changed() { acts.push(Act::Set(key.into(), json!(b))); } @@ -104,7 +104,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value } _ => { ui.horizontal(|ui| { - ui.add_sized(vec2(label_w, 18.0), egui::Label::new(format!("{label}:")).truncate()); + ui.add_sized(vec2(label_w, 18.0), egui::Label::new(format!("{}:", tr(label))).truncate()); let r = match kind { Kind::Int(lo, hi, unit) => { let mut x = v.as_i64().unwrap_or(0); @@ -146,11 +146,11 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value let curv = v.as_str().map_or_else(|| v.to_string(), str::to_string); let shown = opts.iter().find(|o| o.1 == curv).map(|o| o.0).unwrap_or(curv.as_str()).to_string(); egui::ComboBox::from_id_salt(("settings", key)) - .selected_text(shown) + .selected_text(tr(&shown)) .width(220.0) .show_ui(ui, |ui| { for (l, val) in opts { - if ui.selectable_label(*val == curv, *l).clicked() { + if ui.selectable_label(*val == curv, tr(l)).clicked() { acts.push(Act::Set(key.into(), json!(val))); } } @@ -449,7 +449,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.painter().text( r.left_center() + vec2(10.0, 0.0), egui::Align2::LEFT_CENTER, - p.title, + tr(p.title), Tokens::ui(12.5), if sel { Color32::WHITE } else { t.text }, ); @@ -466,7 +466,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { // dialog scrolled sideways a little (#284). ui.set_width(ui.available_width()); let p = &all[idx]; - ui.label(RichText::new(p.title).font(Tokens::semibold(14.0)).color(t.tab_text_active)); + ui.label(RichText::new(tr(p.title)).font(Tokens::semibold(14.0)).color(t.tab_text_active)); ui.add_space(6.0); egui::ScrollArea::vertical().max_height(body_h - 30.0).auto_shrink([false, false]).show(ui, |ui| { page_ui(app, ui, p, &cur, t, &mut acts); diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 1d61ae4c3..7df3439be 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -14,6 +14,7 @@ use effectcraft_engine::time::Tick; use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; use serde_json::json; +use crate::i18n::tr; use crate::icons::{self, Icon}; use crate::theme::Tokens; use crate::{EffectcraftApp, widgets}; @@ -3280,7 +3281,7 @@ fn text_anim_popups( let mut opts: Vec = Vec::new(); let mut acts: Vec<(String, serde_json::Value)> = Vec::new(); if key == "animate" { - opts.push(if per_char { "Disable Per-character 3D" } else { "Enable Per-character 3D" }.into()); + opts.push(tr(if per_char { "Disable Per-character 3D" } else { "Enable Per-character 3D" }).to_string()); acts.push(("layer.enablePerChar3D".into(), json!({"layer": layer.id.0, "enabled": !per_char}))); opts.push("-".into()); acts.push((String::new(), json!(null)));