diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index a92d376e9..7e7a3f787 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -39,11 +39,24 @@ impl EffectcraftApp { out.push((panels::viewers::panel(id), t)); } } - let Some(comp) = self.session.active_comp() else { return out }; - if let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) { + if let Some(comp) = self.session.active_comp() + && let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) + { out.push((PanelKind::EffectControls, format!("Effect Controls {}", l.name))); out.push((PanelKind::Properties, format!("Properties: {}", l.name))); } + // The panels named for themselves, in the interface language. The Window menu has carried a + // (`window.panel`, title) row for these in every catalog that exists, so the dock reads the + // same pair instead of drawing `title()`'s English; a panel the menu does not list (`History`, + // `ScriptUI Panel`) keeps its English name, and a tab that names a comp or a layer is listed + // above and wins, being found first. + for p in PanelKind::ALL { + // The Window menu lists most of these; `Markers` is only in the panel catalog, and the + // two panels in neither (`History`, `ScriptUI Panel`) keep their English names. + let listed = crate::i18n::label(self, "window.panel", p.title()); + let label = if listed == p.title() { crate::i18n::tr(p.title()) } else { listed }; + out.push((p, label.to_string())); + } out } @@ -570,7 +583,7 @@ impl EffectcraftApp { } } // Ghost of the dragged tab. - let g = painter.layout_no_wrap(p.title().to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE); + let g = painter.layout_no_wrap(crate::i18n::label(self, "window.panel", p.title()).to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE); let gr = Rect::from_min_size(pos + vec2(12.0, 10.0), g.size() + vec2(16.0, 8.0)); painter.rect_filled(gr, 4.0, t.focus.gamma_multiply(0.85)); painter.galley(gr.min + vec2(8.0, 4.0), g, Color32::WHITE); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 81343f9f8..a8aa1bb47 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -6961,6 +6961,35 @@ mod tests { } } + /// The dock's tab titles are `PanelKind::title()`; the panels the Window menu lists are drawn in + /// the interface language through their `(window.panel, title)` rows. `History` and `ScriptUI + /// Panel` are in no menu and keep their English names, which is what this pins. + #[test] + fn the_dock_draws_every_panel_it_can_in_the_interface_language() { + use crate::dock::PanelKind; + const WITHOUT_A_ROW: &[&str] = &["History", "ScriptUI Panel"]; + for (name, catalog, code) in [("ja", JAPANESE, "ja"), ("zh-hans", SIMPLIFIED_CHINESE, "zh-hans"), ("zh-hant", TRADITIONAL_CHINESE, "zh-hant")] { + // The dock asks both catalogs, so a title may be named by either. + ui::set_current(code); + for p in PanelKind::ALL { + let title = p.title(); + let in_menu = catalog.iter().any(|(c, en, _)| *c == "window.panel" && *en == title); + let in_panel = ui::tr(title) != title; + assert_eq!( + in_menu || in_panel, + !WITHOUT_A_ROW.contains(&title), + "{name}: {title} is {}named by a catalog and {}in the exception list", + if in_menu || in_panel { "" } else { "not " }, + if WITHOUT_A_ROW.contains(&title) { "" } else { "not " } + ); + } + } + ui::set_current("en"); + // The dock asks the catalog rather than drawing the English name. + let dock_ui = include_str!("dock_ui.rs"); + assert!(dock_ui.contains(r#"crate::i18n::label(self, "window.panel", p.title())"#), "dock_ui.rs no longer asks the catalog for the panel titles"); + } + #[test] fn zh_hans_translates_labels_computed_titles_and_keeps_names_verbatim() { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 9831092da..206111892 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -1370,6 +1370,21 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Total Offset", "总偏移"), ("Per Layer", "每个图层"), ("Track", "跟踪"), + ("Apply", "应用"), + ("Analyze", "分析"), + ("Create Camera", "创建摄像机"), + ("Add Point", "添加点"), + ("Remove Point", "移除点"), + ("Render", "渲染"), + ("Save Current as Template…", "将当前项存为模板…"), + ("New Composition From Footage", "从素材新建合成"), + ("Search effects and presets", "搜索效果和预设"), + ("Search project", "搜索项目"), + ("Search", "搜索"), + ("(no composition)", "没有合成"), + ("per second", "每秒"), + ("Waveform", "波形"), + ("steps", "步"), ]; const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ @@ -2099,6 +2114,21 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Total Offset", "總偏移"), ("Per Layer", "每個圖層"), ("Track", "追蹤"), + ("Apply", "套用"), + ("Analyze", "分析"), + ("Create Camera", "建立攝影機"), + ("Add Point", "添加點"), + ("Remove Point", "移除點"), + ("Render", "算圖"), + ("Save Current as Template…", "將目前項目存為範本…"), + ("New Composition From Footage", "從素材新增合成"), + ("Search effects and presets", "搜尋效果與預設"), + ("Search project", "搜尋專案"), + ("Search", "搜尋"), + ("(no composition)", "沒有合成"), + ("per second", "每秒"), + ("Waveform", "波形"), + ("steps", "步驟"), ]; /// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs. @@ -4478,6 +4508,89 @@ mod tests { } } + /// Labels drawn through a helper's *argument* or through `Painter::text` are invisible to the + /// scan above, which only looks at a literal right after the call token: a `widgets::` helper + /// takes its label as its second or third argument, and `Painter::text` takes the string third. + /// Both shapes are checked here, so an English literal there is caught like any other. + #[test] + fn helper_and_painter_labels_are_translated() { + /// The `widgets.rs` helpers that draw a label, and which argument holds it. + const HELPER_LABELS: &[(&str, usize)] = + &[("text_button", 2), ("search_field", 3), ("text_field", 3), ("dropdown", 2), ("dropdown_tip", 2), ("text_fit", 3), ("check_label", 1)]; + /// Literals that draw as they are: a brand, a value, a separator, a unit, and the kerning + /// sample pair the Character panel shows. + const VERBATIM: &[&str] = &["EffectCraft", "32", "dB", "···", "×", "V/A"]; + + /// The top-level arguments of a call, given the text after its `(`. + fn args_of(s: &str) -> Vec<&str> { + let (mut out, mut depth, mut start, mut instr) = (Vec::new(), 0i32, None, false); + for (i, c) in s.char_indices() { + match c { + '"' if !instr => { + instr = true; + if start.is_none() { + start = Some(i); + } + } + '"' => instr = false, + '(' | '[' | '{' if !instr => depth += 1, + ')' | ']' | '}' if !instr => { + if depth == 0 { + break; + } + depth -= 1; + } + ',' if !instr && depth == 0 => { + if let Some(st) = start { + out.push(s[st..i].trim()); + } + start = None; + } + c if !instr && !c.is_whitespace() && start.is_none() => start = Some(i), + _ => {} + } + } + if let Some(st) = start { + out.push(s[st..].trim()); + } + out + } + + for (path, source) in CONVERTED { + // The test modules of these files draw nothing; they hold sample strings. + let source = source.split("#[cfg(test)]").next().unwrap_or(source); + for (n, line) in source.lines().enumerate() { + let code = line.split("//").next().unwrap_or(line); + let check = |args: Vec<&str>, index: usize, what: &str| { + let Some(arg) = args.get(index) else { return }; + // A single character is a symbol or a glyph, not interface text - the same + // exemption the call-shape scan above makes. + let inner = arg.strip_prefix('"').and_then(|a| a.strip_suffix('"')).filter(|i| !i.contains('"')); + match inner { + Some(text) if text.chars().count() >= 2 && !VERBATIM.contains(&text) => { + panic!("{path}:{}: the interface text {arg} that {what} draws is not in the catalog", n + 1); + } + _ => {} + } + }; + for (call, index) in HELPER_LABELS { + let marker = format!("widgets::{call}("); + let mut from = 0; + while let Some(at) = code[from..].find(&marker) { + let at = from + at + marker.len(); + check(args_of(&code[at..]), *index, &format!("widgets::{call}")); + from = at; + } + } + let mut from = 0; + while let Some(at) = code[from..].find(".text(") { + from = at + 6; + check(args_of(&code[from..]), 2, "Painter::text"); + } + } + } + } + /// The Settings pages come from the engine as data, so the literal scan above cannot see them: /// every title, section, label, choice, button, note and unit they draw needs a Chinese row /// (#1178). Language names are written in their own language, and a few labels read the same diff --git a/crates/ui-egui/src/panels/anim_tools.rs b/crates/ui-egui/src/panels/anim_tools.rs index 15a4f4f89..3f7c0ca46 100644 --- a/crates/ui-egui/src/panels/anim_tools.rs +++ b/crates/ui-egui/src/panels/anim_tools.rs @@ -109,7 +109,7 @@ pub fn wiggler(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { app.ui.anim_tools = st.clone(); let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0)); let enabled = app.session.state.selected_keys.len() >= 2; - if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::new("wg-apply")).clicked() { + if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::new("wg-apply")).clicked() { let params = json!({ "apply": if st.wiggle_spatial { "spatial" } else { "temporal" }, "noise": if st.wiggle_smooth { "smooth" } else { "jagged" }, @@ -150,7 +150,7 @@ pub fn smoother(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { y += ROW + 8.0; let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0)); let enabled = app.session.state.selected_keys.len() >= 3; - if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::new("sm-apply")).clicked() { + if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::new("sm-apply")).clicked() { apply(app, "keys.smooth", json!({"tolerance": tol})); } app.auto.add("smoother.apply", b, "Apply"); @@ -321,7 +321,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec changes.insert("keyframeRate".into(), json!(nr)); } } else { - p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, "per second", Tokens::ui(12.0), t.text_dim); + p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, crate::i18n::tr("per second"), Tokens::ui(12.0), t.text_dim); } y += ROW; let mut check = |app: &mut EffectcraftApp, y: f32, key: &str, text: &str, on: bool| { @@ -392,7 +392,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec } let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0)); let enabled = app.session.state.selected_keys.len() >= 2; - if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::new("mi-apply")).clicked() { + if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::new("mi-apply")).clicked() { match app.session.execute("mask.interpolate", json!({})) { Ok(r) => app.ui.status = format!("Mask Interpolation: {} keyframes added ({} vertices)", r["keys"], r["vertices"]), Err(e) => app.ui.status = e.to_string(), diff --git a/crates/ui-egui/src/panels/camera_tracker_ui.rs b/crates/ui-egui/src/panels/camera_tracker_ui.rs index 2bccfae07..1fe187672 100644 --- a/crates/ui-egui/src/panels/camera_tracker_ui.rs +++ b/crates/ui-egui/src/panels/camera_tracker_ui.rs @@ -413,12 +413,12 @@ pub fn editor( let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0)); let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0)); let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(110.0, 22.0)); - if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::new(("ct-analyze", g.uid))).clicked() && !busy { + if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::new(("ct-analyze", g.uid))).clicked() && !busy { actions.push(("camera.analyze".into(), me.clone())); } app.auto.add(&format!("effectControls.cameraTracker.{}.analyze", g.uid), b1, "Analyze"); if running { - if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::new(("ct-cancel", g.uid))).clicked() { + if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::new(("ct-cancel", g.uid))).clicked() { actions.push(("camera.cancel".into(), json!({}))); } app.auto.add(&format!("effectControls.cameraTracker.{}.cancel", g.uid), b2, "Cancel"); @@ -426,7 +426,7 @@ pub fn editor( let params = effectcraft_engine::camera_track::static_params(g); let solve = ct::solve(¶ms); if solve.is_some() { - if widgets::text_button(ui, b3, "Create Camera", false, &t, egui::Id::new(("ct-camera", g.uid))).clicked() { + if widgets::text_button(ui, b3, crate::i18n::tr("Create Camera"), false, &t, egui::Id::new(("ct-camera", g.uid))).clicked() { actions.push(("camera.create".into(), me.clone())); } app.auto.add(&format!("effectControls.cameraTracker.{}.createCamera", g.uid), b3, "Create Camera"); diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index bad1db39f..24c096977 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -756,12 +756,12 @@ fn warp_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, l let busy = app.session.is_warp_analyzing(); let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0)); let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0)); - if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::new(("warp-analyze", g.uid))).clicked() && !busy { + if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::new(("warp-analyze", g.uid))).clicked() && !busy { actions.push(("warp.analyze".into(), json!({"layer": layer.id.0, "effect": g.uid}))); } app.auto.add(&format!("effectControls.warp.{}.analyze", g.uid), b1, "Analyze"); if running { - if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::new(("warp-cancel", g.uid))).clicked() { + if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::new(("warp-cancel", g.uid))).clicked() { actions.push(("warp.cancel".into(), json!({}))); } app.auto.add(&format!("effectControls.warp.{}.cancel", g.uid), b2, "Cancel"); diff --git a/crates/ui-egui/src/panels/effects_presets.rs b/crates/ui-egui/src/panels/effects_presets.rs index 8eecfd091..a1612a0a6 100644 --- a/crates/ui-egui/src/panels/effects_presets.rs +++ b/crates/ui-egui/src/panels/effects_presets.rs @@ -277,7 +277,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } let sr = Rect::from_min_size(rect.min + vec2(8.0, 8.0), vec2(rect.width() - 44.0, 24.0)); let mut q = app.ui.effects_search.clone(); - widgets::search_field(ui, sr, &mut q, "Search effects and presets", &t); + widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search effects and presets"), &t); app.ui.effects_search = q.clone(); app.auto.add("effects.search", sr, "Search"); // Contents menu. diff --git a/crates/ui-egui/src/panels/flowchart.rs b/crates/ui-egui/src/panels/flowchart.rs index f59d35982..f36fa86f5 100644 --- a/crates/ui-egui/src/panels/flowchart.rs +++ b/crates/ui-egui/src/panels/flowchart.rs @@ -250,7 +250,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let p = ui.painter().with_clip_rect(rect); p.rect_filled(rect, 0.0, t.tl_bg); let Some(rid) = root(app) else { - p.text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint); + p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint); return; }; // ---- toolbar: switches and direction. diff --git a/crates/ui-egui/src/panels/fx_editors.rs b/crates/ui-egui/src/panels/fx_editors.rs index 1ae74caa2..21d56deba 100644 --- a/crates/ui-egui/src/panels/fx_editors.rs +++ b/crates/ui-egui/src/panels/fx_editors.rs @@ -786,19 +786,19 @@ fn reshape_buttons( let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0)); let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(96.0, 22.0)); let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(56.0, 22.0)); - if widgets::text_button(ui, b1, "Add Point", false, &t, egui::Id::new(("reshape-add", euid))).clicked() { + if widgets::text_button(ui, b1, crate::i18n::tr("Add Point"), false, &t, egui::Id::new(("reshape-add", euid))).clicked() { let mut v = pairs.clone(); v.push(add_pair(&pairs)); set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None); } app.auto.add(&format!("effectControls.effect.{euid}.reshape.add"), b1, "Add Point"); - if widgets::text_button(ui, b2, "Remove Point", false, &t, egui::Id::new(("reshape-remove", euid))).clicked() && !pairs.is_empty() { + if widgets::text_button(ui, b2, crate::i18n::tr("Remove Point"), false, &t, egui::Id::new(("reshape-remove", euid))).clicked() && !pairs.is_empty() { let mut v = pairs.clone(); v.pop(); set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None); } app.auto.add(&format!("effectControls.effect.{euid}.reshape.remove"), b2, "Remove Point"); - if widgets::text_button(ui, b3, "Clear", false, &t, egui::Id::new(("reshape-clear", euid))).clicked() && !pairs.is_empty() { + if widgets::text_button(ui, b3, crate::i18n::tr("Clear"), false, &t, egui::Id::new(("reshape-clear", euid))).clicked() && !pairs.is_empty() { set_str(actions, layer, g, "correspondencePoints", String::new(), None); } app.auto.add(&format!("effectControls.effect.{euid}.reshape.clear"), b3, "Clear"); diff --git a/crates/ui-egui/src/panels/learn.rs b/crates/ui-egui/src/panels/learn.rs index b613791fd..1e52b5c25 100644 --- a/crates/ui-egui/src/panels/learn.rs +++ b/crates/ui-egui/src/panels/learn.rs @@ -38,7 +38,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) { p.circle_filled(badge.center(), 22.0, t.accent.gamma_multiply(0.25)); p.circle_stroke(badge.center(), 22.0, Stroke::new(1.5, t.accent)); p.text(badge.center() - vec2(0.0, 5.0), Align2::CENTER_CENTER, tut.steps.len().to_string(), Tokens::semibold(15.0), Color32::WHITE); - p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, "steps", Tokens::ui(9.0), t.text_dim); + p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, crate::i18n::tr("steps"), Tokens::ui(9.0), t.text_dim); let tx = badge.max.x + 16.0; p.text(pos2(tx, r.min.y + 22.0), Align2::LEFT_CENTER, &tut.title, Tokens::semibold(14.0), Color32::WHITE); p.text(pos2(tx, r.min.y + 40.0), Align2::LEFT_CENTER, format!("{} min", tut.minutes), Tokens::ui(11.0), t.text_faint); diff --git a/crates/ui-egui/src/panels/misc.rs b/crates/ui-egui/src/panels/misc.rs index cd0ab6897..c1b7d9ee5 100644 --- a/crates/ui-egui/src/panels/misc.rs +++ b/crates/ui-egui/src/panels/misc.rs @@ -11,7 +11,8 @@ use crate::{EffectcraftApp, widgets}; pub fn placeholder(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, p: PanelKind) { let t = app.tokens; - ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&(p.title())]), Tokens::ui(12.0), t.text_faint); + let name = crate::i18n::label(app, "window.panel", p.title()); + ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&name]), Tokens::ui(12.0), t.text_faint); } /// Preview panel: transport controls, the shortcut whose options are shown, and that diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 1b6ef8c33..21f009912 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -240,7 +240,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) { } } _ => { - ui.label(egui::RichText::new(panel.title()).weak()); + ui.label(egui::RichText::new(crate::i18n::label(app, "window.panel", panel.title())).weak()); } } }); diff --git a/crates/ui-egui/src/panels/project.rs b/crates/ui-egui/src/panels/project.rs index c4e9894c4..69c403a41 100644 --- a/crates/ui-egui/src/panels/project.rs +++ b/crates/ui-egui/src/panels/project.rs @@ -443,7 +443,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Search. let sr = Rect::from_min_size(pos2(rect.min.x + 8.0, head.max.y + 2.0), vec2(rect.width() - 16.0, 22.0)); let mut q = app.ui.project_search.clone(); - widgets::search_field(ui, sr, &mut q, "Search project", &t); + widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search project"), &t); app.ui.project_search = q; app.auto.add("project.search", sr, "Search"); // Column header: Name, Label, then the visible optional columns. diff --git a/crates/ui-egui/src/panels/render_queue.rs b/crates/ui-egui/src/panels/render_queue.rs index e07efe69e..6f9a539d0 100644 --- a/crates/ui-egui/src/panels/render_queue.rs +++ b/crates/ui-egui/src/panels/render_queue.rs @@ -456,7 +456,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let render_b = Rect::from_min_size(pos2(top.max.x - 12.0 - bw, top.min.y + 34.0), vec2(bw, 26.0)); let stop_b = Rect::from_min_size(pos2(render_b.min.x - 8.0 - bw, render_b.min.y), vec2(bw, 26.0)); let can_render = !rendering && queue.iter().any(RenderQueueItem::is_queued) && !available.is_empty(); - if widgets::text_button(ui, render_b, "Render", can_render, &t, egui::Id::new("rq-render")).clicked() && can_render { + if widgets::text_button(ui, render_b, crate::i18n::tr("Render"), can_render, &t, egui::Id::new("rq-render")).clicked() && can_render { actions.push(("renderQueue.render", json!({"wait": false}))); } app.auto.add("renderQueue.render", render_b, "Render"); @@ -469,7 +469,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, tr("Notify"), Tokens::ui(11.5), t.text_dim); app.auto.add("renderQueue.notify", nb, if notify { "Notify: on" } else { "Notify: off" }); if rendering { - if widgets::text_button(ui, stop_b, "Stop", false, &t, egui::Id::new("rq-stop")).clicked() { + if widgets::text_button(ui, stop_b, crate::i18n::tr("Stop"), false, &t, egui::Id::new("rq-stop")).clicked() { actions.push(("renderQueue.stop", json!({}))); } app.auto.add("renderQueue.stop", stop_b, "Stop"); diff --git a/crates/ui-egui/src/panels/templates.rs b/crates/ui-egui/src/panels/templates.rs index ec65ee6d7..a0db75d92 100644 --- a/crates/ui-egui/src/panels/templates.rs +++ b/crates/ui-egui/src/panels/templates.rs @@ -69,7 +69,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) { // Save the open project as a user template. let can_save = app.session.project.comps().next().is_some(); let sr = Rect::from_min_size(pos2(area.max.x - 210.0, y - 4.0), vec2(210.0, 28.0)); - let save = widgets::text_button(ui, sr, "Save Current as Template…", false, &t, egui::Id::new("home-templates-save")).clicked(); + let save = widgets::text_button(ui, sr, crate::i18n::tr("Save Current as Template…"), false, &t, egui::Id::new("home-templates-save")).clicked(); app.auto.add("home.templates.saveCurrent", sr, "Save Current as Template…"); if save { if can_save { diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index aff4ef2c8..4af02bc38 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1386,7 +1386,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let t = app.tokens; let ctx = ui.ctx().clone(); let Some(cid) = app.session.active_comp_id() else { - ui.painter().text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint); + ui.painter().text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint); // Dropping footage into an empty Timeline should create its first composition, // just like dropping onto the Project panel's New Composition button (#649). if let Some(crate::panels::DragPayload::Item(dragged)) = egui::DragAndDrop::payload::(&ctx).as_deref() @@ -1517,7 +1517,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } let sr = Rect::from_min_size(pos2(tc_rect.max.x + 14.0, top + 12.0), vec2((left_w - 420.0).clamp(120.0, 220.0), 22.0)); let mut search = app.ui.timeline.search.clone(); - widgets::search_field(ui, sr, &mut search, "Search", &t); + widgets::search_field(ui, sr, &mut search, crate::i18n::tr("Search"), &t); app.ui.timeline.search = search; app.auto.add("timeline.search", sr, "Search"); let mut bx = rect.min.x + left_w - 10.0; @@ -2518,7 +2518,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { RowKind::Waveform { item } => { lp.rect_filled(left, 0.0, if ri % 2 == 0 { t.row } else { t.row_alt }); let indent = cw.name + 6.0 + 14.0 * row.depth as f32; - lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, "Waveform", Tokens::ui(12.0), t.text); + lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Waveform"), Tokens::ui(12.0), t.text); let wr = Rect::from_min_max(pos2(graph_x0, r.min.y), r.max); gp.rect_filled(wr, 0.0, t.tl_bg); app.auto.add(&format!("timeline.layer.{}.waveform", layer.id.0), wr, "Waveform"); diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index 51bab7fda..cfbc4091f 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -745,7 +745,13 @@ fn locked_pane(app: &mut EffectcraftApp, ui: &mut egui::Ui, full: Rect, bg: Colo // Close the locked viewer. let close = Rect::from_min_size(pos2(r.max.x - 26.0, r.min.y + 4.0), vec2(20.0, 20.0)); let resp = ui.interact(close, egui::Id::new("viewer-locked-close"), Sense::click()); - p.text(close.center(), Align2::CENTER_CENTER, "\u{00d7}", Tokens::ui(14.0), if resp.hovered() { Color32::WHITE } else { Color32::from_white_alpha(170) }); + p.text( + close.center(), + Align2::CENTER_CENTER, + crate::i18n::tr("\u{00d7}"), + Tokens::ui(14.0), + if resp.hovered() { Color32::WHITE } else { Color32::from_white_alpha(170) }, + ); app.auto.add("viewer.locked.close", close, "Close Locked Viewer"); if resp.clicked() { let _ = app.session.execute("view.closeLockedViewer", serde_json::json!({})); @@ -2534,12 +2540,12 @@ fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let c = rect.center(); p.text(c - vec2(0.0, 40.0), Align2::CENTER_CENTER, crate::i18n::tr("Composition"), Tokens::semibold(16.0), t.text); let b1 = Rect::from_center_size(c + vec2(0.0, 0.0), vec2(220.0, 30.0)); - if widgets::text_button(ui, b1, "New Composition", true, &t, egui::Id::new("empty-newcomp")).clicked() { + if widgets::text_button(ui, b1, crate::i18n::tr("New Composition"), true, &t, egui::Id::new("empty-newcomp")).clicked() { crate::panels::dialogs::open_new_comp(app); } app.auto.add("viewer.empty.newComp", b1, "New Composition"); let b2 = Rect::from_center_size(c + vec2(0.0, 40.0), vec2(220.0, 30.0)); - if widgets::text_button(ui, b2, "New Composition From Footage", false, &t, egui::Id::new("empty-fromfootage")).clicked() { + if widgets::text_button(ui, b2, crate::i18n::tr("New Composition From Footage"), false, &t, egui::Id::new("empty-fromfootage")).clicked() { let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({})); } app.auto.add("viewer.empty.import", b2, "New Composition From Footage");