From ded4f3925ceed85660c5fef2430d3e599f94a17f Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 13:03:21 +0200 Subject: [PATCH 1/2] i18n: the layer and timeline context menus draw their items in the interface language (fixes #1308) --- crates/ui-egui/src/i18n.rs | 51 +++++++++++++++++++------- crates/ui-egui/src/i18n/ui.rs | 7 ++++ crates/ui-egui/src/panels/timeline.rs | 52 ++++++++++++++++++++++++--- 3 files changed, 92 insertions(+), 18 deletions(-) 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() { From f8d82b7265089ebc3dab814a509c78b49985970d Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Thu, 15 Oct 2026 21:23:14 +0200 Subject: [PATCH 2/2] i18n: the effect and property context menus draw their items in the interface language too (#1308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Effect Controls header menu and the Timeline property menu build their items with the same local closure the layer menu used, so they still drew English. Both now go through i18n::context_item; Copy, Paste, Edit Value… and Remove Expression get panel catalog rows. --- crates/ui-egui/src/i18n/ui.rs | 28 ++++++++++++++++ crates/ui-egui/src/panels/effect_controls.rs | 2 +- crates/ui-egui/src/panels/timeline.rs | 35 +++++++++++++++++++- crates/ui-egui/tests/ui_effect_names.rs | 35 ++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 023dad48..cf90170c 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -221,6 +221,10 @@ const JAPANESE: &[(&str, &str)] = &[ ("Drag to slip the source", "ドラッグしてソースをスリップ"), ("Drop Frame", "ドロップフレーム"), ("Duplicate", "複製"), + ("Copy", "コピー"), + ("Paste", "ペースト"), + ("Edit Value…", "値を編集…"), + ("Remove Expression", "エクスプレッションを削除"), ("Duration:", "デュレーション:"), ("Edit Gradient…", "グラデーションを編集…"), ("Edit Target: {}", "編集対象:{}"), @@ -766,6 +770,10 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Drag to slip the source", "拖动可滑移源"), ("Drop Frame", "丢帧"), ("Duplicate", "重复"), + ("Copy", "复制"), + ("Paste", "粘贴"), + ("Edit Value…", "编辑值…"), + ("Remove Expression", "移除表达式"), ("Duration:", "持续时间:"), ("Edit Gradient…", "编辑渐变…"), ("Edit Target: {}", "编辑目标:{}"), @@ -1499,6 +1507,10 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Drag to slip the source", "拖曳可滑移源"), ("Drop Frame", "丟格"), ("Duplicate", "重複"), + ("Copy", "拷貝"), + ("Paste", "貼上"), + ("Edit Value…", "編輯值…"), + ("Remove Expression", "移除運算式"), ("Duration:", "持續時間:"), ("Edit Gradient…", "編輯漸變…"), ("Edit Target: {}", "編輯目標:{}"), @@ -2235,6 +2247,10 @@ const UKRAINIAN: &[(&str, &str)] = &[ ("Drag to slip the source", "Перетягніть, щоб зсунути матеріал у межах шару"), ("Drop Frame", "Пропуск номерів"), ("Duplicate", "Дублювати"), + ("Copy", "Копіювати"), + ("Paste", "Вставити"), + ("Edit Value…", "Редагувати значення…"), + ("Remove Expression", "Вилучити вираз"), ("Duration:", "Тривалість:"), ("Edit Gradient…", "Редагувати градієнт…"), ("Edit Target: {}", "Ціль редагування: {}"), @@ -2788,6 +2804,10 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("Drag to slip the source", "Arraste para deslizar a origem"), ("Drop Frame", "Drop frame"), ("Duplicate", "Duplicar"), + ("Copy", "Copiar"), + ("Paste", "Colar"), + ("Edit Value…", "Editar valor…"), + ("Remove Expression", "Remover expressão"), ("Duration:", "Duração:"), ("Edit Gradient…", "Editar gradiente…"), ("Edit Target: {}", "Editar destino: {}"), @@ -3239,6 +3259,10 @@ const CZECH: &[(&str, &str)] = &[ ("Drag to slip the source", "Tažením posunete zdroj v čase"), ("Drop Frame", "Vynechávat čísla snímků"), ("Duplicate", "Duplikovat"), + ("Copy", "Kopírovat"), + ("Paste", "Vložit"), + ("Edit Value…", "Upravit hodnotu…"), + ("Remove Expression", "Odstranit výraz"), ("Duration:", "Délka trvání:"), ("Edit Gradient…", "Upravit přechod…"), ("Edit Target: {}", "Upravit cíl: {}"), @@ -3796,6 +3820,10 @@ const SPANISH: &[(&str, &str)] = &[ ("Drag to slip the source", "Arrastra para deslizar el origen"), ("Drop Frame", "Drop frame"), ("Duplicate", "Duplicar"), + ("Copy", "Copiar"), + ("Paste", "Pegar"), + ("Edit Value…", "Editar valor…"), + ("Remove Expression", "Quitar expresión"), ("Duration:", "Duración:"), ("Edit Gradient…", "Editar degradado…"), ("Edit Target: {}", "Destino de la edición: {}"), diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index bad1db39..05b58d61 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -1279,7 +1279,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let lid = layer.id.0; let mut picked_label: Option = None; let mut item = |ui: &mut egui::Ui, label: &str, enabled: bool, cmd: &str, params: serde_json::Value| { - if ui.add_enabled(enabled, egui::Button::new(label)).clicked() { + if ui.add_enabled(enabled, egui::Button::new(crate::i18n::context_item(cmd, ¶ms, label))).clicked() { actions.push((cmd.into(), params)); ui.close(); } diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index ce1c3e4d..425d48c3 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3796,7 +3796,7 @@ fn prop_menu( ) { let (l, uid) = (layer.id.0, prop.uid); let mut item = |ui: &mut egui::Ui, key: &str, label: &str, selected: bool, cmd: &str, params: serde_json::Value| { - let resp = ui.add(egui::Button::new(label).selected(selected)); + let resp = ui.add(egui::Button::new(crate::i18n::context_item(cmd, ¶ms, label)).selected(selected)); auto.add(&format!("timeline.prop.{uid}.menu.{key}"), resp.rect, label); if resp.clicked() { actions.push((cmd.into(), params)); @@ -4573,6 +4573,39 @@ mod tests { assert!(actions.is_empty()); } + /// The property context menu draws its items in the interface language too: the commands the + /// menu bar has from its rows, Reset, Edit Value… and Remove Expression from the panel catalog. + /// The automation labels stay English. + #[test] + fn the_property_context_menu_draws_in_the_interface_language() { + let mut s = effectcraft_engine::Session::default(); + s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "duration": 4})).unwrap(); + let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap(); + let opacity = s.active_comp().unwrap().layer(LayerId(l)).unwrap().props.prop("transform/opacity").unwrap().uid; + s.execute("prop.setExpression", json!({"layer": l, "prop": opacity})).unwrap(); + let comp = s.active_comp_id().unwrap(); + let layer = s.active_comp().unwrap().layer(LayerId(l)).unwrap().clone(); + let (position, opacity) = (layer.props.prop("transform/position").unwrap(), layer.props.prop("transform/opacity").unwrap()); + let (mut auto, mut actions) = (crate::automation::Registry::default(), Vec::new()); + let mut menu = |language| { + crate::i18n::set_current(language); + let mut drawn = drawn_text(|ui| prop_menu(ui, &mut auto, comp, &layer, position, &mut actions)); + drawn.extend(drawn_text(|ui| prop_menu(ui, &mut auto, comp, &layer, opacity, &mut actions))); + drawn + }; + let chinese = menu("zh-hans"); + let english = menu("en"); + for label in ["重置", "编辑值…", "单独尺寸", "添加表达式", "移除表达式", "向基本图形添加属性"] { + assert!(chinese.iter().any(|t| t == label), "{label} missing from {chinese:?}"); + } + assert!(!chinese.iter().any(|t| t.is_ascii()), "English left in {chinese:?}"); + for label in ["Reset", "Edit Value…", "Separate Dimensions", "Remove Expression", "Add Property to Essential Graphics"] { + assert!(english.iter().any(|t| t == label), "{label} missing from {english:?}"); + } + assert!(auto.elements.iter().any(|e| e.label == "Remove Expression"), "the automation label stays 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() { diff --git a/crates/ui-egui/tests/ui_effect_names.rs b/crates/ui-egui/tests/ui_effect_names.rs index c65d2cea..28d1a1f3 100644 --- a/crates/ui-egui/tests/ui_effect_names.rs +++ b/crates/ui-egui/tests/ui_effect_names.rs @@ -85,6 +85,41 @@ fn the_generated_effect_menu_is_translated() { assert_eq!(menu_label(&en, "Effect", "Blur & Sharpen", BLUR).as_deref(), Some(BLUR_EN), "the English menu is unchanged"); } +/// Right-click the Effect Controls header of effect `fx`. +fn open_effect_menu(h: &mut Harness<'_, EffectcraftApp>, fx: u64) { + let e = h.state().auto.find(&format!("effectControls.effect.{fx}")).expect("the effect's header").rect; + let pos = egui::pos2(e[0] + e[2] / 2.0, e[1] + e[3] / 2.0); + h.event(egui::Event::PointerMoved(pos)); + h.step(); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() }); + h.step(); + } + h.run_steps(2); +} + +/// The effect header's context menu draws its items in the interface language, like the layer +/// menu (#1308): it used to draw Copy, Paste, Duplicate, Reset, Remove, Move Up and Move Down in +/// English whatever the language. +#[test] +fn the_effect_context_menu_follows_the_interface_language() { + const ENGLISH: [&str; 7] = ["Copy", "Paste", "Duplicate", "Reset", "Remove", "Move Up", "Move Down"]; + let (mut h, fx) = harness("zh-hans"); + open_effect_menu(&mut h, fx); + for label in ["复制", "粘贴", "重复", "重命名", "重置", "移除", "上移", "下移"] { + assert!(h.query_by_label(label).is_some(), "{label} missing from the effect menu"); + } + for label in ENGLISH { + assert!(h.query_by_label(label).is_none(), "{label} drawn in English"); + } + + let (mut en, fx) = harness("en"); + open_effect_menu(&mut en, fx); + for label in ENGLISH { + assert!(en.query_by_label(label).is_some(), "{label} missing from the English menu"); + } +} + /// Everything an agent or a script addresses stays English: the automation label, the property /// tree's group name and `effect.list`. #[test]