diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 4caae8dd..7dc93b97 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -118,19 +118,24 @@ pub(crate) fn label<'a>(app: &EffectcraftApp, command: &str, source: &'a str) -> _ => {} } } - match language { - "ja" => lookup(JAPANESE, command, source), - "zh-hans" => lookup(SIMPLIFIED_CHINESE, command, source), - "zh-hant" => lookup(TRADITIONAL_CHINESE, command, source), - "uk" => lookup(UKRAINIAN, command, source), - "es" => lookup(SPANISH, command, source), - "pt-br" => lookup(PORTUGUESE, command, source), - "ru" => lookup(RUSSIAN, command, source), - "fr" => lookup(FRENCH, command, source), - "de" => lookup(GERMAN, command, source), - "cs" => lookup(CZECH, command, source), - _ => source, - } + menu_catalog(language).map_or(source, |catalog| lookup(catalog, command, source)) +} + +/// The menu catalog for `language`; `None` for English. +fn menu_catalog(language: &str) -> Option<&'static [(&'static str, &'static str, &'static str)]> { + Some(match language { + "ja" => JAPANESE, + "zh-hans" => SIMPLIFIED_CHINESE, + "zh-hant" => TRADITIONAL_CHINESE, + "uk" => UKRAINIAN, + "es" => SPANISH, + "pt-br" => PORTUGUESE, + "ru" => RUSSIAN, + "fr" => FRENCH, + "de" => GERMAN, + "cs" => CZECH, + _ => return None, + }) } /// `source` looked up in `catalog` by (command, English label); the source itself when the pair is @@ -139,6 +144,26 @@ fn lookup<'a>(catalog: &[(&str, &str, &'a str)], command: &str, source: &'a str) catalog.iter().find(|(id, en, _)| *id == command && *en == source).map(|(_, _, translated)| *translated).unwrap_or(source) } +/// The text a context menu draws for an item that runs `command` with `params` (#1308). +/// +/// The Timeline's and the Composition panel's context menus repeat menu-bar commands, so they read +/// the menu bar's row for the same command, and an `effect.apply` item draws the effect's name as +/// the Effect menu does. The menu tree writes the ellipsis as `...` where the context menus write +/// `…`. A label with no menu row (Delete, which the menu bar calls Clear) goes through the panel +/// catalog. Only the drawn text is translated: the automation label stays the English `source`. +pub(crate) fn context_item(command: &str, params: &serde_json::Value, source: &str) -> String { + if command == "effect.apply" + && let Some(id) = params.get("effect").and_then(|v| v.as_str()) + { + return effects::name(id, source); + } + let ascii = source.strip_suffix('…').map(|s| format!("{s}...")); + let key = ascii.as_deref().unwrap_or(source); + menu_catalog(ui::current()) + .and_then(|catalog| catalog.iter().find(|(id, en, _)| *id == command && *en == key)) + .map_or_else(|| tr(source).to_string(), |(_, _, translated)| (*translated).to_string()) +} + pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> String { // The generated Effect menu's entries label themselves with the effect's registry name; the // catalog replaces it with the interface language's (#821). The entry keeps its command and diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 3a4dd4af..023dad48 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -226,6 +226,7 @@ const JAPANESE: &[(&str, &str)] = &[ ("Edit Target: {}", "編集対象:{}"), ("Edit in the Timeline", "タイムラインで編集"), ("EffectCraft didn't quit normally last time.", "EffectCraftは前回正常に終了しませんでした。"), + ("Effects", "エフェクト"), ("Effects & Presets options", "エフェクト&プリセットのオプション"), ("Copy Presets Folder Path", "プリセットフォルダーのパスをコピー"), ("Effects: {}", "エフェクト:{}"), @@ -770,6 +771,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Edit Target: {}", "编辑目标:{}"), ("Edit in the Timeline", "在时间轴中编辑"), ("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常退出。"), + ("Effects", "效果"), ("Effects & Presets options", "效果和预设选项"), ("Copy Presets Folder Path", "复制预设文件夹路径"), ("Effects: {}", "效果:{}"), @@ -1502,6 +1504,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Edit Target: {}", "編輯目標:{}"), ("Edit in the Timeline", "在時間軸中編輯"), ("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常結束。"), + ("Effects", "效果"), ("Effects & Presets options", "效果和預設集選項"), ("Copy Presets Folder Path", "複製預設集資料夾路徑"), ("Effects: {}", "效果:{}"), @@ -2237,6 +2240,7 @@ const UKRAINIAN: &[(&str, &str)] = &[ ("Edit Target: {}", "Ціль редагування: {}"), ("Edit in the Timeline", "Редагувати на шкалі часу"), ("EffectCraft didn't quit normally last time.", "Минулого разу EffectCraft завершив роботу некоректно."), + ("Effects", "Ефекти"), ("Effects & Presets options", "Параметри ефектів і наборів"), ("Copy Presets Folder Path", "Копіювати шлях до папки наборів"), ("Effects: {}", "Ефекти: {}"), @@ -2789,6 +2793,7 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("Edit Target: {}", "Editar destino: {}"), ("Edit in the Timeline", "Editar na linha do tempo"), ("EffectCraft didn't quit normally last time.", "O EffectCraft não foi encerrado normalmente da última vez."), + ("Effects", "Efeitos"), ("Effects & Presets options", "Opções de Efeitos e predefinições"), ("Copy Presets Folder Path", "Copiar caminho da pasta de predefinições"), ("Effects: {}", "Efeitos: {}"), @@ -3239,6 +3244,7 @@ const CZECH: &[(&str, &str)] = &[ ("Edit Target: {}", "Upravit cíl: {}"), ("Edit in the Timeline", "Upravit na časové ose"), ("EffectCraft didn't quit normally last time.", "EffectCraft nebyl při posledním použití ukončen běžným způsobem."), + ("Effects", "Efekty"), ("Effects & Presets options", "Možnosti efektů a přednastavení"), ("Copy Presets Folder Path", "Kopírovat cestu ke složce přednastavení"), ("Effects: {}", "Efekty: {}"), @@ -3795,6 +3801,7 @@ const SPANISH: &[(&str, &str)] = &[ ("Edit Target: {}", "Destino de la edición: {}"), ("Edit in the Timeline", "Editar en la cronología"), ("EffectCraft didn't quit normally last time.", "EffectCraft no se cerró normalmente la última vez."), + ("Effects", "Efectos"), ("Effects & Presets options", "Opciones de Efectos y ajustes preestablecidos"), ("Effects: {}", "Efectos: {}"), ("Empty folder", "Carpeta vacía"), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 163e6507..ce1c3e4d 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -2960,7 +2960,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { ("shape", "Shape Layer", "layer.newShape", json!({"kind": "none"})), ("adjustment", "Adjustment Layer", "layer.newAdjustment", json!({})), ] { - let response = ui.button(label); + let response = ui.button(crate::i18n::context_item(command, ¶ms, label)); app.auto.add(&format!("timeline.context.{area}.{key}"), response.rect, label); if response.clicked() { actions.push((command.into(), params)); @@ -3877,7 +3877,7 @@ fn layer_context_menu( pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], caps: LayerMenuCaps, actions: &mut Vec<(String, serde_json::Value)>) { let Some(&id) = layers.first() else { return }; let mut item = |ui: &mut egui::Ui, label: &str, cmd: &str, params: serde_json::Value| { - if ui.button(label).clicked() { + if ui.button(crate::i18n::context_item(cmd, ¶ms, label)).clicked() { actions.push(("layer.select".into(), json!({"layers": layers}))); actions.push((cmd.into(), params)); ui.close(); @@ -3885,7 +3885,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], caps: LayerMenuCaps, }; ui.menu_button(crate::i18n::tr("New"), |ui| { item(ui, "Text", "layer.newText", json!({})); - item(ui, "Solid…", "app.solidSettings", json!({})); + item(ui, "Solid…", "layer.newSolid", json!({})); item(ui, "Light…", "layer.newLight", json!({})); item(ui, "Camera…", "layer.newCamera", json!({})); item(ui, "Null Object", "layer.newNull", json!({})); @@ -3916,7 +3916,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], caps: LayerMenuCaps, item(ui, "Time-Reverse Layer", "layer.timeReverse", json!({"layers": layers})); item(ui, "Time Stretch…", "layer.timeStretch", json!({})); item(ui, "Freeze Frame", "layer.freezeFrame", json!({"layers": layers})); - item(ui, "Freeze on Last Frame", "layer.freezeOnLastFrame", json!({"layers": layers})); + item(ui, "Freeze On Last Frame", "layer.freezeOnLastFrame", json!({"layers": layers})); }); ui.menu_button(crate::i18n::tr("Markers"), |ui| { item(ui, "Add Marker", "layer.addMarker", json!({"layers": layers})); @@ -3959,7 +3959,7 @@ fn effects_submenu(ui: &mut egui::Ui, layers: &[u64], item: &mut dyn FnMut(&mut widgets::menu_scroll(ui, |ui| { for (cat, mut fx) in cats { fx.sort_by_key(|e| e.name.to_ascii_lowercase()); - ui.menu_button(cat, |ui| { + ui.menu_button(crate::i18n::effects::category(cat), |ui| { widgets::menu_scroll(ui, |ui| { for e in fx { item(ui, e.name, "effect.apply", json!({"effect": e.id, "layers": layers})); @@ -4531,6 +4531,48 @@ mod tests { assert!(c.contains(&comp.duration.seconds()) && c.contains(&0.0)); } + /// Every string drawn by one frame of `draw`. + fn drawn_text(mut draw: impl FnMut(&mut egui::Ui)) -> Vec { + fn collect(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Text(t) => out.push(t.galley.text().to_string()), + egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)), + _ => {} + } + } + let ctx = egui::Context::default(); + ctx.run_ui(Default::default(), |ui| draw(ui)).textures_delta.clear(); + let mut frame = ctx.run_ui(Default::default(), |ui| draw(ui)); + frame.textures_delta.clear(); + let mut out = Vec::new(); + frame.shapes.iter().for_each(|c| collect(&c.shape, &mut out)); + out + } + + /// #1308: the layer context menu draws its items in the interface language, from the menu + /// bar's rows for the same commands (whose ellipsis is `...`), and its Effects submenu draws + /// the categories the Effect menu does; the actions it records stay the same. + #[test] + fn the_layer_context_menu_draws_in_the_interface_language() { + let caps = LayerMenuCaps { vector: true, text: false }; + let mut actions = Vec::new(); + crate::i18n::set_current("zh-hans"); + let menu = drawn_text(|ui| layer_menu(ui, &[7], caps, &mut actions)); + let effects = drawn_text(|ui| effects_submenu(ui, &[7], &mut |_: &mut egui::Ui, _: &str, _: &str, _: serde_json::Value| {})); + crate::i18n::set_current("en"); + let english = drawn_text(|ui| layer_menu(ui, &[7], caps, &mut actions)); + for label in ["新建", "图层设置...", "从矢量图层创建形状", "预合成...", "重复", "拆分图层", "删除"] { + assert!(menu.iter().any(|t| t == label), "{label} missing from {menu:?}"); + } + assert!(!menu.iter().any(|t| t.is_ascii()), "English left in {menu:?}"); + assert!(effects.iter().any(|t| t == "模糊和锐化"), "{effects:?}"); + assert!(!effects.iter().any(|t| t == "Blur & Sharpen"), "{effects:?}"); + for label in ["Layer Settings…", "Duplicate", "Delete"] { + assert!(english.iter().any(|t| t == label), "{label} missing from {english:?}"); + } + assert!(actions.is_empty()); + } + /// UI fidelity (M13.5): compact After Effects-like rows and muted label-coloured bars. #[test] fn rows_and_bars_follow_after_effects_proportions() {