diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 4afb43b1..1a655bc4 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,64 @@ 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", "切换透明网格"), + // 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)] = &[ @@ -907,6 +965,61 @@ 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"), + // 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)] @@ -965,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. @@ -976,6 +1095,7 @@ mod tests { ".heading(\"", ".monospace(\"", ".on_hover_text(\"", + ".on_disabled_hover_text(\"", ".hint_text(\"", ".text(\"", ".prefix(\"", @@ -1039,19 +1159,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/comp_settings.rs b/crates/ui-egui/src/panels/comp_settings.rs index 39b7580e..b18e4c8d 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 f123906b..08e7c302 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, @@ -418,8 +419,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/forms.rs b/crates/ui-egui/src/panels/forms.rs index 117c95fb..c7f330b0 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 @@ -888,7 +890,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 3e170c12..6e1c5d4c 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 2dcecc1d..9e78ffbc 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 583c800b..502b8410 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 ac79d8f0..6e3335b5 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 1d61ae4c..7df3439b 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))); diff --git a/crates/ui-egui/src/panels/viewer_tools.rs b/crates/ui-egui/src/panels/viewer_tools.rs index 8d41eb73..2161a030 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),