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 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<String>, 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 ▸ <category> (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<String>, 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);

View File

@ -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

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
/// 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

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"];
/// 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> {
let mut v = menu_items_for(&app.session);
for it in &mut v {