mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-02 18:09:21 +00:00
Merge pull request #804 from vangel76/gap-french-localization
#498: French (fr) interface translation
This commit is contained in:
@@ -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
@@ -195,10 +195,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