i18n: the layer and timeline context menus draw their items in the interface language (fixes #1308)

This commit is contained in:
Marco Arnulfo
2026-10-15 20:47:06 +02:00
parent 0311bdc34d
commit ded4f3925c
3 changed files with 92 additions and 18 deletions
+38 -13
View File
@@ -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
+7
View File
@@ -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"),
+47 -5
View File
@@ -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, &params, 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, &params, 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<String> {
fn collect(shape: &egui::Shape, out: &mut Vec<String>) {
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() {