#1145: macOS app menu follows the interface language
About FilmCraft and Join the ArtCraft Discord… in the macOS app menu take the translated labels of the same Help menu commands and are updated on a language switch. Hide, Hide Others and Quit get their text from the catalogs instead of muda's English defaults and keep their native actions. menus::NATIVE_APP_MENU lists these native-only labels together with the Settings submenu, so the app and the tests use the same keys. German rows for the three new keys; "Quit FilmCraft" is already translated in es, ja, zh-cn and fr (the app.quit shortcut), the other languages show English until they have translations. The German menu test accepts and requires the native app menu labels. Closes #1145 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0800a8b0aa
commit
4ebcf3aa67
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue