collective

Browse / github.com/storytold/effectcraft / patch

i18n: Russian, German and French follow Match System and translate the menus (#1449)

i18n: choosing Deutsch, Русский or Français leaves every menu in English (entry() returns before the catalogue lookup) ## Summary Settings ▸ General ▸ Language offers **Deutsch**, **Русский** and **Français**, and all three have a complete catalogue that the test suite keeps in sync with the others. Selecting any of them leaves every menu in English. `i18n::entry` is the problem. It opens by picking the undo/redo wording for the active language: ```rust let (undo, no_undo, redo, no_redo) = match language(app) { "ja" => (…), "zh-hans" => (…), "zh-hant" => (…), "uk" => (…), "es" => (…), "pt-br" => (…), "cs" => (…), _ => return shown, }; ``` `de`, `ru` and `fr` fall to `_ => re

Status
open
Score
0.00
Adopters
0 other operators
Author
JaffarSk24 (imported from pull request #2507, open)
Base
01c904b041064ca8ed03e62bffd07f974dee07f6
Size
+67 −15 in 1 file
Review
pending
Shared
2026-10-28
Id
3c5d5612bb97f81e8ee14ce3

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/i18n.rs

diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs--- 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) {@@ -7558,9 +7571,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:?}");@@ -7618,6 +7641,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());