diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 611a226ee..254eec49c 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -123,7 +123,7 @@ fn web_draws(language: &str) -> bool { /// The language EffectCraft shows for a BCP 47 locale (`ja-JP` → `ja`, `zh-Hans-CN` → `zh-hans`, /// `zh-TW` → `zh-hant`, `uk-UA` → `uk`, `es-ES` → `es`, `pt-BR` → `pt-br`, `cs-CZ` → `cs`, -/// `ka-GE` → `ka`). +/// `ka-GE` → `ka`, `ru-RU` → `ru`, `de-AT` → `de`, `fr-CA` → `fr`). fn supported(locale: Option<&str>) -> &'static str { let Some(locale) = locale else { return "en" }; let mut parts = locale.split(['-', '_']); @@ -135,6 +135,9 @@ fn supported(locale: Option<&str>) -> &'static str { "es" => "es", "cs" => "cs", "ka" => "ka", + "ru" => "ru", + "de" => "de", + "fr" => "fr", // The only Portuguese catalog: Brazil's, also for Portugal rather than English. "pt" => "pt-br", "zh" => { @@ -252,22 +255,32 @@ pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> Strin return effects::name(id, &e.label); } // Undo/redo names come from the history, so they are formatted per language here rather than - // looked up in a catalog. An English (or untranslated) interface keeps the shown label. - let (undo, no_undo, redo, no_redo) = match language(app) { - "ja" => ("取り消し", "取り消しできません", "やり直し", "やり直しできません"), - "zh-hans" => ("撤销", "无可撤销的操作", "重做", "无可重做的操作"), - "zh-hant" => ("復原", "沒有可復原的操作", "重做", "沒有可重做的操作"), - "uk" => ("Скасувати", "Немає дій для скасування", "Повторити", "Немає дій для повторення"), - "es" => ("Deshacer", "No se puede deshacer", "Rehacer", "No se puede rehacer"), - "pt-br" => ("Desfazer", "Não há nada para desfazer", "Refazer", "Não há nada para refazer"), - "cs" => ("Zpět", "Není co vrátit", "Znovu", "Není co zopakovat"), + // looked up in a catalog. A language without this wording keeps the shown undo/redo label, and + // the rest of its menu still goes through the catalog below (#1449). + let history = match language(app) { + "en" => return shown, + "ja" => Some(("取り消し", "取り消しできません", "やり直し", "やり直しできません")), + "zh-hans" => Some(("撤销", "无可撤销的操作", "重做", "无可重做的操作")), + "zh-hant" => Some(("復原", "沒有可復原的操作", "重做", "沒有可重做的操作")), + "uk" => Some(("Скасувати", "Немає дій для скасування", "Повторити", "Немає дій для повторення")), + "es" => Some(("Deshacer", "No se puede deshacer", "Rehacer", "No se puede rehacer")), + "pt-br" => Some(("Desfazer", "Não há nada para desfazer", "Refazer", "Não há nada para refazer")), + "cs" => Some(("Zpět", "Není co vrátit", "Znovu", "Není co zopakovat")), // A colon before the history's (untranslated) action name: "დაბრუნება: Comp1". - "ka" => ("დაბრუნება:", "დაბრუნება შეუძლებელია", "ხელახლა შესრულება:", "ხელახლა შესრულება შეუძლებელია"), - _ => return shown, + "ka" => Some(("დაბრუნება:", "დაბრუნება შეუძლებელია", "ხელახლა შესრულება:", "ხელახლა შესრულება შეუძლებელია")), + // A colon before the history's (untranslated) action name: "Отменить: Comp1". + "ru" => Some(("Отменить:", "Нечего отменять", "Повторить:", "Нечего повторять")), + _ => None, }; match e.command.as_str() { - "edit.undo" => app.session.history.undo.last().map(|u| format!("{undo} {}", u.0)).unwrap_or_else(|| no_undo.into()), - "edit.redo" => app.session.history.redo.last().map(|u| format!("{redo} {}", u.0)).unwrap_or_else(|| no_redo.into()), + "edit.undo" | "edit.redo" => { + let Some((undo, no_undo, redo, no_redo)) = history else { return shown }; + if e.command == "edit.undo" { + app.session.history.undo.last().map(|u| format!("{undo} {}", u.0)).unwrap_or_else(|| no_undo.into()) + } else { + app.session.history.redo.last().map(|u| format!("{redo} {}", u.0)).unwrap_or_else(|| no_redo.into()) + } + } "window.panel" => { let prefix = format!("{}: ", e.label); if let Some(name) = shown.strip_prefix(&prefix) { @@ -7569,9 +7582,19 @@ mod tests { (Some("ka"), "ka"), (Some("ka-GE"), "ka"), (Some("ka_GE.UTF-8"), "ka"), + (Some("ru"), "ru"), + (Some("ru-RU"), "ru"), + (Some("ru-SK"), "ru"), + (Some("ru_RU.UTF-8"), "ru"), + (Some("de"), "de"), + (Some("de-DE"), "de"), + (Some("de_AT.UTF-8"), "de"), + (Some("fr"), "fr"), + (Some("fr-FR"), "fr"), + (Some("fr_CA.UTF-8"), "fr"), (Some("en-US"), "en"), (Some("jv-ID"), "en"), - (Some("de-DE"), "en"), + (Some("nl-NL"), "en"), (None, "en"), ] { assert_eq!(supported(locale), language, "{locale:?}"); @@ -7629,6 +7652,35 @@ mod tests { assert_eq!(code("unknown"), "en"); } + /// Every catalog language draws its menu entries from the catalog, including those with no + /// undo/redo wording of their own: choosing Deutsch, Русский or Français left the menus in + /// English (#1449). + #[test] + fn every_catalog_language_translates_menu_entries() { + let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); + let menu_entry = |command: &str, label: &str| MenuEntry { + label: label.into(), + command: command.into(), + params: serde_json::Value::Null, + shortcut: None, + alt_shortcuts: Vec::new(), + }; + let new_comp = menu_entry("comp.new", "New Composition..."); + let undo = menu_entry("edit.undo", "Undo"); + for code in CATALOG_CODES { + app.session.execute("prefs.set", json!({"key":"general.language", "value":code})).unwrap(); + let drawn = entry(&app, &new_comp, new_comp.label.clone()); + assert_eq!(drawn, label(&app, "comp.new", "New Composition..."), "{code}"); + assert_ne!(drawn, new_comp.label, "{code}: the menu entry is drawn in English"); + } + app.session.execute("prefs.set", json!({"key":"general.language", "value":"ru"})).unwrap(); + assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Нечего отменять"); + app.session.execute("prefs.set", json!({"key":"general.language", "value":"de"})).unwrap(); + assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Can't Undo", "no German undo wording yet: the shown label"); + app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap(); + assert_eq!(entry(&app, &new_comp, new_comp.label.clone()), "New Composition..."); + } + #[test] fn general_settings_exposes_the_language_automation_id() { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());