mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-22 18:22:22 +00:00
#498: French (fr) interface translation
Problem: French, one of Premiere's interface languages and one of the 12 key languages in docs/gaps.md G14, had no catalog; #498 asked for it. Fix: a new fr.tsv catalog with every entry Spanish has (menus, panels, dialogs, Settings pages, the Effects / Effect Controls registry, the Keyboard Shortcuts dialog, status messages) plus the native macOS Settings item, written from the plain meaning of the English text with ordinary French editing vocabulary (infinitive commands, "vous", French sentence case and spacing). Language::Fr is persisted as `fr`; app.language.french joins Edit > Language (checkmark, native macOS menu), Settings > General offers Français, and System Language picks French for any fr-* locale. Like Japanese, tests require only the core menus, so later strings show in English until translated; a French entry without a Spanish one fails. A new test checks the bundled fonts cover every French letter (the glyph check is shared with Ukrainian). Docs: G14, localization-parity, ROADMAP language row, ui-egui README. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f346da650f
commit
73f9982c0c
12 files changed
+3672
-32
No files matched your search
@@ -9,6 +9,7 @@ iconography, images or other Adobe assets; open licences only; every asset attri
|
||||
|
||||
| Asset | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| `crates/ui-egui/src/i18n/fr.tsv` | FilmCraft contributors | Original work: clean-room French interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translations | MIT OR Apache-2.0 |
|
||||
| `crates/ui-egui/src/i18n/uk.tsv` | FilmCraft contributors | Original work: clean-room Ukrainian interface translations | MIT OR Apache-2.0 |
|
||||
| `assets/fonts/Inter-Regular.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ Premiere ships 10 interface languages. Measured from our catalogs (3,498 strings
|
||||
| Spanish | partial (whole interface) | 99.9% |
|
||||
| Hindi | none (no Devanagari shaping in the interface) | 0% |
|
||||
| Arabic | none (no RTL interface) | 0% |
|
||||
| French | none | 0% |
|
||||
| French | partial (whole interface; native review welcome) | ~100% |
|
||||
| Portuguese (Brazil) | menus only | 9.9% |
|
||||
| Indonesian | none | 0% |
|
||||
| Japanese | partial (whole interface) | 99.7% |
|
||||
|
||||
@@ -107,6 +107,8 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Sho
|
||||
filmcraft_ui_egui::i18n::Language::De
|
||||
} else if checked("app.language.russian") {
|
||||
filmcraft_ui_egui::i18n::Language::Ru
|
||||
} else if checked("app.language.french") {
|
||||
filmcraft_ui_egui::i18n::Language::Fr
|
||||
} else {
|
||||
filmcraft_ui_egui::i18n::Language::En
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ pub const PREFS_VERSION: u32 = 2;
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct GeneralPrefs {
|
||||
/// Interface language (persisted independently of projects): `en`, `ja`, `es`, `pt-br`, `uk`,
|
||||
/// `zh-cn`, `de`, or `system` (the default: the operating system's preferred language when the
|
||||
/// `zh-cn`, `de`, `ru`, `fr`, or `system` (the default: the operating system's preferred language when the
|
||||
/// interface has it, otherwise English).
|
||||
pub interface_language: String,
|
||||
/// "At Startup": `showHome` (FilmCraft: the demo project), `openMostRecent`, `emptyProject`.
|
||||
@@ -776,6 +776,7 @@ static CATEGORIES: &[Category] = &[
|
||||
("zh-cn", "简体中文"),
|
||||
("de", "Deutsch"),
|
||||
("ru", "Русский"),
|
||||
("fr", "Français"),
|
||||
]),
|
||||
true,
|
||||
),
|
||||
|
||||
@@ -29,13 +29,15 @@ fn interface_language_persists_and_rejects_hostile_values() {
|
||||
let mut s = Session { prefs_path: Some(path.clone()), ..Session::default() };
|
||||
set(&mut s, "general.interfaceLanguage", json!("es"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "es");
|
||||
for value in [json!("xx"), json!("es-MX"), json!("de-DE"), json!(null), json!(-1), json!({}), json!("x".repeat(4096))] {
|
||||
for value in [json!("xx"), json!("es-MX"), json!("de-DE"), json!("fr-FR"), json!(null), json!(-1), json!({}), json!("x".repeat(4096))] {
|
||||
assert!(s.execute("prefs.set", json!({"key": "general.interfaceLanguage", "value": value})).is_err());
|
||||
assert_eq!(s.prefs.general.interface_language, "es");
|
||||
}
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "es");
|
||||
set(&mut s, "general.interfaceLanguage", json!("de"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "de");
|
||||
set(&mut s, "general.interfaceLanguage", json!("fr"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "fr");
|
||||
// back to following the operating system (#218)
|
||||
set(&mut s, "general.interfaceLanguage", json!("system"));
|
||||
assert_eq!(Preferences::load(&path).general.interface_language, "system");
|
||||
|
||||
@@ -18,7 +18,7 @@ the committed span and undo. Set `FILMCRAFT_UI_SNAPSHOT_DIR` to render screensho
|
||||
## Localisation
|
||||
|
||||
Edit > Language offers English, Japanese, Spanish, Brazilian Portuguese, Ukrainian, Simplified
|
||||
Chinese and German. The language is stored in the engine's `general.interfaceLanguage` preference,
|
||||
Chinese, German, Russian and French. The language is stored in the engine's `general.interfaceLanguage` preference,
|
||||
restored on startup, and also available in Settings > General.
|
||||
Its default, System Language (`system`), follows the operating system: the first of the user's
|
||||
preferred languages that the interface has (the host supplies them through
|
||||
@@ -32,7 +32,7 @@ fills translated templates, and `i18n::t` translates names from registries. Plac
|
||||
(including user filenames containing braces) are inserted literally. Catalog translations are
|
||||
original work using ordinary language, without proprietary localisation resources.
|
||||
|
||||
Spanish and Japanese cover menus, panels, dialogs, settings and registry labels. Searches accept both the
|
||||
Spanish, Japanese, Simplified Chinese and French cover menus, panels, dialogs, settings and registry labels. Searches accept both the
|
||||
translated label and its English source, including Unicode capitals. Project content, command ids
|
||||
and preference values retain their original values. Engine errors, CLI and MCP messages remain
|
||||
English. Brazilian Portuguese and Ukrainian currently cover the core menus, German every menu; they
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
asset: fr.tsv
|
||||
title: French interface translation catalog
|
||||
author: FilmCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
license-file: LICENSE-MIT and LICENSE-APACHE
|
||||
added: 2026-10-10 by vangel76
|
||||
notes: Original English-to-French interface translations using clean-room NLE vocabulary.
|
||||
@@ -44,6 +44,7 @@ pub enum Language {
|
||||
ZhCn,
|
||||
De,
|
||||
Ru,
|
||||
Fr,
|
||||
}
|
||||
|
||||
static JAPANESE: OnceLock<Catalog> = OnceLock::new();
|
||||
@@ -53,9 +54,10 @@ static UKRAINIAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static CHINESE: OnceLock<Catalog> = OnceLock::new();
|
||||
static GERMAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static RUSSIAN: OnceLock<Catalog> = OnceLock::new();
|
||||
static FRENCH: OnceLock<Catalog> = OnceLock::new();
|
||||
|
||||
impl Language {
|
||||
pub const ALL: [Self; 8] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru];
|
||||
pub const ALL: [Self; 9] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
@@ -67,6 +69,7 @@ impl Language {
|
||||
Self::ZhCn => "简体中文",
|
||||
Self::De => "Deutsch",
|
||||
Self::Ru => "Русский",
|
||||
Self::Fr => "Français",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +83,7 @@ impl Language {
|
||||
"zh-cn" => Some(Self::ZhCn),
|
||||
"de" => Some(Self::De),
|
||||
"ru" => Some(Self::Ru),
|
||||
"fr" => Some(Self::Fr),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -87,11 +91,12 @@ impl Language {
|
||||
/// Interface Language ▸ System Language (#218): the first of the user's preferred languages
|
||||
/// (BCP 47 or POSIX locale tags such as `es-419`, `pt_BR.UTF-8`, most preferred first) that the
|
||||
/// interface has, else English. Any Portuguese gets the Brazilian catalog, the only one there is;
|
||||
/// any German (`de-AT`, `de_CH.UTF-8`…) gets the German one. Chinese gets the Simplified catalog
|
||||
/// any German (`de-AT`, `de_CH.UTF-8`…) gets the German one and any French (`fr-CA`, `fr_BE.UTF-8`…)
|
||||
/// the French one. Chinese gets the Simplified catalog
|
||||
/// only for Simplified locales (`zh`, `zh-CN`, `zh-SG`, `zh-Hans…`); Traditional ones (`zh-TW`,
|
||||
/// `zh-HK`, `zh-MO`, `zh-Hant…`) are skipped.
|
||||
pub fn from_locales(tags: &[String]) -> Self {
|
||||
const PRIMARY: [(&str, Language); 7] = [
|
||||
const PRIMARY: [(&str, Language); 8] = [
|
||||
("en", Language::En),
|
||||
("ja", Language::Ja),
|
||||
("es", Language::Es),
|
||||
@@ -99,6 +104,7 @@ impl Language {
|
||||
("uk", Language::Uk),
|
||||
("de", Language::De),
|
||||
("ru", Language::Ru),
|
||||
("fr", Language::Fr),
|
||||
];
|
||||
tags.iter()
|
||||
.find_map(|tag| {
|
||||
@@ -127,6 +133,7 @@ impl Language {
|
||||
Self::ZhCn => "zh-cn",
|
||||
Self::De => "de",
|
||||
Self::Ru => "ru",
|
||||
Self::Fr => "fr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +148,7 @@ impl Language {
|
||||
Self::ZhCn => Some(CHINESE.get_or_init(|| Catalog::parse(include_str!("zh-cn.tsv")))),
|
||||
Self::De => Some(GERMAN.get_or_init(|| Catalog::parse(include_str!("de.tsv")))),
|
||||
Self::Ru => Some(RUSSIAN.get_or_init(|| Catalog::parse(include_str!("ru.tsv")))),
|
||||
Self::Fr => Some(FRENCH.get_or_init(|| Catalog::parse(include_str!("fr.tsv")))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +318,7 @@ mod tests {
|
||||
("zh-cn", include_str!("zh-cn.tsv")),
|
||||
("de", include_str!("de.tsv")),
|
||||
("ru", include_str!("ru.tsv")),
|
||||
("fr", include_str!("fr.tsv")),
|
||||
] {
|
||||
let (entries, errors) = catalog::parse_entries(text);
|
||||
assert!(errors.is_empty(), "{code}: {errors:?}");
|
||||
@@ -350,6 +359,13 @@ mod tests {
|
||||
assert_eq!(Language::Ru.tr("File"), "Файл");
|
||||
assert_eq!(Language::Ru.tr("мой клип.mp4"), "мой клип.mp4");
|
||||
assert_eq!(Language::Ru.tr("An untranslated label"), "An untranslated label");
|
||||
assert_eq!(Language::Fr.name(), "Français");
|
||||
assert_eq!(Language::Fr.tr("File"), "Fichier");
|
||||
assert_eq!(Language::Fr.tr("Save As…"), "Enregistrer sous…");
|
||||
assert_eq!(Language::Fr.tr("Montage d'été.mp4"), "Montage d'été.mp4");
|
||||
assert_eq!(Language::Fr.tr("An untranslated label"), "An untranslated label");
|
||||
assert_eq!(Language::parse("fr"), Some(Language::Fr));
|
||||
assert_eq!(Language::parse("fr-FR"), None, "the preference stores the bare code");
|
||||
for l in Language::ALL {
|
||||
assert_eq!(Language::parse(l.code()), Some(l));
|
||||
let json = serde_json::to_string(&l).unwrap();
|
||||
@@ -801,6 +817,23 @@ mod tests {
|
||||
let mut restarted = filmcraft_engine::Session::default();
|
||||
restarted.prefs = serde_json::from_str(&prefs).unwrap();
|
||||
assert_eq!(crate::FilmcraftApp::new(restarted).ui.language, Language::De);
|
||||
let result = crate::menus::invoke(&mut app, &ctx, "app.language.french", serde_json::json!({})).unwrap();
|
||||
assert_eq!(result, serde_json::json!("fr"));
|
||||
assert_eq!((app.ui.language, current()), (Language::Fr, Language::Fr));
|
||||
assert_eq!(app.session.prefs.general.interface_language, "fr");
|
||||
for it in crate::menus::menu_items(&app).iter().filter(|it| it.id.starts_with("app.language.")) {
|
||||
assert_eq!(it.checked, Some(it.id == "app.language.french"), "{}", it.id);
|
||||
}
|
||||
let items = crate::menus::menu_items(&app);
|
||||
let label = |id: &str| items.iter().find(|it| it.id == id).map(|it| it.label.as_str());
|
||||
assert_eq!(label("file.saveAs"), Some("Enregistrer sous…"));
|
||||
assert_eq!(label("app.language.german"), Some("Deutsch"));
|
||||
let saved = serde_json::to_string(&app.ui).unwrap();
|
||||
assert!(saved.contains("\"language\":\"fr\""), "{saved}");
|
||||
let prefs = serde_json::to_string(&app.session.prefs).unwrap();
|
||||
let mut restarted = filmcraft_engine::Session::default();
|
||||
restarted.prefs = serde_json::from_str(&prefs).unwrap();
|
||||
assert_eq!(crate::FilmcraftApp::new(restarted).ui.language, Language::Fr);
|
||||
crate::menus::invoke(&mut app, &ctx, "app.language.english", serde_json::json!({})).unwrap();
|
||||
assert_eq!(app.ui.language, Language::En);
|
||||
set_current(Language::En);
|
||||
@@ -829,11 +862,15 @@ mod tests {
|
||||
for tag in ["de", "DE", "de-DE", "de-AT", "de-CH", "de-LI", "de-LU", "de_DE.UTF-8", "de_AT.UTF-8@euro", "de_CH"] {
|
||||
assert_eq!(l(&[tag]), Language::De, "{tag}");
|
||||
}
|
||||
assert_eq!(l(&["fr-FR", "it", "es-MX", "ja"]), Language::Es, "the first one the interface has");
|
||||
assert_eq!(l(&["fr-CH", "de-CH", "en"]), Language::De);
|
||||
for tag in ["fr", "FR", "fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr_FR.UTF-8", "fr_BE.UTF-8@euro", "fr_CA"] {
|
||||
assert_eq!(l(&[tag]), Language::Fr, "{tag}");
|
||||
}
|
||||
assert_eq!(l(&["it-IT", "nl", "es-MX", "ja"]), Language::Es, "the first one the interface has");
|
||||
assert_eq!(l(&["it-CH", "de-CH", "fr-CH", "en"]), Language::De);
|
||||
assert_eq!(l(&["it-CH", "fr-CH", "de-CH", "en"]), Language::Fr);
|
||||
assert_eq!(l(&["en-GB", "es"]), Language::En);
|
||||
assert_eq!(l(&["en-US", "de"]), Language::En);
|
||||
for none in [&[][..], &["fr"], &["C"], &["POSIX"], &[""], &["e"], &["esp"], &["-es"], &["deu"], &["d"], &["-de"]] {
|
||||
for none in [&[][..], &["it"], &["C"], &["POSIX"], &[""], &["e"], &["esp"], &["-es"], &["deu"], &["d"], &["-de"], &["fra"], &["f"], &["-fr"]] {
|
||||
assert_eq!(l(none), Language::En, "{none:?}");
|
||||
}
|
||||
assert_eq!(l(&[&"x".repeat(1 << 20), "es"]), Language::Es);
|
||||
@@ -856,7 +893,7 @@ mod tests {
|
||||
let count = asked.clone();
|
||||
app.hooks.system_languages = Some(Box::new(move || {
|
||||
count.set(count.get() + 1);
|
||||
vec!["fr-FR".into(), "es-ES".into()]
|
||||
vec!["it-IT".into(), "es-ES".into()]
|
||||
}));
|
||||
app.apply_prefs(&ctx);
|
||||
assert_eq!(app.ui.language, Language::Es);
|
||||
@@ -915,6 +952,36 @@ mod tests {
|
||||
assert_eq!(Language::De.tr("Settings"), "Voreinstellungen");
|
||||
}
|
||||
|
||||
/// French covers the whole interface (the Spanish catalog's entries and the native macOS
|
||||
/// Settings menu), but, like Japanese, the tests require only the core menus, so strings added
|
||||
/// later show in English until they are translated. Every entry must still have a Spanish
|
||||
/// counterpart: a renamed or removed source would otherwise leave a dead French entry.
|
||||
#[test]
|
||||
fn french_covers_the_menus_and_has_no_dead_entries() {
|
||||
let (entries, errors) = catalog::parse_entries(include_str!("fr.tsv"));
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
for (ctx, en, _) in &entries {
|
||||
assert!(ctx.is_empty() && (Language::Es.has(en) || en == "Settings"), "French entry without a Spanish one: {ctx:?} {en:?}");
|
||||
}
|
||||
let (portuguese, _) = catalog::parse_entries(include_str!("pt-br.tsv"));
|
||||
for (_, en, _) in portuguese {
|
||||
assert!(Language::Fr.has(&en), "missing French menu label: {en}");
|
||||
}
|
||||
for en in crate::menus::MENUS.into_iter().chain(["Audio", "Settings"]) {
|
||||
assert!(Language::Fr.has(en), "untranslated French menu: {en}");
|
||||
}
|
||||
// beyond the menus: panels, effects and their parameters, settings, status messages
|
||||
for (en, fr) in
|
||||
[("Settings", "Préférences"), ("Gaussian Blur", "Flou gaussien"), ("Audio Track Mixer", "Mixeur de pistes audio"), ("Sequence", "Séquence")]
|
||||
{
|
||||
assert_eq!(Language::Fr.tr(en), fr);
|
||||
}
|
||||
set_current(Language::Fr);
|
||||
assert!(matches_query("Gaussian Blur", "flou"));
|
||||
assert!(matches_query("Gaussian Blur", "gaussian"));
|
||||
set_current(Language::En);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn russian_entries_cover_the_original_menu_catalog() {
|
||||
let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
|
||||
@@ -933,22 +1000,14 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_renders_with_bundled_fonts() {
|
||||
/// Every non-ASCII character of `text` renders with the bundled interface fonts, in every family.
|
||||
fn assert_bundled_fonts_cover(text: impl Iterator<Item = char>) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install(&ctx, &crate::theme::Tokens::for_kind(crate::theme::ThemeKind::default()));
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
output.textures_delta.clear();
|
||||
let (entries, _) = catalog::parse_entries(include_str!("uk.tsv"));
|
||||
let chars: std::collections::BTreeSet<_> = text.filter(|ch| !ch.is_ascii()).collect();
|
||||
ctx.fonts_mut(|fonts| {
|
||||
// Include the entire alphabet, even letters not yet used by a translated menu label.
|
||||
let alphabet = "АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя";
|
||||
let chars: std::collections::BTreeSet<_> = alphabet
|
||||
.chars()
|
||||
.chain(Language::Uk.name().chars())
|
||||
.chain(entries.iter().flat_map(|(_, _, uk)| uk.chars()))
|
||||
.filter(|ch| !ch.is_ascii())
|
||||
.collect();
|
||||
for family in crate::theme::font_families() {
|
||||
let font = egui::FontId::new(13.0, family);
|
||||
// has_glyph compares font faces, giving false negatives when Inter also supplies
|
||||
@@ -966,6 +1025,24 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_renders_with_bundled_fonts() {
|
||||
let (entries, _) = catalog::parse_entries(include_str!("uk.tsv"));
|
||||
// Include the entire alphabet, even letters not yet used by a translated menu label.
|
||||
let alphabet = "АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя";
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Uk.name().chars()).chain(entries.iter().flat_map(|(_, _, uk)| uk.chars())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn french_renders_with_bundled_fonts() {
|
||||
let (entries, _) = catalog::parse_entries(include_str!("fr.tsv"));
|
||||
// every French letter and quotation mark, even ones no entry uses yet, and every letter the
|
||||
// entries use (symbols such as ✔ come from the English sources)
|
||||
let alphabet = "ÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸàâæçéèêëîïôœùûüÿ«»’";
|
||||
let letters = entries.iter().flat_map(|(_, _, fr)| fr.chars()).filter(|ch| ch.is_alphabetic());
|
||||
assert_bundled_fonts_cover(alphabet.chars().chain(Language::Fr.name().chars()).chain(letters));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_needs_an_installed_font() {
|
||||
let mut app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
|
||||
|
||||
@@ -30,6 +30,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
uic!("app.language.chinese", "简体中文", ["Edit", "Language"], None),
|
||||
uic!("app.language.german", "Deutsch", ["Edit", "Language"], None),
|
||||
uic!("app.language.russian", "Русский", ["Edit", "Language"], None),
|
||||
uic!("app.language.french", "Français", ["Edit", "Language"], None),
|
||||
uic!("source.playback.toggle", "Source Play/Stop", [], None),
|
||||
uic!("source.playback.play", "Play Source", [], None),
|
||||
uic!("source.playback.stop", "Stop Source", [], None),
|
||||
@@ -184,6 +185,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
|
||||
| "app.language.chinese"
|
||||
| "app.language.german"
|
||||
| "app.language.russian"
|
||||
| "app.language.french"
|
||||
) {
|
||||
// Japanese needs the craft-fonts (built with CRAFT_FONTS_DIR) or a font installed on the system
|
||||
if id == "app.language.japanese" && !crate::i18n::install_japanese_font(ctx) {
|
||||
@@ -201,6 +203,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
|
||||
"app.language.chinese" => crate::i18n::Language::ZhCn,
|
||||
"app.language.german" => crate::i18n::Language::De,
|
||||
"app.language.russian" => crate::i18n::Language::Ru,
|
||||
"app.language.french" => crate::i18n::Language::Fr,
|
||||
_ => crate::i18n::Language::En,
|
||||
};
|
||||
// The preference is updated in memory before it is written, so a failed write (read-only
|
||||
@@ -625,6 +628,7 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
|
||||
"app.language.chinese" => it.checked = Some(app.ui.language == crate::i18n::Language::ZhCn),
|
||||
"app.language.german" => it.checked = Some(app.ui.language == crate::i18n::Language::De),
|
||||
"app.language.russian" => it.checked = Some(app.ui.language == crate::i18n::Language::Ru),
|
||||
"app.language.french" => it.checked = Some(app.ui.language == crate::i18n::Language::Fr),
|
||||
_ => {}
|
||||
}
|
||||
if it.id.starts_with("view.") {
|
||||
|
||||
+3
-3
@@ -190,10 +190,10 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
|
||||
## G14. Localization: 8 of the 12 key languages missing
|
||||
|
||||
- **Have:** Spanish, Japanese and Simplified Chinese catalogs cover ~99.6–99.9% of the ~3,500
|
||||
cataloged strings; Portuguese (Brazil) and Ukrainian ~10% (menus).
|
||||
- **Have:** Spanish, Japanese, Simplified Chinese and French (#498) catalogs cover ~99.6–100% of
|
||||
the ~3,500 cataloged strings; Portuguese (Brazil) and Ukrainian ~10% (menus).
|
||||
- **Missing:** Hindi, Arabic (no RTL interface layout; Arabic titles reported broken, #395),
|
||||
French (#498), Indonesian, German (#490), Korean (#508; needs Hangul fonts in craft-fonts),
|
||||
Indonesian, German beyond menus (#490), Korean (#508; needs Hangul fonts in craft-fonts),
|
||||
Vietnamese, Portuguese beyond menus; Premiere also ships Italian and Russian. Engine errors,
|
||||
CLI and MCP stay English. No language has a recorded native-speaker review.
|
||||
- **Estimate:** 150–230 h. Doc: [localization-parity.md](localization-parity.md).
|
||||
|
||||
@@ -10,14 +10,15 @@ Portuguese (Brazil), Russian, Simplified Chinese.
|
||||
|
||||
Edit ▸ Language (also Settings ▸ General, `general.interfaceLanguage`) switches the interface and
|
||||
persists across restarts; System Language (the default) follows the OS's or browser's preferred
|
||||
languages and falls back to English. Commands `app.language.<english|japanese|spanish|portuguese|ukrainian|chinese>`
|
||||
languages and falls back to English. Commands `app.language.<english|japanese|spanish|portuguese|ukrainian|chinese|german|russian|french>`
|
||||
are reachable over the control channel. One catalog per language,
|
||||
`crates/ui-egui/src/i18n/<code>.tsv` (format in the header of `es.tsv`); UI code wraps strings in
|
||||
`tl!("…")` / `tlf!("…{name}", name)`, and registry names (effects and their parameters, settings,
|
||||
commands, panels, workspaces, presets) are translated where they are drawn. Searches match the
|
||||
displayed labels as well as English sources. Tests fail when a `tl!` literal, a menu label or a
|
||||
registry name has no Spanish entry; `zh-cn.tsv` must also cover every `tl!` literal and menu
|
||||
label; `ja.tsv` has the same entries but is not yet required by tests. Engine error messages, the
|
||||
label; `ja.tsv` and `fr.tsv` have the same entries but are not yet required by tests (French must
|
||||
cover the core menus and keep no entry without a Spanish one). Engine error messages, the
|
||||
control channel, the CLI and MCP stay English.
|
||||
|
||||
Fonts come from [craft-fonts](https://github.com/storytold/craft-fonts) (`CRAFT_FONTS_DIR`, all
|
||||
@@ -41,7 +42,7 @@ whole interface, one PR) at ~4–8 h per language for the catalog plus tests.
|
||||
| Spanish | es | 3,496 (99.9%) | as above | Latin | not recorded | partial | 4–8 h + review |
|
||||
| Hindi | hi | 0 (0%) | — | **no Devanagari shaping in the interface** | — | none | 30–50 h (shaping in UI + catalog + fonts) |
|
||||
| Arabic | ar | 0 (0%) | — | **no RTL layout, no shaping in the interface**; titles broken (#395) | — | none | 40–70 h |
|
||||
| French | fr | 0 (0%) | — | Latin | — | none (requested #498) | 6–10 h + review |
|
||||
| French | fr | 3,526 (same entries as Spanish, plus the macOS Settings item) | as Spanish | Latin (bundled fonts) | not recorded; welcome | partial (#498) | review |
|
||||
| Portuguese (Brazil) | pt-br | 348 (9.9%) | menus only | Latin | not recorded | menus only | 5–9 h + review |
|
||||
| Indonesian | id | 0 (0%) | — | Latin | — | none | 6–10 h + review |
|
||||
| Japanese | ja | 3,487 (99.7%) | as zh-cn; system-font kanji may pick the Chinese fallback (#547) | CJK fonts, vertical title text | not recorded | partial | 4–8 h + review |
|
||||
@@ -52,9 +53,9 @@ whole interface, one PR) at ~4–8 h per language for the catalog plus tests.
|
||||
|
||||
Premiere also ships Italian and Russian, which we don't (6–10 h each).
|
||||
|
||||
**Localization dimension:** 4 of the 12 key languages near-complete (English, Spanish, Japanese,
|
||||
Simplified Chinese), 1 menus-only, 7 none; against Premiere's 10 languages we cover 4 well and 1
|
||||
partly. **~35%, 150–230 h** for all twelve to `full` plus Italian and Russian, of which the script
|
||||
**Localization dimension:** 5 of the 12 key languages near-complete (English, Spanish, Japanese,
|
||||
Simplified Chinese, French), 1 menus-only, 6 none; against Premiere's 10 languages we cover 5 well
|
||||
and 1 partly. **~35%, 150–230 h** for all twelve to `full` plus Italian and Russian, of which the script
|
||||
work (RTL and complex shaping in the egui interface) is the largest and least parallel part.
|
||||
Every language needs native-speaker review (human).
|
||||
|
||||
|
||||
Reference in new issue
Block a user