collective

Browse / github.com/storytold/lightcraft / patch

LR-BEHAV-LOCALE: the Edit menu drops its 18-row Language submenu; Settings has the choice

LR-BEHAV-LOCALE: the Edit menu drops its 18-row Language submenu; Settings has the choice Off `main`. ## Problem Edit ends with a Language submenu of 18 rows, one per interface language. The same choice already sits in Settings ▸ General ▸ Language (every language, the active one marked), so the menu repeats it. Mac apps keep the language in Settings or follow the system, and the 18 rows are the longest submenu in the bar. ## Change - The 18 `app.language.*` commands no longer name a menu (`crates/ui-egui/src/menus/ui_commands.rs`), so Edit ▸ Language is gone from the native macOS bar and the in-window bar on Windows, Linux and the web. The commands stay in the registry: agents

Status
open
Score
0.00
Adopters
0 other operators
Author
nledford (imported from pull request #3201, open)
Base
07bc2008d9457603bb77f6b3de33a901db75a8c4
Size
+98 −67 in 26 files
Review
pending
Shared
2026-10-28
Id
c56f2ac5ea6eade4143557bc

How it went

No other operator has reported using it yet.

Diff

raw · README.md README.zh-hans.md README.zh-hant.md crates/ui-egui/src/i18n.rs crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/menus/menubar.rs crates/ui-egui/src/menus/menubar/tests.rs crates/ui-egui/src/menus/ui_commands.rs crates/ui-egui/src/tests/dialog_settings.rs docs/localization-cs.md docs/localization-de.md docs/localization-es.md docs/localization-fi.md docs/localization-id.md docs/localization-ja.md docs/localization-ka.md docs/localization-pl.md docs/localization-pt-br.md docs/localization-ru.md docs/localization-vi.md docs/localization-zh-hans.md docs/localization-zh-hant.md docs/localization.md docs/parity-checklist.md docs/whats-new.md xtask/src/parity.rs

diff --git a/README.md b/README.md--- a/README.md+++ b/README.md@@ -468,8 +468,7 @@ CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p lightcraft Without it LightCraft builds and runs the same, but Chinese and Japanese text have no glyphs. Fonts are never committed to this repo — the standard for what goes there lives with it: see the [craft-fonts README](https://github.com/storytold/craft-fonts#readme). -**Interface language:** **Edit → Language** (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Español, Deutsch, Русский, Français, Українська, Polski, Čeština, Italiano, ქართული, Svenska, Suomi, Bahasa Indonesia, Tiếng Việt) or **Settings → General →-Language**; the choice applies immediately and persists. See [docs/localization.md](docs/localization.md).+**Interface language:** **Settings → General → Language** (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Español, Deutsch, Русский, Français, Українська, Polski, Čeština, Italiano, ქართული, Svenska, Suomi, Bahasa Indonesia, Tiếng Việt); the choice applies immediately and persists. See [docs/localization.md](docs/localization.md).  **Logs:** the desktop app writes its log to standard error and to `logs/lightcraft.log` in its settings folder (Linux `$XDG_CONFIG_HOME/lightcraft/logs/`, by default `~/.config/lightcraft/logs/`; macOSdiff --git a/README.zh-hans.md b/README.zh-hans.md--- a/README.zh-hans.md+++ b/README.zh-hans.md@@ -394,8 +394,7 @@ CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p lightcraft 不指定它时,LightCraft 构建和运行的方式完全相同,只是中文和日文文字没有字形。字体从不提交到 本仓库;见 [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)。 -**界面语言:** **编辑 → 语言**(English、简体中文、繁體中文(台灣)、日本語、Português (Brasil)、Español、Deutsch、Русский、Français)或 **设置 → 一般 →-语言**;选择立即生效并会保留。见 [docs/localization.md](docs/localization.md)。+**界面语言:** **设置 → 一般 → 语言**(English、简体中文、繁體中文(台灣)、日本語、Português (Brasil)、Español、Deutsch、Русский、Français);选择立即生效并会保留。见 [docs/localization.md](docs/localization.md)。  **日志:** 桌面应用把日志写到标准错误,以及设置文件夹里的 `logs/lightcraft.log` (Linux `$XDG_CONFIG_HOME/lightcraft/logs/`,默认 `~/.config/lightcraft/logs/`;macOSdiff --git a/README.zh-hant.md b/README.zh-hant.md--- a/README.zh-hant.md+++ b/README.zh-hant.md@@ -419,8 +419,7 @@ CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p lightcraft 不指定它時,LightCraft 一樣能建置與執行,只是中文和日文文字沒有字形。字型從不提交到 本儲存庫,字型的收錄規範記載在該儲存庫:見 [craft-fonts README](https://github.com/storytold/craft-fonts#readme)。 -**介面語言:** **編輯 → 語言**(English、简体中文、繁體中文(台灣)、日本語、Português (Brasil)、Español、Deutsch、Русский、Français、Українська、Čeština、ქართული)或 **設定 → 一般 →-語言**;選擇後立即生效並會保留。詳見 [docs/localization.md](docs/localization.md)。+**介面語言:** **設定 → 一般 → 語言**(English、简体中文、繁體中文(台灣)、日本語、Português (Brasil)、Español、Deutsch、Русский、Français、Українська、Čeština、ქართული);選擇後立即生效並會保留。詳見 [docs/localization.md](docs/localization.md)。  **記錄檔:** 桌面應用程式會把記錄檔寫到標準錯誤輸出,以及設定資料夾裡的 `logs/lightcraft.log` (Linux `$XDG_CONFIG_HOME/lightcraft/logs/`,預設為 `~/.config/lightcraft/logs/`;macOSdiff --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@@ -1,10 +1,10 @@ //! Presentation-only localization. Command ids, user names and catalog data remain unchanged. //! //! A language is one entry in [`language_table!`] below: its BCP-47 code, the endonym shown in the-//! Language menus, the ISO 15924 script its text needs (which picks the CJK font fallback), and its+//! Settings language row, the ISO 15924 script its text needs (which picks the CJK font fallback), and its //! message catalog (`locales/<code>.json`, embedded at build time). Adding a language is that entry //! plus its catalogs and the corresponding language command in `menus.rs`. Settings read the-//! locale table, while menus and the control channel share the command mapping.+//! locale table, while the control channel uses the command mapping.  use std::{cell::RefCell, collections::BTreeMap, sync::OnceLock}; @@ -34,7 +34,7 @@ macro_rules! language_table {                 match self { Self::$source => $source_code, $(Self::$variant => $code,)* }             } -            /// The language's own name, as shown in the Language menus.+            /// The language's own name, as shown in the Settings language row.             pub const fn name(self) -> &'static str {                 match self { Self::$source => "English", $(Self::$variant => $name,)* }             }@@ -140,7 +140,7 @@ language_table! { }  impl Locale {-    /// Every language in the order the Settings row and Edit ▸ Language list them: English first+    /// Every language in the order the Settings row lists them: English first     /// (the source language), then the rest by their own name with accents folded, so Čeština     /// sits under C. Names in other scripts follow the Latin ones in code point order: Cyrillic,     /// Georgian, then the CJK languages.@@ -581,9 +581,9 @@ mod picker_order_tests {     }      #[test]-    fn language_menu_lists_the_picker_order() {+    fn language_commands_list_the_picker_order() {         let menu: Vec<&str> = crate::menus::LANGUAGE_COMMANDS.iter().map(|c| c.1).collect();         let picker: Vec<&str> = Locale::for_picker().into_iter().map(Locale::name).collect();-        assert_eq!(menu, picker, "Edit ▸ Language and the Settings row list the languages the same way");+        assert_eq!(menu, picker, "the language commands and the Settings row list the languages the same way");     } }diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs--- a/crates/ui-egui/src/i18n/tests.rs+++ b/crates/ui-egui/src/i18n/tests.rs@@ -22,7 +22,6 @@ fn languages_switch_persist_and_keep_user_values() {         app.run(&command, serde_json::json!({})).unwrap();         assert_eq!(app.ui.language, locale);         assert_eq!(language(), locale);-        assert_eq!(crate::menubar::checked(&app, &command), Some(true));         assert_eq!(builtin_label("Warm Glow", false), "Warm Glow");         assert_eq!(builtin_label("Warm Glow", true), locale.tr("Warm Glow"));         assert_eq!(tr("photo-{title}.jpg"), "photo-{title}.jpg");@@ -334,7 +333,7 @@ fn translated_catalogs_do_not_use_missing_menu_triangles() {     } } -/// The language menu covers every language, and a language's own command selects it.+/// Every language has a command, and a language's own command selects it. #[test] fn language_commands_cover_every_language() {     let commands = [@@ -357,11 +356,11 @@ fn language_commands_cover_every_language() {         ("app.language.indonesian", Locale::Id),         ("app.language.vietnamese", Locale::Vi),     ];-    // One command per language, and every command reachable from the menu table.+    // One command per language, and every command in the UI command tables.     assert_eq!(commands.len(), Locale::ALL.len());     for (id, language) in commands {         assert_eq!(crate::menus::language_from_command(id), Some(language), "{id}");-        assert!(crate::menus::ui_commands().any(|command| command.0 == id), "{id} is not in the menu");+        assert!(crate::menus::ui_commands().any(|command| command.0 == id), "{id} is not a UI command");     }     assert_eq!(crate::menus::language_from_command("app.language.klingon"), None);     assert_eq!(crate::menus::language_from_command("view.detail"), None);@@ -527,7 +526,6 @@ fn german_switches_via_menu_and_control_and_persists() {     app.run("app.language.german", serde_json::json!({})).unwrap();     assert_eq!(app.ui.language, Locale::De);     assert_eq!(language(), Locale::De);-    assert_eq!(crate::menubar::checked(&app, "app.language.german"), Some(true));     let saved = serde_json::to_string(&app.ui).unwrap();     assert!(saved.contains(r#""language":"de""#));     assert_eq!(serde_json::from_str::<crate::state::UiState>(&saved).unwrap().language, Locale::De);@@ -815,8 +813,6 @@ fn traditional_chinese_switches_through_menu_and_control_and_persists() {     assert_eq!(source_label(lightcraft_engine::LibrarySource::All, &app.session.catalog), Locale::ZhHant.tr("All Photos"));     assert_eq!(date_group_label("", false), Locale::ZhHant.tr("Unknown Date"));     assert_eq!(display_time("a-user-value"), "a-user-value");-    assert_eq!(crate::menubar::checked(&app, "app.language.traditionalChinese"), Some(true));-    assert_eq!(crate::menubar::checked(&app, "app.language.english"), Some(false));     assert_eq!(tr("my-photo.jpg"), "my-photo.jpg");     for id in ["album.addPhotos", "metadata.applyPreset", "label.applySet"] {         assert_eq!(crate::menubar::display_item_label(id, &serde_json::json!({"id": 1}), "Color"), "Color");@@ -1138,7 +1134,6 @@ fn polish_switches_formats_dates_and_reads_every_count() {     app.run("app.language.polish", serde_json::json!({})).unwrap();     assert_eq!(app.ui.language, Locale::Pl);     assert_eq!(Locale::Pl.name(), "Polski");-    assert_eq!(crate::menubar::checked(&app, "app.language.polish"), Some(true));     assert_eq!(tr("File"), "Plik");     assert_eq!(tr("Exposure"), "Ekspozycja");     assert_eq!(date_group_label("2026-09-20", false), "Niedziela, 20.09.2026");diff --git a/crates/ui-egui/src/menus/menubar.rs b/crates/ui-egui/src/menus/menubar.rs--- a/crates/ui-egui/src/menus/menubar.rs+++ b/crates/ui-egui/src/menus/menubar.rs@@ -388,8 +388,6 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option<bool> {     let u = &app.ui;     let panel = |p: RightPanel| Some(u.right == p);     match id {-        // Every language's command is checked when it is the active one.-        _ if crate::menus::language_from_command(id).is_some() => Some(crate::menus::language_from_command(id) == Some(u.language)),         "develop.autoSync" => Some(app.session.auto_sync),         "view.photoCounts" => Some(u.show_counts),         "view.secondWindow" => Some(u.second_window),diff --git a/crates/ui-egui/src/menus/menubar/tests.rs b/crates/ui-egui/src/menus/menubar/tests.rs--- a/crates/ui-egui/src/menus/menubar/tests.rs+++ b/crates/ui-egui/src/menus/menubar/tests.rs@@ -823,3 +823,20 @@ fn no_two_menu_commands_share_a_label() {     }     assert!(clashes.is_empty(), "{clashes:#?}"); }++/// The interface language is chosen in Settings ▸ General (one choice per language), so Edit has no+/// Language submenu of 18 rows. The commands stay: agents and tests still switch language by id.+#[test]+fn edit_menu_has_no_language_submenu_because_settings_has_the_choice() {+    let mut a = app();+    let bar = menu_bar(&a);+    let edit = bar.iter().find(|(t, _)| t == "Edit").expect("Edit menu").1.as_slice();+    assert!(!edit.iter().any(|n| matches!(n, MenuNode::Submenu { label, .. } if label == "Language")), "Edit still has a Language submenu");+    for (title, nodes) in &bar {+        for command in crate::menus::LANGUAGE_COMMANDS {+            assert!(find(nodes, command.0).is_none(), "{} is in {title}", command.0);+        }+    }+    run_item(&mut a, "app.language.german", Value::Null).expect("the command still switches language");+    assert_eq!(a.ui.language, crate::i18n::Locale::De);+}diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs--- a/crates/ui-egui/src/menus/ui_commands.rs+++ b/crates/ui-egui/src/menus/ui_commands.rs@@ -1,28 +1,29 @@ use super::*; -/// The "Edit → Language" entries, one per language in [`crate::i18n::Locale::ALL`], so a language-/// has a matching menu command (also listed through the control channel). Tests check coverage.+/// One command per language in [`crate::i18n::Locale::ALL`], so a language has a matching command+/// (also listed through the control channel). They are in no menu: Settings ▸ General has the+/// language choice. Tests check coverage. pub const LANGUAGE_COMMANDS: &[UiCommand] = &[-    ("app.language.english", crate::i18n::Locale::En.name(), None, "Edit>Language"),+    ("app.language.english", crate::i18n::Locale::En.name(), None, ""),     // The picker order (`Locale::for_picker`): alphabetical by the language's own name after     // English; Cyrillic, Georgian, then CJK names follow the Latin ones. A test keeps them equal.-    ("app.language.indonesian", crate::i18n::Locale::Id.name(), None, "Edit>Language"),-    ("app.language.czech", crate::i18n::Locale::Cs.name(), None, "Edit>Language"),-    ("app.language.german", crate::i18n::Locale::De.name(), None, "Edit>Language"),-    ("app.language.spanish", crate::i18n::Locale::Es.name(), None, "Edit>Language"),-    ("app.language.french", crate::i18n::Locale::Fr.name(), None, "Edit>Language"),-    ("app.language.italian", crate::i18n::Locale::It.name(), None, "Edit>Language"),-    ("app.language.polish", crate::i18n::Locale::Pl.name(), None, "Edit>Language"),-    ("app.language.portuguese", crate::i18n::Locale::PtBr.name(), None, "Edit>Language"),-    ("app.language.finnish", crate::i18n::Locale::Fi.name(), None, "Edit>Language"),-    ("app.language.swedish", crate::i18n::Locale::Sv.name(), None, "Edit>Language"),-    ("app.language.vietnamese", crate::i18n::Locale::Vi.name(), None, "Edit>Language"),-    ("app.language.russian", crate::i18n::Locale::Ru.name(), None, "Edit>Language"),-    ("app.language.ukrainian", crate::i18n::Locale::Uk.name(), None, "Edit>Language"),-    ("app.language.georgian", crate::i18n::Locale::Ka.name(), None, "Edit>Language"),-    ("app.language.japanese", crate::i18n::Locale::Ja.name(), None, "Edit>Language"),-    ("app.language.simplifiedChinese", crate::i18n::Locale::ZhHans.name(), None, "Edit>Language"),-    ("app.language.traditionalChinese", crate::i18n::Locale::ZhHant.name(), None, "Edit>Language"),+    ("app.language.indonesian", crate::i18n::Locale::Id.name(), None, ""),+    ("app.language.czech", crate::i18n::Locale::Cs.name(), None, ""),+    ("app.language.german", crate::i18n::Locale::De.name(), None, ""),+    ("app.language.spanish", crate::i18n::Locale::Es.name(), None, ""),+    ("app.language.french", crate::i18n::Locale::Fr.name(), None, ""),+    ("app.language.italian", crate::i18n::Locale::It.name(), None, ""),+    ("app.language.polish", crate::i18n::Locale::Pl.name(), None, ""),+    ("app.language.portuguese", crate::i18n::Locale::PtBr.name(), None, ""),+    ("app.language.finnish", crate::i18n::Locale::Fi.name(), None, ""),+    ("app.language.swedish", crate::i18n::Locale::Sv.name(), None, ""),+    ("app.language.vietnamese", crate::i18n::Locale::Vi.name(), None, ""),+    ("app.language.russian", crate::i18n::Locale::Ru.name(), None, ""),+    ("app.language.ukrainian", crate::i18n::Locale::Uk.name(), None, ""),+    ("app.language.georgian", crate::i18n::Locale::Ka.name(), None, ""),+    ("app.language.japanese", crate::i18n::Locale::Ja.name(), None, ""),+    ("app.language.simplifiedChinese", crate::i18n::Locale::ZhHans.name(), None, ""),+    ("app.language.traditionalChinese", crate::i18n::Locale::ZhHant.name(), None, ""), ];  /// Every UI command: the languages, then everything else. `xtask parity` reads both tables from@@ -31,8 +32,8 @@ pub fn ui_commands() -> impl Iterator<Item = &'static UiCommand> {     LANGUAGE_COMMANDS.iter().chain(UI_COMMANDS) } -/// The language a Language-menu command selects, if the id is one. The engine and the UI both go-/// through here, so the menu, the settings row and the control channel agree on the mapping.+/// The language a language command selects, if the id is one. The engine and the UI both go+/// through here, so the settings row and the control channel agree on the mapping. pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {     match id {         "app.language.english" => Some(crate::i18n::Locale::En),diff --git a/crates/ui-egui/src/tests/dialog_settings.rs b/crates/ui-egui/src/tests/dialog_settings.rs--- a/crates/ui-egui/src/tests/dialog_settings.rs+++ b/crates/ui-egui/src/tests/dialog_settings.rs@@ -390,3 +390,21 @@ fn each_tab_remembers_how_far_it_was_scrolled() {     show(&mut h, "import");     assert!((rect(&h, "check:settings.perCamera").top() - scrolled).abs() < 0.5, "Import is where it was left after a reopen"); }++/// Settings ▸ General ▸ Language is where the interface language is chosen (Edit has no Language+/// menu): each choice switches the app at once.+#[test]+fn each_language_choice_in_general_switches_the_interface() {+    let mut h = demo([1600.0, 1000.0]);+    show(&mut h, "general");+    let picker = crate::i18n::Locale::for_picker();+    for (i, language) in picker.iter().enumerate() {+        let r = h.request("ui.clickWidget", json!({"id": format!("button:settingsLanguage-{i}")}), T);+        assert_eq!(r["ok"], true, "{language:?}: {r}");+        h.step();+        h.step();+        assert_eq!(h.app.ui.language, *language, "choice {i}");+        assert_eq!(crate::i18n::language(), *language, "choice {i} is applied at once");+    }+    crate::i18n::set_language(crate::i18n::Locale::En);+}diff --git a/docs/localization-cs.md b/docs/localization-cs.md--- a/docs/localization-cs.md+++ b/docs/localization-cs.md@@ -1,7 +1,6 @@ # Čeština -Češtinu (`cs`, English name **Czech**, native name **Čeština**) zapnete přes **Úpravy → Jazyk →-Čeština** nebo **Nastavení → Obecné → Jazyk**. Změna platí ihned a ukládá se pro další spuštění.+Češtinu (`cs`, English name **Czech**, native name **Čeština**) zapnete přes **Nastavení → Obecné → Jazyk → Čeština**. Změna platí ihned a ukládá se pro další spuštění. Příkaz je `app.language.czech`; řídicí kanál přijímá `ui.set {"language":"cs"}` i značky `cs-CZ` a `cs_CZ.UTF-8`. Pro běh bez okna lze nastavit `LIGHTCRAFT_LANGUAGE=cs`. diff --git a/docs/localization-de.md b/docs/localization-de.md--- a/docs/localization-de.md+++ b/docs/localization-de.md@@ -1,6 +1,6 @@ # LightCraft auf Deutsch -Wähle **Bearbeiten → Sprache → Deutsch** oder **Einstellungen → Allgemein → Sprache**.+Wähle **Einstellungen → Allgemein → Sprache → Deutsch**. Die Auswahl gilt sofort und wird für den nächsten Start gespeichert (`language: "de"` in `ui.json`).  Die deutschen Kataloge decken alle Schlüssel der bisherigen Übersetzungen sowie sämtlichediff --git a/docs/localization-es.md b/docs/localization-es.md--- a/docs/localization-es.md+++ b/docs/localization-es.md@@ -1,6 +1,6 @@ # LightCraft en español -Elegir **Editar → Idioma → Español** o **Ajustes → General → Idioma**. El cambio se aplica al+Elegir **Ajustes → General → Idioma → Español**. El cambio se aplica al momento y se conserva para el próximo inicio (`language: "es"` en `ui.json`). `LIGHTCRAFT_LANGUAGE` acepta cualquier variante del español (`es`, `es-ES`, `es_MX.UTF-8`, `es-419`…). diff --git a/docs/localization-fi.md b/docs/localization-fi.md--- a/docs/localization-fi.md+++ b/docs/localization-fi.md@@ -1,7 +1,7 @@ # Suomi  Suomen (`fi`, englanniksi **Finnish**, omakielinen nimi **Suomi**) saa käyttöön valitsemalla-**Muokkaa → Kieli → Suomi** tai **Asetukset → Yleiset → Kieli**. Muutos tulee voimaan heti ja+**Asetukset → Yleiset → Kieli → Suomi**. Muutos tulee voimaan heti ja tallentuu seuraavaa käynnistystä varten. Komento on `app.language.finnish`; ohjauskanava hyväksyy `ui.set {"language":"fi"}` sekä tunnisteet `fi-FI` ja `fi_FI.UTF-8`. Ikkunattomassa ajossa voi käyttää `LIGHTCRAFT_LANGUAGE=fi`.diff --git a/docs/localization-id.md b/docs/localization-id.md--- a/docs/localization-id.md+++ b/docs/localization-id.md@@ -1,6 +1,6 @@ # LightCraft in Indonesian -Choose **Edit → Language → Bahasa Indonesia** or **Settings → General → Language**. The choice applies+Choose **Settings → General → Language → Bahasa Indonesia**. The choice applies at once and is saved for the next launch (`language: "id"` in `ui.json`); the control command is `app.language.indonesian`. `LIGHTCRAFT_LANGUAGE` selects it for headless runs and accepts `id`, `id-ID` and `id_ID.UTF-8`; the saved value is always `id`. LightCraft does not follow the system language, fordiff --git a/docs/localization-ja.md b/docs/localization-ja.md--- a/docs/localization-ja.md+++ b/docs/localization-ja.md@@ -3,7 +3,7 @@ LightCraft は日本語・簡体字中国語・英語(English)の表示に対応しています。 言語設定は `ui.json` の `language` に保存します。 -- 「編集 → 言語」または「設定 → 一般 → 言語」で切り替えます。選択は次回起動にも引き継ぎます。+- 「設定 → 一般 → 言語」で切り替えます。選択は次回起動にも引き継ぎます。 - メニュー、写真編集、マスク、切り抜き、設定、読み込み、書き出し、主な処理状況を翻訳しています。 - 通常表示と太字の双方に、SIL OFLのBIZ UDPGothicを使います。日本語フォントはこのリポジトリではなく   [storytold/craft-fonts](https://github.com/storytold/craft-fonts) にあり、任意のビルド入力diff --git a/docs/localization-ka.md b/docs/localization-ka.md--- a/docs/localization-ka.md+++ b/docs/localization-ka.md@@ -1,6 +1,6 @@ # LightCraft in Georgian -Choose **Edit → Language → ქართული** or **Settings → General → Language**. The choice applies at once+Choose **Settings → General → Language → ქართული**. The choice applies at once and is saved for the next launch (`language: "ka"` in `ui.json`); the control command is `app.language.georgian`. `LIGHTCRAFT_LANGUAGE` selects it for headless runs and accepts `ka`, `ka-GE` and `ka_GE.UTF-8`; the saved value is always `ka`. LightCraft does not follow the system language,diff --git a/docs/localization-pl.md b/docs/localization-pl.md--- a/docs/localization-pl.md+++ b/docs/localization-pl.md@@ -1,7 +1,7 @@ # Polski -Język polski (`pl`, English name **Polish**, nazwa natywna **Polski**) włącza się przez **Edytuj → Język →-Polski** albo **Ustawienia → Ogólne → Język**. Zmiana działa od razu i jest zapamiętywana przy kolejnym+Język polski (`pl`, English name **Polish**, nazwa natywna **Polski**) włącza się przez **Ustawienia → Ogólne → Język →+Polski**. Zmiana działa od razu i jest zapamiętywana przy kolejnym uruchomieniu. Polecenie to `app.language.polish`; kanał sterowania przyjmuje `ui.set {"language":"pl"}` oraz znaczniki `pl-PL` i `pl_PL.UTF-8`. Bez okna można ustawić `LIGHTCRAFT_LANGUAGE=pl`. diff --git a/docs/localization-pt-br.md b/docs/localization-pt-br.md--- a/docs/localization-pt-br.md+++ b/docs/localization-pt-br.md@@ -3,7 +3,7 @@ O LightCraft pode ser exibido em inglês, chinês simplificado, chinês tradicional (Taiwan), japonês e português do Brasil. A preferência de idioma é gravada em `language` no `ui.json`. -- Troque em **Editar → Idioma** ou em **Configurações → Geral → Idioma**. A escolha é mantida nas+- Troque em **Configurações → Geral → Idioma**. A escolha é mantida nas   próximas inicializações. - Traduzimos menus, edição de fotos, máscaras, recorte, configurações, importação, exportação e as   principais mensagens de progresso.diff --git a/docs/localization-ru.md b/docs/localization-ru.md--- a/docs/localization-ru.md+++ b/docs/localization-ru.md@@ -1,6 +1,6 @@ # LightCraft на русском -Выберите **Правка → Язык → Русский** или **Настройки → Общие → Язык**. Выбор применяется сразу+Выберите **Настройки → Общие → Язык → Русский**. Выбор применяется сразу и сохраняется для следующего запуска (`language: "ru"` в `ui.json`).  Русский каталог охватывает меню, сочетания клавиш, библиотеку, обработку, кадрирование, маски,diff --git a/docs/localization-vi.md b/docs/localization-vi.md--- a/docs/localization-vi.md+++ b/docs/localization-vi.md@@ -1,6 +1,6 @@ # LightCraft bằng tiếng Việt -Chọn **Chỉnh sửa → Ngôn ngữ → Tiếng Việt** hoặc **Cài đặt → Chung → Ngôn ngữ**.+Chọn **Cài đặt → Chung → Ngôn ngữ → Tiếng Việt**. Lựa chọn áp dụng ngay và được lưu cho lần mở sau (`language: "vi"` trong `ui.json`).  Các tag locale hệ thống sau cũng chọn tiếng Việt: `vi`, `vi-VN`, `vi_VN`, `vi_VN.UTF-8`.diff --git a/docs/localization-zh-hans.md b/docs/localization-zh-hans.md--- a/docs/localization-zh-hans.md+++ b/docs/localization-zh-hans.md@@ -2,13 +2,13 @@  LightCraft 的界面支持英语、简体中文、繁体中文(台湾)和日语。语言设置保存在 `ui.json` 的 `language` 字段中。 -- 通过「编辑 → 语言」或「设置 → 一般 → 语言」切换,选择会保留到下次启动。+- 通过「设置 → 一般 → 语言」切换,选择会保留到下次启动。 - 已翻译的范围与 `zh-hant.json` 键集完全一致(1246 条),两份目录的键与键序一一对应。 - 正文字重与半粗体都使用 SIL OFL 的 **Noto Sans CJK SC**(简体中文)与 BIZ UDPGothic(日语)。   这些字体不在本仓库,而在 [storytold/craft-fonts](https://github.com/storytold/craft-fonts);   使用可选的构建输入 `CRAFT_FONTS_DIR=../craft-fonts` 时(所有正式发布版)会内嵌进来。   不指定该输入时程序照常构建运行,但中文与日文文字没有字形,会显示为方框 ——-  语言菜单里四种语言仍然都在,选中「简体中文」也只会得到一屏方框。未内嵌字体时构建会打印一条+  「设置 → 一般 → 语言」里四种语言仍然都在,选中「简体中文」也只会得到一屏方框。未内嵌字体时构建会打印一条   `cargo::warning` 提示这一点。 - 翻译只发生在显示层:命令 ID、照片文件名、用户输入的元数据都不做改动。 - 尚未翻译的技术性错误、补充说明和发行说明仍以英文显示。diff --git a/docs/localization-zh-hant.md b/docs/localization-zh-hant.md--- a/docs/localization-zh-hant.md+++ b/docs/localization-zh-hant.md@@ -1,6 +1,6 @@ # LightCraft 繁體中文(台灣)介面 -從「編輯 → 語言 → 繁體中文(台灣)」切換,或開啟「設定 → 一般 → 語言」選擇。+從「設定 → 一般 → 語言 → 繁體中文(台灣)」切換。 macOS 的設定也可由「LightCraft → 設定…」或 `⌘,` 開啟。 語言立即生效,並儲存在應用程式設定的 `ui.json`,下次啟動會沿用。 英文、簡體中文、繁體中文與日文可隨時互相切換。diff --git a/docs/localization.md b/docs/localization.md--- a/docs/localization.md+++ b/docs/localization.md@@ -39,12 +39,12 @@ are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-h    ```     The fields are the BCP-47 code (also the settings-file value), the **endonym** shown in the-   Language menu, the ISO 15924 script (`Latn`, `Jpan`, `Hans`, `Hant`, `Kore`…), and the embedded+   Settings language row, the ISO 15924 script (`Latn`, `Jpan`, `Hans`, `Hant`, `Kore`…), and the embedded    catalog. `build.rs` picks up `<code>-formats.json` by name, so the macro and the format lookup    follow automatically. Add the corresponding `app.language.<name>` entry to    `LANGUAGE_COMMANDS` and `language_from_command` in `src/menus.rs`, and to the-   `language_commands_cover_every_language` test. Settings read the locale table; menus and-   the control channel share the language-command mapping.+   `language_commands_cover_every_language` test. Settings read the locale table; the control+   channel uses the language commands (they are in no menu). 3. **Make sure the glyphs exist.** The script drives which craft-fonts faces the UI installs: a    language whose script has no face in the build shows boxes (the test says so instead of failing).    Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never here; adiff --git a/docs/parity-checklist.md b/docs/parity-checklist.md--- a/docs/parity-checklist.md+++ b/docs/parity-checklist.md@@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-LOCALE: Edit no longer lists 18 languages, Settings ▸ General ▸ Language is the one place to choose; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6  The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good@@ -480,7 +480,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-BEHAV-TEXTFIELD | Text fields behave alike | P1 | 🟡 | `crates/ui-egui/src/widgets/text_field.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/right.rs` (`meta_field`), `crates/ui-egui/src/dialogs.rs`, `crates/ui-egui/src/panels/unnamed.rs` | one shared field: right-click Cut / Copy / Paste / Select All (no Paste in the browser), Esc gives back the text from before the edit, Return / leaving / Esc reported to the caller, optional select-on-focus; agents use `ui.clipboard` (MCP `clipboard`) and `ui.inspect` → `copied`. Used by Search Photos, slider values, the Info panel's metadata fields (multi-line captions), the one-field dialogs (New / Rename Album, New Smart Album, Rename / Merge Keywords, text prompts) and the unnamed faces' name field so far; the other text fields (most dialogs, import, settings, masking, keywords …) still use egui's own and have no menu | | LR-BEHAV-HEADLESS | Headless UI snapshots | P2 | ✅ | `apps/lightcraft-cli/src/main.rs`, `crates/ui-egui/src/headless.rs`, `docs/control-protocol.md` | `snapshot` rasterizes the UI and defaults photo development to the CPU before the first frame, without adapter discovery; scaled dimensions are validated before allocation, and UI zoom preserves the requested physical viewport; JSON-lines control scripts are supported | | LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/widgets/text_field.rs`, `crates/ui-egui/src/panels/grid.rs`, `crates/ui-egui/tests/it/accessibility.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control, value, range and step and take Increment / Decrement, buttons / icon buttons / dropdowns / section headers / sources their labels and state, text fields their caption, grid thumbnails file name, rating, flag and label. `tests/it/accessibility.rs` fails on an unnamed control, a slider without range or an untitled dialog in every view, Develop tool and 13 dialogs, and checks that Crop and Remove sliders are reached with Tab and that Tab walks the right panels top to bottom. Canvas tools by keyboard (`crates/ui-egui/src/canvas_keys.rs`, `crates/ui-egui/src/tests/canvas_keys.rs`): crop corners, edges and box, spot targets and sources, a New spot point (Return adds), radial / linear gradient handles and mask pins take the focus, are announced with their position and move with the arrows (Shift: further). Dialogs with one name field (New Album, New Smart Album, Save Metadata Preset, the renames) take the focus once when they open, so Tab and Shift+Tab reach their check box, Cancel and OK, and Return and Esc still confirm and cancel (`crates/ui-egui/src/tests/dialog_tab.rs`). Brush strokes and red-eye ellipses are pointer-only; not tried with a real screen reader ([audit](ui-parity.md#accessibility-audit)) |-| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese`, `cmd:app.language.french`, `cmd:app.language.spanish`, `cmd:app.language.german`, `cmd:app.language.russian`, `cmd:app.language.ukrainian`, `cmd:app.language.georgian`, `cmd:app.language.polish`, `cmd:app.language.czech`, `cmd:app.language.italian`, `cmd:app.language.swedish`, `cmd:app.language.finnish`, `cmd:app.language.indonesian`, `cmd:app.language.vietnamese` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) |+| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese`, `cmd:app.language.french`, `cmd:app.language.spanish`, `cmd:app.language.german`, `cmd:app.language.russian`, `cmd:app.language.ukrainian`, `cmd:app.language.georgian`, `cmd:app.language.polish`, `cmd:app.language.czech`, `cmd:app.language.italian`, `cmd:app.language.swedish`, `cmd:app.language.finnish`, `cmd:app.language.indonesian`, `cmd:app.language.vietnamese` | Settings > General > Language lists every language in the table and the active one is checked; the commands are in no menu, so Edit has no Language submenu (docs/localization.md) | | LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization.md`, `docs/localization-zh-hans.md`, `docs/localization-zh-hant.md`, `docs/localization-ja.md`, `docs/localization-pt-br.md`, `docs/localization-es.md`, `docs/localization-de.md`, `docs/localization-ru.md`, `docs/localization-ka.md`, `docs/localization-pl.md` | One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese, Brazilian Portuguese, Spanish, German, Russian, French, Ukrainian, Polish, Czech, Italian, Georgian, Swedish, Finnish, Indonesian and Vietnamese UI with a persisted language preference and craft-fonts regular/bold faces per script (a variable face, as for Arabic, is drawn heavier for bold from its one file); menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language registers the locale and its language command, plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the Ukrainian catalog covers every existing catalog key, command/control label and rule-editor label; the German catalog reports newly added gaps; the other catalogs fall back to English for messages they lack. Technical errors remain English; release notes are translated in German and Ukrainian; craft-fonts faces for other scripts (Thai, Arabic, …) are UI fallbacks in every language, for file, folder and album names (#736); Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds the Japanese, Thai and Georgian faces in WASM while loading the Simplified Chinese face from a separate font file. | | LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | | | LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights |@@ -799,6 +799,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove  | Date | Change | Summary | |---|---|---|+| 2026-10-27 | patch | LR-BEHAV-LOCALE: the Edit menu loses its Language submenu (18 rows); Settings ▸ General ▸ Language, which already lists every language, is the one place to choose; the `app.language.*` commands stay for agents; status counts unchanged | | 2026-10-25 | patch | LR-BEHAV-TITLEBAR: the top bar's title-bar gestures come from craft-window's `title_bar`: a double-click follows the system setting, and a press on a button, the search field or a panel floating over the bar never moves the window; status counts unchanged | | 2026-10-24 | minor | KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 (`keyword.nextSet`, `keyword.previousSet`) go to the next / previous keyword set; status counts unchanged (KEYC-METADATA stays 🟡) | | 2026-10-24 | minor | KEYC-METADATA: ⌘K / Ctrl+K (`panel.addKeywords`, Add Keywords) shows the Keywords panel, opens Keywording and puts the cursor in the Add keyword box; status counts unchanged (KEYC-METADATA stays 🟡) |diff --git a/docs/whats-new.md b/docs/whats-new.md--- a/docs/whats-new.md+++ b/docs/whats-new.md@@ -1,5 +1,10 @@ # What's new in LightCraft +## 2026-10-27++### The Edit menu loses its Language submenu+- Choose the interface language in Settings ▸ General ▸ Language, which already lists every language. Edit no longer carries 18 more rows for it, on any platform.+ ## 2026-10-24  ### Relative-date filters and smart albums follow the clockdiff --git a/xtask/src/parity.rs b/xtask/src/parity.rs--- a/xtask/src/parity.rs+++ b/xtask/src/parity.rs@@ -141,7 +141,7 @@ pub fn parse(md: &str) -> Doc { }  /// Ids in the menu tables (`("view.detail", "Detail", Some("D"), "View"),` lines): the language-/// commands live in their own table, which the Language menu builds from the i18n language list.+/// commands live in their own table, one per language in the i18n language list. pub fn ui_command_ids(menus_rs: &str) -> Vec<String> {     let mut ids = Vec::new();     for table in ["pub const UI_COMMANDS", "pub const LANGUAGE_COMMANDS"] {@@ -539,6 +539,6 @@ old         for id in ["view.detail", "app.language.english", "app.language.japanese", "app.language.simplifiedChinese", "app.language.portuguese"] {             assert!(ids.iter().any(|known| known == id), "{id} not found");         }-        assert_eq!(ids.iter().filter(|id| id.as_str() == "app.language.english").count(), 1, "one Language menu entry per language");+        assert_eq!(ids.iter().filter(|id| id.as_str() == "app.language.english").count(), 1, "one language command per language");     } }