From 4d9cde8feedfdc428a41d66e4d742bac3ea79114 Mon Sep 17 00:00:00 2001 From: Losminek <301412321+Losminek@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:28:51 +0200 Subject: [PATCH] i18n: Czech one/few/other plurals Czech counts take three integer forms: one (1), few (2-4) and other (0 and 5+). Since #257 catalogs can supply `few`, so `plural()` gets a Czech rule and the 38 plural entries in cs.toml now read naturally ("1 vrstva", "3 vrstvy", "5 vrstev") instead of the count-neutral "Vrstvy: {n}" that #469 used for everything but 1. Test czech_integer_plurals covers the rule (1; 2-4; 0, 5, 11, 12, 21, 22, 100, 1 000 000), a real key through counted(), and that every Czech plural entry has one and few. Docs no longer say Czech has only one/other. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/locales/cs.toml | 76 ++++++++++++++++---------------- crates/ui-egui/src/i18n.rs | 5 ++- crates/ui-egui/src/i18n_tests.rs | 20 +++++++++ docs/localization-parity.md | 9 ++-- docs/translating.md | 6 +-- 5 files changed, 70 insertions(+), 46 deletions(-) diff --git a/crates/ui-egui/locales/cs.toml b/crates/ui-egui/locales/cs.toml index aba0bec7..268ff527 100644 --- a/crates/ui-egui/locales/cs.toml +++ b/crates/ui-egui/locales/cs.toml @@ -7,7 +7,7 @@ status = "draft — needs native review" [ui] "% of all commits" = "% všech commitů" "(no longer published)" = "(již nepublikováno)" -"({n} warnings: some entries were skipped)" = { one = "({n} varování: některé položky byly přeskočeny)", other = "(Varování: {n}; některé položky byly přeskočeny)" } +"({n} warnings: some entries were skipped)" = { one = "({n} varování: některé položky byly přeskočeny)", few = "({n} varování: některé položky byly přeskočeny)", other = "({n} varování: některé položky byly přeskočeny)" } "+ Create variable" = "+ Vytvořit proměnnou" ".fig (experimental)" = ".fig (experimentální)" "1…9, 0" = "1…9, 0" @@ -455,11 +455,11 @@ status = "draft — needs native review" "Export settings" = "Nastavení exportu" "Export variables" = "Exportovat proměnné" "Export {name}" = "Exportovat {name}" -"Export {n} layers" = { one = "Exportovat {n} vrstvu", other = "Exportovat vrstvy: {n}" } +"Export {n} layers" = { one = "Exportovat {n} vrstvu", few = "Exportovat {n} vrstvy", other = "Exportovat {n} vrstev" } "Exported {file}" = "Exportován soubor {file}" "Exported {name}" = "Exportováno: {name}" -"Exported {name}; {n} kinds of content were left out" = { one = "Exportováno: {name}; vynechán {n} druh obsahu", other = "Exportováno: {name}; vynechané druhy obsahu: {n}" } -"Exported {n} files" = { one = "Exportován {n} soubor", other = "Exportované soubory: {n}" } +"Exported {name}; {n} kinds of content were left out" = { one = "Exportováno: {name}; vynechán {n} druh obsahu", few = "Exportováno: {name}; vynechány {n} druhy obsahu", other = "Exportováno: {name}; vynecháno {n} druhů obsahu" } +"Exported {n} files" = { one = "Exportován {n} soubor", few = "Exportovány {n} soubory", other = "Exportováno {n} souborů" } "Export…" = "Exportovat…" "Expose properties to instances" = "Zpřístupnit vlastnosti instancím" "Exposure" = "Expozice" @@ -477,7 +477,7 @@ status = "draft — needs native review" "File not found" = "Soubor nenalezen" "File published" = "Soubor publikován" "Files aren't end-to-end encrypted: the server's operator can read them." = "Soubory nemají koncové šifrování: provozovatel serveru je může číst." -"Files nobody opens for {n} days are deleted." = { one = "Soubory, které nikdo neotevře déle než {n} den, se smažou.", other = "Soubory, které nikdo neotevře déle než {n} d, se smažou." } +"Files nobody opens for {n} days are deleted." = { one = "Soubory, které nikdo neotevře déle než {n} den, se smažou.", few = "Soubory, které nikdo neotevře déle než {n} dny, se smažou.", other = "Soubory, které nikdo neotevře déle než {n} dní, se smažou." } "Fill" = "Výplň" "Fill (fr)" = "Vyplnit (fr)" "Fill container" = "Vyplnit kontejner" @@ -601,7 +601,7 @@ status = "draft — needs native review" "Import plugin from manifest…" = "Importovat plugin z manifestu…" "Import variables" = "Importovat proměnné" "Imported {name}" = "Importováno: {name}" -"Imported {n} new variables, updated {updated}" = { one = "Importována {n} nová proměnná, aktualizováno: {updated}", other = "Nově importované proměnné: {n}, aktualizované: {updated}" } +"Imported {n} new variables, updated {updated}" = { one = "Importována {n} nová proměnná, aktualizováno: {updated}", few = "Importovány {n} nové proměnné, aktualizováno: {updated}", other = "Importováno {n} nových proměnných, aktualizováno: {updated}" } "Import…" = "Importovat…" "In an instance, this comes from the main component" = "V instanci se toto přebírá z hlavní komponenty" "In the browser you can join sessions the desktop app hosts and files on a FigCraft server. A page opened over https reaches other computers only through wss:// links (a server behind TLS)." = "V prohlížeči se můžete připojit k relacím hostovaným desktopovou aplikací a k souborům na serveru FigCraft. Stránka otevřená přes https může přistupovat k jiným počítačům pouze přes odkazy wss:// (server za TLS)." @@ -1013,7 +1013,7 @@ status = "draft — needs native review" "Recent" = "Nedávné" "Recent files" = "Nedávné soubory" "Recents" = "Nedávné" -"Recovered {n} unsaved files after the app quit unexpectedly: save them to keep them" = { one = "Po neočekávaném ukončení aplikace obnoven {n} neuložený soubor: uložte ho pro zachování", other = "Po neočekávaném ukončení aplikace obnoveny neuložené soubory (počet: {n}): uložte je pro zachování" } +"Recovered {n} unsaved files after the app quit unexpectedly: save them to keep them" = { one = "Po neočekávaném ukončení aplikace obnoven {n} neuložený soubor: uložte ho pro zachování", few = "Po neočekávaném ukončení aplikace obnoveny {n} neuložené soubory: uložte je pro zachování", other = "Po neočekávaném ukončení aplikace obnoveno {n} neuložených souborů: uložte je pro zachování" } "Rectangle" = "Obdélník" "Rectangles" = "Obdélníky" "Red" = "Červená" @@ -1287,8 +1287,8 @@ status = "draft — needs native review" "Swap to {name}" = "Vyměnit za {name}" "Swap width and height" = "Prohodit šířku a výšku" "Swapped instance" = "Instance vyměněna" -"Swapped {n} assets" = { one = "Vyměněn {n} zdroj", other = "Vyměněné zdroje: {n}" } -"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Vyměněn {n} zdroj; bez shody: {missed}, nadále používají původní knihovnu", other = "Vyměněné zdroje: {n}; bez shody: {missed}, nadále používají původní knihovnu" } +"Swapped {n} assets" = { one = "Vyměněn {n} zdroj", few = "Vyměněny {n} zdroje", other = "Vyměněno {n} zdrojů" } +"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Vyměněn {n} zdroj; bez shody: {missed}, nadále používají původní knihovnu", few = "Vyměněny {n} zdroje; bez shody: {missed}, nadále používají původní knihovnu", other = "Vyměněno {n} zdrojů; bez shody: {missed}, nadále používají původní knihovnu" } "Sweep" = "Rozsah" "System" = "Systém" "Tab / Shift+Tab" = "Tab / Shift+Tab" @@ -1479,7 +1479,7 @@ status = "draft — needs native review" "Zoom to selection" = "Přiblížit na výběr" "Zoom tool: click to zoom in, drag an area" = "Nástroj přiblížení: kliknutím přiblížíte, tažením vyberete oblast" "Zoom tool: zoom out" = "Nástroj přiblížení: oddálit" -"and {n} more" = { one = "a {n} další", other = "a další (počet: {n})" } +"and {n} more" = { one = "a {n} další", few = "a {n} další", other = "a {n} dalších" } "bleed marks" = "značky spadávky" "by {author}" = "autor: {author}" "call any command its access allows" = "volat libovolný příkaz povolený jeho oprávněními" @@ -1531,35 +1531,35 @@ status = "draft — needs native review" "{name}, in a live session" = "{name}, v živé relaci" "{name}, online, open only to you" = "{name}, online, přístupný pouze vám" "{name}, {kind}, {w} by {h}, selected" = "{name}, {kind}, {w} × {h}, vybráno" -"{n} added" = { one = "Přidáno: {n}", other = "Přidáno: {n}" } -"{n} autosave versions" = { one = "{n} automaticky uložená verze", other = "Automaticky uložené verze: {n}" } -"{n} changed" = { one = "Změněno: {n}", other = "Změněno: {n}" } -"{n} columns" = { one = "{n} sloupec", other = "Sloupce: {n}" } -"{n} components" = { one = "{n} komponenta", other = "Komponenty: {n}" } -"{n} components and styles have newer versions" = { one = "{n} komponenta nebo styl má novější verzi", other = "Komponenty a styly s novější verzí: {n}" } -"{n} conflicts to resolve" = { one = "{n} konflikt k vyřešení", other = "Konflikty k vyřešení: {n}" } -"{n} days ago" = { one = "Před {n} dnem", other = "Před {n} dny" } -"{n} files" = { one = "{n} soubor", other = "Soubory: {n}" } -"{n} hours ago" = { one = "Před {n} hodinou", other = "Před {n} hodinami" } -"{n} instances" = { one = "{n} instance", other = "Instance: {n}" } -"{n} layers" = { one = "{n} vrstva", other = "Vrstvy: {n}" } -"{n} layers as one file" = { one = "{n} vrstva jako jeden soubor", other = "Vrstvy jako jeden soubor: {n}" } -"{n} layers selected" = { one = "Vybrána {n} vrstva", other = "Vybrané vrstvy: {n}" } -"{n} layers · {parts}" = { one = "{n} vrstva · {parts}", other = "Vrstvy: {n} · {parts}" } -"{n} matches" = { one = "{n} shoda", other = "Shody: {n}" } -"{n} minutes ago" = { one = "Před {n} minutou", other = "Před {n} minutami" } -"{n} months ago" = { one = "Před {n} měsícem", other = "Před {n} měsíci" } +"{n} added" = { one = "Přidáno: {n}", few = "Přidáno: {n}", other = "Přidáno: {n}" } +"{n} autosave versions" = { one = "{n} automaticky uložená verze", few = "{n} automaticky uložené verze", other = "{n} automaticky uložených verzí" } +"{n} changed" = { one = "Změněno: {n}", few = "Změněno: {n}", other = "Změněno: {n}" } +"{n} columns" = { one = "{n} sloupec", few = "{n} sloupce", other = "{n} sloupců" } +"{n} components" = { one = "{n} komponenta", few = "{n} komponenty", other = "{n} komponent" } +"{n} components and styles have newer versions" = { one = "{n} komponenta nebo styl má novější verzi", few = "{n} komponenty nebo styly mají novější verzi", other = "{n} komponent nebo stylů má novější verzi" } +"{n} conflicts to resolve" = { one = "{n} konflikt k vyřešení", few = "{n} konflikty k vyřešení", other = "{n} konfliktů k vyřešení" } +"{n} days ago" = { one = "Před {n} dnem", few = "Před {n} dny", other = "Před {n} dny" } +"{n} files" = { one = "{n} soubor", few = "{n} soubory", other = "{n} souborů" } +"{n} hours ago" = { one = "Před {n} hodinou", few = "Před {n} hodinami", other = "Před {n} hodinami" } +"{n} instances" = { one = "{n} instance", few = "{n} instance", other = "{n} instancí" } +"{n} layers" = { one = "{n} vrstva", few = "{n} vrstvy", other = "{n} vrstev" } +"{n} layers as one file" = { one = "{n} vrstva jako jeden soubor", few = "{n} vrstvy jako jeden soubor", other = "{n} vrstev jako jeden soubor" } +"{n} layers selected" = { one = "Vybrána {n} vrstva", few = "Vybrány {n} vrstvy", other = "Vybráno {n} vrstev" } +"{n} layers · {parts}" = { one = "{n} vrstva · {parts}", few = "{n} vrstvy · {parts}", other = "{n} vrstev · {parts}" } +"{n} matches" = { one = "{n} shoda", few = "{n} shody", other = "{n} shod" } +"{n} minutes ago" = { one = "Před {n} minutou", few = "Před {n} minutami", other = "Před {n} minutami" } +"{n} months ago" = { one = "Před {n} měsícem", few = "Před {n} měsíci", other = "Před {n} měsíci" } "{n} of {total}" = "{n} z {total}" -"{n} people joined" = { one = "Připojena {n} osoba", other = "Připojené osoby: {n}" } -"{n} preferred components" = { one = "{n} preferovaná komponenta", other = "Preferované komponenty: {n}" } -"{n} ready" = { one = "Připraveno: {n}", other = "Připraveno: {n}" } -"{n} removed" = { one = "Odebráno: {n}", other = "Odebráno: {n}" } -"{n} replies" = { one = "{n} odpověď", other = "Odpovědi: {n}" } -"{n} rows" = { one = "{n} řádek", other = "Řádky: {n}" } -"{n} styles" = { one = "{n} styl", other = "Styly: {n}" } -"{n} variables" = { one = "{n} proměnná", other = "Proměnné: {n}" } -"{n} weeks ago" = { one = "Před {n} týdnem", other = "Před {n} týdny" } -"{n} years ago" = { one = "Před {n} rokem", other = "Před {n} lety" } +"{n} people joined" = { one = "Připojila se {n} osoba", few = "Připojily se {n} osoby", other = "Připojilo se {n} osob" } +"{n} preferred components" = { one = "{n} preferovaná komponenta", few = "{n} preferované komponenty", other = "{n} preferovaných komponent" } +"{n} ready" = { one = "Připraveno: {n}", few = "Připraveno: {n}", other = "Připraveno: {n}" } +"{n} removed" = { one = "Odebráno: {n}", few = "Odebráno: {n}", other = "Odebráno: {n}" } +"{n} replies" = { one = "{n} odpověď", few = "{n} odpovědi", other = "{n} odpovědí" } +"{n} rows" = { one = "{n} řádek", few = "{n} řádky", other = "{n} řádků" } +"{n} styles" = { one = "{n} styl", few = "{n} styly", other = "{n} stylů" } +"{n} variables" = { one = "{n} proměnná", few = "{n} proměnné", other = "{n} proměnných" } +"{n} weeks ago" = { one = "Před {n} týdnem", few = "Před {n} týdny", other = "Před {n} týdny" } +"{n} years ago" = { one = "Před {n} rokem", few = "Před {n} lety", other = "Před {n} lety" } "{n}d" = "{n} d" "{n}h" = "{n} h" "{n}m" = "{n} min" diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 46ccd909..d30130fb 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -163,6 +163,9 @@ enum Plural { /// CLDR integer rules. Existing one/other catalogs may omit the extra forms. fn plural(code: &str, n: usize) -> Plural { match code { + "cs" if n == 1 => Plural::One, + "cs" if (2..=4).contains(&n) => Plural::Few, + "cs" => Plural::Other, "ru" if n % 10 == 1 && n % 100 != 11 => Plural::One, "ru" if (2..=4).contains(&(n % 10)) && !(12..=14).contains(&(n % 100)) => Plural::Few, "ru" => Plural::Many, @@ -174,7 +177,7 @@ fn plural(code: &str, n: usize) -> Plural { } // CLDR's million-count "many" in French, Spanish and Portuguese still reads their existing -// "other" wording. Only Russian currently supplies separate few/many forms. +// "other" wording. Czech supplies separate few forms; Russian supplies few/many forms. /// Whether count `n` takes the "one" form: 0 and 1 in French and Brazilian Portuguese, never in /// Japanese, Korean and Chinese (no plural forms: always "other"), 1 elsewhere (CLDR's integer rules). diff --git a/crates/ui-egui/src/i18n_tests.rs b/crates/ui-egui/src/i18n_tests.rs index 4a03ae69..07f32037 100644 --- a/crates/ui-egui/src/i18n_tests.rs +++ b/crates/ui-egui/src/i18n_tests.rs @@ -419,6 +419,26 @@ fn the_key_scanner_reads_rust_literals() { assert_eq!(literals(src), vec!["A \"b\"", "C {n}", "DE"]); } +#[test] +fn czech_integer_plurals() { + let cs = LANGS.iter().position(|l| l.code == "cs").unwrap(); + assert_eq!(plural("cs", 1), Plural::One); + assert_eq!(fill(counted(cs, "{n} layers", 1), &[("n", &1)]), "1 vrstva"); + for n in [2, 3, 4] { + assert_eq!(plural("cs", n), Plural::Few, "{n}"); + assert_eq!(fill(counted(cs, "{n} layers", n), &[("n", &n)]), format!("{n} vrstvy")); + } + for n in [0, 5, 11, 12, 21, 22, 100, 1_000_000] { + assert_eq!(plural("cs", n), Plural::Other, "{n}"); + assert_eq!(fill(counted(cs, "{n} layers", n), &[("n", &n)]), format!("{n} vrstev")); + } + for (key, text) in LANGS[cs].ui { + if let Text::Plural { one, few, .. } = text { + assert!(one.is_some() && few.is_some(), "{key}: missing Czech plural forms"); + } + } +} + #[test] fn russian_language_tags_and_integer_plurals() { let ru = LANGS.iter().position(|l| l.code == "ru").unwrap(); diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 1fbf705f..45a7a925 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -21,7 +21,7 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v - **Layer:** one TOML catalog per language in `crates/ui-egui/locales/`, baked into the binary by the UI crate's build script (desktop and web; nothing read at run time). Keys are the English text - (`tr("Fill")`, `trf` for `{named}` placeholders, `trn` for counts with one/other plural forms), so + (`tr("Fill")`, `trf` for `{named}` placeholders, `trn` for counts with one/few/many/other plural forms), so a missing translation shows English, never a raw key. One English text that needs two translations takes a context (`"Fill|sizing"`). - **Strings:** 1,417 UI keys in `en.toml` (36 with plural forms; ~110 are labels from engine and @@ -55,9 +55,10 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v until someone translates it. Every draft translates every string as of 2026-10-12 (checked by adding each to `COMPLETE` locally and running the check). - **Not yet localized:** messages the engine writes (errors, a few toasts, Dev Mode property values - such as "inside"), number formatting (decimal points stay `.`), and plural forms beyond one/other - (French and Brazilian Portuguese take the singular for 0 as well; Japanese, Korean and Chinese - have one form; CLDR's "many" for millions reads as the plural). + such as "inside"), number formatting (decimal points stay `.`), and decimal plural rules + (`trn` takes integers: Czech uses one/few/other and Russian one/few/many; French and Brazilian + Portuguese take the singular for 0 as well; Japanese, Korean and Chinese have one form; + CLDR's "many" for millions in French, Spanish and Portuguese reads as the plural). - **Script support in documents:** Unicode bidi with Arabic shaping and mirrored brackets in text layers; right-to-left list markers and indents; right-to-left names in the layers panel, tabs and canvas labels (visual order, joined Arabic); inline IME composition with the candidate window diff --git a/docs/translating.md b/docs/translating.md index bdb3a913..d93c0255 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -80,9 +80,9 @@ language also needs its script in `cjk::language`. `few` for 2–4, 22–24… except 12–14; and `many` for the other integers, including 0. Keep `{n}` in all Russian forms. Add new rules to `plural` in [`crates/ui-egui/src/i18n.rs`](../crates/ui-egui/src/i18n.rs). A missing form reads `other`; - a missing key uses the fallback catalog's own plural rule. Czech has no rule of its own yet, so it - picks `one` for 1 and `other` for everything else: its `other` forms are count-neutral for 0, - 2–4 and 5+ (e.g. `Vrstvy: {n}`). + a missing key uses the fallback catalog's own plural rule. Czech uses `one` for 1, `few` for + 2–4 and `other` for 0 and 5+ (e.g. `{n} vrstva`, `{n} vrstvy`, `{n} vrstev`). Keep `{n}` in all + three forms. Czech's CLDR `many` applies only to decimals; `trn` takes integer counts. 4. To require full coverage, add the code to `COMPLETE` in [`crates/ui-egui/src/i18n_tests.rs`](../crates/ui-egui/src/i18n_tests.rs). Every later PR that adds a string then has to translate it into each complete language, so only Spanish is there: