Merge pull request #1151 from keller-lukas/fix/1145-macos-app-menu-i18n

macOS: app menu follows the interface language (#1145)
This commit is contained in:
Brandon Thomas 2026-10-11 18:52:58 -04:00
commit 9dc3421bb1
4 changed files with 38 additions and 12 deletions

View File

@ -4,7 +4,7 @@
use std::sync::mpsc::{Receiver, channel}; use std::sync::mpsc::{Receiver, channel};
use filmcraft_ui_egui::FilmcraftApp; 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::accelerator::Accelerator;
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}; use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
@ -28,25 +28,39 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Sho
let mut titles: Vec<(String, Submenu)> = Vec::new(); let mut titles: Vec<(String, Submenu)> = Vec::new();
let bar = Menu::new(); let bar = Menu::new();
let app_menu = Submenu::new("FilmCraft", true); let app_menu = Submenu::new("FilmCraft", true);
let [settings_label, hide_label, hide_others_label, quit_label] = NATIVE_APP_MENU;
// FilmCraft ▸ Settings ▸ <category> (Premiere's app-menu layout; General is Cmd+,) // FilmCraft ▸ Settings ▸ <category> (Premiere's app-menu layout; General is Cmd+,)
let settings = Submenu::new(app.ui.language.tr("Settings"), true); let settings = Submenu::new(app.ui.language.tr(settings_label), true);
titles.push(("Settings".into(), settings.clone())); titles.push((settings_label.into(), settings.clone()));
for it in items.iter().filter(|i| i.id.starts_with("app.settings.")) { for it in items.iter().filter(|i| i.id.starts_with("app.settings.")) {
let label = it.label.trim_end_matches('…').to_string(); 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)); let mi = MenuItem::with_id(it.id.clone(), label, true, it.shortcut.as_deref().and_then(accel));
native.push((it.id.clone(), mi.clone())); native.push((it.id.clone(), mi.clone()));
let _ = settings.append(&mi); 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(&[ let _ = app_menu.append_items(&[
&MenuItem::with_id("app.about", "About FilmCraft", true, None), &about,
&MenuItem::with_id("help.discord", "Join the ArtCraft Discord…", true, None), &discord,
&PredefinedMenuItem::separator(), &PredefinedMenuItem::separator(),
&settings, &settings,
&PredefinedMenuItem::separator(), &PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None), hide,
&PredefinedMenuItem::hide_others(None), hide_others,
&PredefinedMenuItem::separator(), &PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None), quit,
]); ]);
let _ = bar.append(&app_menu); let _ = bar.append(&app_menu);
for top in MENUS { for top in MENUS {
@ -115,6 +129,9 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Sho
for (title, submenu) in &titles { for (title, submenu) in &titles {
submenu.set_text(language.tr(title)); submenu.set_text(language.tr(title));
} }
for (label, item) in &predefined {
item.set_text(language.tr(label));
}
for (id, mi) in &native { for (id, mi) in &native {
if let Some(it) = items.iter().find(|i| &i.id == id) { if let Some(it) = items.iter().find(|i| &i.id == id) {
mi.set_text(&it.label); mi.set_text(&it.label);

View File

@ -6,7 +6,7 @@
# apps, text addresses the user informally ("du") and commands use the infinitive ("Projekt # apps, text addresses the user informally ("du") and commands use the infinitive ("Projekt
# öffnen…"). Product, format and feature names (FilmCraft, ArtCraft, Lumetri, Essential Graphics, # ö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 # 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- # Terms: track = Spur, bin = Ablage, marker = Marker, playhead = Abspielposition, In/Out = In-/Out-
# Punkt, transition = Übergang, frame hold = Standbild, panel = Bedienfeld, workspace = # Punkt, transition = Übergang, frame hold = Standbild, panel = Bedienfeld, workspace =
@ -180,6 +180,8 @@
Guide Templates Hilfslinienvorlagen Guide Templates Hilfslinienvorlagen
Help Hilfe Help Hilfe
Hide All Caption Tracks Alle Untertitelspuren ausblenden Hide All Caption Tracks Alle Untertitelspuren ausblenden
Hide FilmCraft FilmCraft ausblenden
Hide Others Andere ausblenden
High Quality Playback Wiedergabe in hoher Qualität High Quality Playback Wiedergabe in hoher Qualität
History Verlauf History Verlauf
Import From Importieren aus Import From Importieren aus
@ -269,6 +271,7 @@
Properties Eigenschaften Properties Eigenschaften
Proxy Proxy Proxy Proxy
Purple Lila Purple Lila
Quit FilmCraft FilmCraft beenden
Reconnect Full Resolution Media… Medien in voller Auflösung neu verknüpfen… Reconnect Full Resolution Media… Medien in voller Auflösung neu verknüpfen…
Recover Unsaved Changes… Nicht gespeicherte Änderungen wiederherstellen… Recover Unsaved Changes… Nicht gespeicherte Änderungen wiederherstellen…
Rectangle Rechteck Rectangle Rechteck

Can't render this file because it contains an unexpected character in line 6 and column 45.

View File

@ -931,7 +931,7 @@ mod tests {
} }
/// German covers at least the core menus (the Brazilian Portuguese entries, the top-level menus /// 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] #[test]
fn german_entries_cover_the_original_menu_catalog() { fn german_entries_cover_the_original_menu_catalog() {
let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default()); 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 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")); let (entries, _) = catalog::parse_entries(include_str!("de.tsv"));
for (_, en, _) in entries { 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")); let (portuguese, _) = catalog::parse_entries(include_str!("pt-br.tsv"));
for (_, en, _) in portuguese { for (_, en, _) in portuguese {
assert!(Language::De.has(&en), "missing German menu label: {en}"); 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 // "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!(Language::De.has(en), "untranslated German menu: {en}");
} }
assert_eq!(Language::De.tr("Settings"), "Voreinstellungen"); 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 /// French covers the whole interface (the Spanish catalog's entries and the native macOS

View File

@ -622,6 +622,11 @@ pub struct MenuItem {
pub const MENUS: [&str; 9] = ["File", "Edit", "Clip", "Sequence", "Markers", "Graphics and Titles", "View", "Window", "Help"]; 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<MenuItem> { pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
let mut v = menu_items_for(&app.session); let mut v = menu_items_for(&app.session);
for it in &mut v { for it in &mut v {