i18n: add French (fr) UI translation (#410)

* i18n: add French (fr) UI translation

A complete French catalog (menus, dialogs, panels, plurals), registered with its
plural rule (0 and 1 take the singular). Covers the latest commands, Stamp Visible
and Stamp Down included. A test checks the bundled UI fonts carry every French glyph.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* i18n(fr): translate labels added on main since this PR (from #582 by neko233-com)

Co-Authored-By: neko233-com <242357072+neko233-com@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(fr): translate the labels added on main since this catalog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(fr): Camera Raw scopes and the Patch tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: neko233-com <242357072+neko233-com@users.noreply.github.com>
This commit is contained in:
Simon Leblanc 2026-10-07 13:49:30 +02:00 committed by GitHub
parent b45dfde498
commit a4e96991f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 2527 additions and 4 deletions

View File

@ -424,6 +424,21 @@ mod tests {
}
}
/// The bundled UI fonts cover French (the `fr` UI language) on their own: every accented
/// letter, the ligatures, the guillemets and the no-break space used before `: ; ? !`.
#[test]
fn bundled_fonts_cover_french() {
use skrifa::MetadataProvider;
let french = "àâæçéèêëîïôœùûüÿ";
let letters: String = french.chars().chain(french.chars().flat_map(char::to_uppercase)).chain("«»\u{a0}’".chars()).collect();
for (name, bytes) in super::BUNDLED {
let font = skrifa::FontRef::new(bytes).expect("bundled font parses");
let cmap = font.charmap();
let missing: String = letters.chars().filter(|c| cmap.map(*c).is_none_or(|g| g.to_u32() == 0)).collect();
assert!(missing.is_empty(), "{name} lacks French glyphs: {missing:?}");
}
}
#[test]
fn missing_font_dirs_are_skipped() {
let mut files = Vec::new();

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@ pub struct LangInfo {
/// Catalog file contents (empty for the built-in English).
pub source: &'static str,
/// Plural form index for a count (English: 0 = one, 1 = other; Japanese and Chinese: always 0;
/// Czech: 0 = one, 1 = few (2–4), 2 = other). A catalog's `@plural` entries list one form per
/// Czech: 0 = one, 1 = few (2–4), 2 = other; French: 0 = one (0 and 1), 1 = other). A catalog's `@plural` entries list one form per
/// index.
pub plural: fn(u64) -> usize,
/// Must the catalog cover every menu string? (checked by the tests)
@ -69,8 +69,13 @@ fn plural_cs(n: u64) -> usize {
}
}
/// French: 0 and 1 take the singular, everything else the plural.
fn plural_fr(n: u64) -> usize {
usize::from(n > 1)
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 7] = [
pub static LANGUAGES: [LangInfo; 8] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
LangInfo {
@ -84,6 +89,7 @@ pub static LANGUAGES: [LangInfo; 7] = [
LangInfo { code: "es", name: "Español", source: include_str!("es.tsv"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "ru", name: "Русский", source: include_str!("ru.tsv"), plural: plural_russian, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "cs", name: "Čeština", source: include_str!("cs.tsv"), plural: plural_cs, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "fr", name: "Français", source: include_str!("fr.tsv"), plural: plural_fr, complete_menus: true, catalog: OnceLock::new() },
];
impl LangInfo {
@ -295,7 +301,7 @@ mod tests {
assert_eq!(lang_from_tag("POSIX"), Some(Lang::EN));
assert_eq!(lang_from_tag("cs_CZ.UTF-8"), Some(CS()));
assert_eq!(lang_from_tag("cs-CZ"), Some(CS()));
assert_eq!(lang_from_tag("fr_FR"), None);
assert_eq!(lang_from_tag("de_DE"), None);
// Traditional Chinese: by region, by script, and with a region after the script.
assert_eq!(lang_from_tag("zh_TW.UTF-8"), Some(ZH()));
assert_eq!(lang_from_tag("zh-TW"), Some(ZH()));
@ -324,7 +330,7 @@ mod tests {
#[test]
fn macos_language_list_is_parsed() {
assert_eq!(first_supported("(\n \"ja-JP\",\n \"en-US\"\n)\n"), Some(JA()));
assert_eq!(first_supported("(\n \"fr-FR\",\n \"en-US\"\n)\n"), Some(Lang::EN));
assert_eq!(first_supported("(\n \"de-DE\",\n \"en-US\"\n)\n"), Some(Lang::EN));
assert_eq!(first_supported("(\n \"zh-Hant-TW\",\n \"en-US\"\n)\n"), Some(ZH()));
assert_eq!(first_supported("("), None);
}
@ -430,6 +436,22 @@ mod tests {
assert_eq!(placeholders("a {x} b {y} {"), ["x", "y"]);
}
#[test]
fn french_resolves_and_pluralises() {
let fr = Lang::from_code("fr").expect("fr registered");
for tag in ["fr", "fr_FR.UTF-8", "fr-CA", "fr_BE", "fr-CH"] {
assert_eq!(lang_from_tag(tag), Some(fr), "{tag}");
}
assert_eq!(tr(fr, "Layer"), "Calque");
assert_eq!(tr_id(fr, "select.all", "All"), "Tout sélectionner", "an id override wins over the plain label");
assert_eq!(tr(fr, "All"), "Tout");
let forms: Vec<usize> = [0, 1, 2, 5, 100, u64::MAX].into_iter().map(plural_fr).collect();
assert_eq!(forms, [0, 0, 1, 1, 1, 1]);
assert_eq!(trn(fr, 0, "{n} item", "{n} items"), "0 élément");
assert_eq!(trn(fr, 1, "{n} item", "{n} items"), "1 élément");
assert_eq!(trn(fr, 3, "{n} item", "{n} items"), "3 éléments");
}
#[test]
fn czech_plural_rule() {
let forms: Vec<usize> = [0, 1, 2, 3, 4, 5, 11, 12, 21, 22, 100, u64::MAX].into_iter().map(plural_cs).collect();