diff --git a/apps/filmcraft/src/native_menu.rs b/apps/filmcraft/src/native_menu.rs index 61241a8a..1abd3451 100644 --- a/apps/filmcraft/src/native_menu.rs +++ b/apps/filmcraft/src/native_menu.rs @@ -4,7 +4,7 @@ use std::sync::mpsc::{Receiver, channel}; use filmcraft_ui_egui::FilmcraftApp; -use filmcraft_ui_egui::menus::{MENUS, MenuItem as Item, menu_items}; +use filmcraft_ui_egui::menus::{MENUS, MenuItem as Item, NATIVE_APP_MENU, menu_items}; use muda::accelerator::Accelerator; use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}; @@ -28,25 +28,39 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver, Sho let mut titles: Vec<(String, Submenu)> = Vec::new(); let bar = Menu::new(); let app_menu = Submenu::new("FilmCraft", true); + let [settings_label, hide_label, hide_others_label, quit_label] = NATIVE_APP_MENU; // FilmCraft ▸ Settings ▸ (Premiere's app-menu layout; General is Cmd+,) - let settings = Submenu::new(app.ui.language.tr("Settings"), true); - titles.push(("Settings".into(), settings.clone())); + let settings = Submenu::new(app.ui.language.tr(settings_label), true); + titles.push((settings_label.into(), settings.clone())); for it in items.iter().filter(|i| i.id.starts_with("app.settings.")) { let label = it.label.trim_end_matches('…').to_string(); let mi = MenuItem::with_id(it.id.clone(), label, true, it.shortcut.as_deref().and_then(accel)); native.push((it.id.clone(), mi.clone())); let _ = settings.append(&mi); } + // About and Discord repeat Help menu commands: same translated labels, updated with them + let label = |id: &str, english: &str| items.iter().find(|i| i.id == id).map_or_else(|| english.to_string(), |i| i.label.clone()); + let about = MenuItem::with_id("app.about", label("app.about", "About FilmCraft"), true, None); + let discord = MenuItem::with_id("help.discord", label("help.discord", "Join the ArtCraft Discord…"), true, None); + native.push(("app.about".into(), about.clone())); + native.push(("help.discord".into(), discord.clone())); + // the predefined items keep their native actions; our text lets them follow the language + let predefined = [ + (hide_label, PredefinedMenuItem::hide(Some(app.ui.language.tr(hide_label)))), + (hide_others_label, PredefinedMenuItem::hide_others(Some(app.ui.language.tr(hide_others_label)))), + (quit_label, PredefinedMenuItem::quit(Some(app.ui.language.tr(quit_label)))), + ]; + let [(_, hide), (_, hide_others), (_, quit)] = &predefined; let _ = app_menu.append_items(&[ - &MenuItem::with_id("app.about", "About FilmCraft", true, None), - &MenuItem::with_id("help.discord", "Join the ArtCraft Discord…", true, None), + &about, + &discord, &PredefinedMenuItem::separator(), &settings, &PredefinedMenuItem::separator(), - &PredefinedMenuItem::hide(None), - &PredefinedMenuItem::hide_others(None), + hide, + hide_others, &PredefinedMenuItem::separator(), - &PredefinedMenuItem::quit(None), + quit, ]); let _ = bar.append(&app_menu); for top in MENUS { @@ -115,6 +129,9 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver, Sho for (title, submenu) in &titles { submenu.set_text(language.tr(title)); } + for (label, item) in &predefined { + item.set_text(language.tr(label)); + } for (id, mi) in &native { if let Some(it) = items.iter().find(|i| &i.id == id) { mi.set_text(&it.label); diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 51a33dd5..11c51af7 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -6,7 +6,7 @@ # apps, text addresses the user informally ("du") and commands use the infinitive ("Projekt # öffnen…"). Product, format and feature names (FilmCraft, ArtCraft, Lumetri, Essential Graphics, # EDL, AAF…) keep their spelling. This catalog covers every menu, including the native macOS -# Settings menu; anything else is shown in English. +# app menu; anything else is shown in English. # # Terms: track = Spur, bin = Ablage, marker = Marker, playhead = Abspielposition, In/Out = In-/Out- # Punkt, transition = Übergang, frame hold = Standbild, panel = Bedienfeld, workspace = @@ -180,6 +180,8 @@ Guide Templates Hilfslinienvorlagen Help Hilfe Hide All Caption Tracks Alle Untertitelspuren ausblenden + Hide FilmCraft FilmCraft ausblenden + Hide Others Andere ausblenden High Quality Playback Wiedergabe in hoher Qualität History Verlauf Import From Importieren aus @@ -269,6 +271,7 @@ Properties Eigenschaften Proxy Proxy Purple Lila + Quit FilmCraft FilmCraft beenden Reconnect Full Resolution Media… Medien in voller Auflösung neu verknüpfen… Recover Unsaved Changes… Nicht gespeicherte Änderungen wiederherstellen… Rectangle Rechteck diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 7225b5c9..574e9442 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -931,7 +931,7 @@ mod tests { } /// German covers at least the core menus (the Brazilian Portuguese entries, the top-level menus - /// and the native macOS Settings menu); every entry must still be a menu label. + /// and the native macOS app menu); every entry must still be a menu label. #[test] fn german_entries_cover_the_original_menu_catalog() { let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default()); @@ -939,17 +939,18 @@ mod tests { let known = |text: &str| crate::menus::MENUS.contains(&text) || items.iter().any(|it| it.label == text || it.path.iter().any(|p| p == text)); let (entries, _) = catalog::parse_entries(include_str!("de.tsv")); for (_, en, _) in entries { - assert!(known(&en) || en == "Settings", "not a menu label: {en}"); + assert!(known(&en) || crate::menus::NATIVE_APP_MENU.contains(&en.as_str()), "not a menu label: {en}"); } let (portuguese, _) = catalog::parse_entries(include_str!("pt-br.tsv")); for (_, en, _) in portuguese { assert!(Language::De.has(&en), "missing German menu label: {en}"); } // "Clip" and "Audio" are the same word in German, so check for an entry, not a change - for en in crate::menus::MENUS.into_iter().chain(["Audio", "Settings"]) { + for en in crate::menus::MENUS.into_iter().chain(["Audio"]).chain(crate::menus::NATIVE_APP_MENU) { assert!(Language::De.has(en), "untranslated German menu: {en}"); } assert_eq!(Language::De.tr("Settings"), "Voreinstellungen"); + assert_eq!(Language::De.tr("Quit FilmCraft"), "FilmCraft beenden"); } /// French covers the whole interface (the Spanish catalog's entries and the native macOS diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index f4871150..ceae2234 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -622,6 +622,11 @@ pub struct MenuItem { pub const MENUS: [&str; 9] = ["File", "Edit", "Clip", "Sequence", "Markers", "Graphics and Titles", "View", "Window", "Help"]; +/// Labels only the native macOS app menu shows, with no command in the menu tree behind them: the +/// Settings submenu and the predefined Hide, Hide Others and Quit items. They are translated through +/// the catalogs like menu labels (`apps/filmcraft/src/native_menu.rs`). +pub const NATIVE_APP_MENU: [&str; 4] = ["Settings", "Hide FilmCraft", "Hide Others", "Quit FilmCraft"]; + pub fn menu_items(app: &FilmcraftApp) -> Vec { let mut v = menu_items_for(&app.session); for it in &mut v {