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)));