i18n: add Polish (pl) interface language

Complete Polish catalog (4,492 rows): every menu string, tl! literal,
plural and status/error message, enforced by the existing completeness
tests (complete_menus, KEEPS_MENU_NAMES, COMPLETE_MESSAGES).

- pl.tsv: clean-room translation from the English meaning, following
  common Polish desktop-software conventions (obszar kompozycji for
  artboard, obszar roboczy for workspace, obrys, wypełnienie, próbki…)
- plural_polish: one / few (2–4, not 12–14) / many (0, 5–21, 25…)
- every pl-* locale (pl-PL, pl_PL.UTF-8) resolves to it
- tests: Polish plural forms, vocabulary, nested message translation;
  Polish letters in the bundled-font coverage test
- docs: README, development.md, localization-parity.md

Drafted with AI assistance; native review welcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DukHNvUY45rjRnvtz91x33
This commit is contained in:
Tomasz Lewandowski
2026-10-13 06:34:12 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent f3040e3762
commit db54eefeae
7 changed files with 4598 additions and 10 deletions
+1 -1
View File
@@ -220,7 +220,7 @@ doesn't write Affinity files. Current `.af` validation includes 33 pinned files,
and every-board SVG/PDF/PSD export, with fixes for Affinity 3 artboards, source-backed JPEGs and text runs. and every-board SVG/PDF/PSD export, with fixes for Affinity 3 artboards, source-backed JPEGs and text runs.
[Scope and limits](crates/affinity/README.md); [source audit and remaining gaps](docs/affinity-validation.md). [Scope and limits](crates/affinity/README.md); [source audit and remaining gaps](docs/affinity-validation.md).
The interface The interface
speaks English, German, Japanese, Korean, Vietnamese, Traditional and Simplified Chinese, Spanish, French, Hungarian, Italian, Russian and Ukrainian (and Czech and Brazilian Portuguese in the menus); speaks English, German, Japanese, Korean, Vietnamese, Traditional and Simplified Chinese, Spanish, French, Hungarian, Italian, Polish, Russian and Ukrainian (and Czech and Brazilian Portuguese in the menus);
[localization-parity.md](docs/localization-parity.md) has the details. [localization-parity.md](docs/localization-parity.md) has the details.
The scores are self-assessed from our code against Illustrator's public documentation; The scores are self-assessed from our code against Illustrator's public documentation;
[target-app-parity.md](docs/target-app-parity.md#how-this-was-measured-2026-10-10) explains how far to trust them. [target-app-parity.md](docs/target-app-parity.md#how-this-was-measured-2026-10-10) explains how far to trust them.
+1 -1
View File
@@ -739,7 +739,7 @@ mod tests {
let fr = crate::i18n::Lang::from_code("fr").unwrap(); let fr = crate::i18n::Lang::from_code("fr").unwrap();
let shown = crate::i18n::message(fr, "2 font file(s) are now in /Fonts; 1 couldn't be added: a.otf: larger than 256 MB"); let shown = crate::i18n::message(fr, "2 font file(s) are now in /Fonts; 1 couldn't be added: a.otf: larger than 256 MB");
assert!(shown.contains("se trouvent maintenant dans /Fonts") && shown.contains("a.otf") && shown.contains("plus de 256 Mo"), "{shown}"); assert!(shown.contains("se trouvent maintenant dans /Fonts") && shown.contains("a.otf") && shown.contains("plus de 256 Mo"), "{shown}");
for code in ["de", "es", "fr", "hu", "it", "ru"] { for code in ["de", "es", "fr", "hu", "it", "pl", "ru"] {
let lang = crate::i18n::Lang::from_code(code).unwrap(); let lang = crate::i18n::Lang::from_code(code).unwrap();
for s in [ for s in [
"every search thread stopped on an internal error (please report this bug)", "every search thread stopped on an internal error (please report this bug)",
+17 -1
View File
@@ -81,8 +81,20 @@ fn plural_east_slavic(n: u64) -> usize {
} }
} }
/// Polish: only 1 is `one`; 2–4 (but not 12–14) `few`; everything else (0, 5–21, 25…) `many`
/// (unlike Russian and Ukrainian, 21, 31 … take `many`: "21 warstw").
fn plural_polish(n: u64) -> usize {
if n == 1 {
return 0;
}
match (n % 10, n % 100) {
(2..=4, r) if !(12..=14).contains(&r) => 1,
_ => 2,
}
}
/// The registry. English first: it is the fallback and the source language. /// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 15] = [ pub static LANGUAGES: [LangInfo; 16] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
// Japanese: the whole interface (every menu string and `tl!` literal) and the status and error // Japanese: the whole interface (every menu string and `tl!` literal) and the status and error
// messages, keeping the product, workspace and perspective preset names in English (`MENU_KEEP_AS_IS`). // messages, keeping the product, workspace and perspective preset names in English (`MENU_KEEP_AS_IS`).
@@ -156,6 +168,10 @@ pub static LANGUAGES: [LangInfo; 15] = [
// English as Spanish; a count takes the singular ("3 réteg"), so plurals have one form. Every // English as Spanish; a count takes the singular ("3 réteg"), so plurals have one form. Every
// `hu-*` locale (including `hu-HU`) resolves here. // `hu-*` locale (including `hu-HU`) resolves here.
LangInfo { code: "hu", name: "Magyar", source: include_str!("hu.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, LangInfo { code: "hu", name: "Magyar", source: include_str!("hu.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
// Polish: the whole interface and the status and error messages, with one/few/many plurals,
// keeping the same names in English as Spanish; every `pl-*` locale (including `pl-PL`)
// resolves here.
LangInfo { code: "pl", name: "Polski", source: include_str!("pl.tsv"), plural: plural_polish, complete_menus: true, catalog: OnceLock::new() },
// Brazilian Portuguese: every menu label (`menu_catalogs_translate_every_menu_label`), every // Brazilian Portuguese: every menu label (`menu_catalogs_translate_every_menu_label`), every
// `tl!` literal and the plural messages; left English on purpose are the `MENU_KEEP_AS_IS` // `tl!` literal and the plural messages; left English on purpose are the `MENU_KEEP_AS_IS`
// strings (product name, language names, workspace names, perspective grid presets). // strings (product name, language names, workspace names, perspective grid presets).
File diff suppressed because it is too large Load Diff
+57 -4
View File
@@ -452,7 +452,7 @@ fn cs() -> Lang {
} }
/// Languages whose catalogs leave [`MENU_KEEP_AS_IS`] in English. /// Languages whose catalogs leave [`MENU_KEEP_AS_IS`] in English.
const KEEPS_MENU_NAMES: [&str; 12] = ["cs", "de", "es", "fr", "hu", "it", "ja", "ko", "pt-br", "ru", "uk", "vi"]; const KEEPS_MENU_NAMES: [&str; 13] = ["cs", "de", "es", "fr", "hu", "it", "ja", "ko", "pl", "pt-br", "ru", "uk", "vi"];
/// Menu labels the menu-complete catalogs (Czech, German, Spanish, Italian, Japanese, Brazilian Portuguese) show as they are: the product name, a format name, the built-in workspace /// Menu labels the menu-complete catalogs (Czech, German, Spanish, Italian, Japanese, Brazilian Portuguese) show as they are: the product name, a format name, the built-in workspace
/// names and the perspective grid presets (names, shown untranslated wherever else they appear). /// names and the perspective grid presets (names, shown untranslated wherever else they appear).
@@ -981,7 +981,7 @@ fn russian_plurals_have_three_forms() {
assert_eq!(forms, [2, 0, 1, 1, 2, 2, 0, 1, 2, 2, 0]); assert_eq!(forms, [2, 0, 1, 1, 2, 2, 0, 1, 2, 2, 0]);
} }
/// Czech, German, Spanish, French, Italian and Hungarian letters (and the punctuation their text uses) come from each family's own first font, not /// Czech, German, Spanish, French, Italian, Hungarian and Polish letters (and the punctuation their text uses) come from each family's own first font, not
/// from a fallback further down the stack. (`has_glyph` can't tell: it counts characters of the /// from a fallback further down the stack. (`has_glyph` can't tell: it counts characters of the
/// face that draws missing glyphs, the first one, as missing.) /// face that draws missing glyphs, the first one, as missing.)
#[test] #[test]
@@ -996,7 +996,7 @@ fn latin_script_glyphs_are_available_without_system_fonts() {
let first = first.unwrap(); let first = first.unwrap();
let mut font = fonts.fonts.font(&family); let mut font = fonts.fonts.font(&family);
let chars = font.characters(); let chars = font.characters();
for ch in "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ„“‚‘…–ñÑüÜ¿¡”àèìòùÀÈÌÒÙ«»’âêîôûçëïÿœæÂÊÎÔÛÇËÏŒÆäöÄÖßőűŐŰ\u{a0}".chars() for ch in "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ„“‚‘…–ñÑüÜ¿¡”àèìòùÀÈÌÒÙ«»’âêîôûçëïÿœæÂÊÎÔÛÇËÏŒÆäöÄÖßőűŐŰąćęłńśźżĄĆĘŁŃŚŹŻ\u{a0}".chars()
{ {
assert!(chars.get(&ch).is_some_and(|fonts| fonts.contains(&first)), "{first} ({family:?}) has no {ch}"); assert!(chars.get(&ch).is_some_and(|fonts| fonts.contains(&first)), "{first} ({family:?}) has no {ch}");
} }
@@ -1163,7 +1163,7 @@ fn select_key_param_error_is_translated() {
} }
/// Languages whose catalogs cover every status and error message. /// Languages whose catalogs cover every status and error message.
const COMPLETE_MESSAGES: &[&str] = &["cs", "de", "es", "fr", "hu", "it", "ja", "ko", "ru", "uk", "vi"]; const COMPLETE_MESSAGES: &[&str] = &["cs", "de", "es", "fr", "hu", "it", "ja", "ko", "pl", "ru", "uk", "vi"];
/// Crates whose error and status messages reach the status bar. /// Crates whose error and status messages reach the status bar.
const MESSAGE_CRATES: &[&str] = &["ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad", "trace"]; const MESSAGE_CRATES: &[&str] = &["ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad", "trace"];
@@ -1295,3 +1295,56 @@ fn czech_render_site_follow_up() {
assert_eq!(trn(cs(), n, "{n} PR", "{n} PRs"), format!("{n} PR")); assert_eq!(trn(cs(), n, "{n} PR", "{n} PRs"), format!("{n} PR"));
} }
} }
fn pl() -> Lang {
Lang::from_code("pl").expect("pl registered")
}
#[test]
fn polish_plurals_have_three_forms() {
let forms: Vec<usize> = [0, 1, 2, 4, 5, 11, 12, 14, 21, 22, 25, 101, 112, 122].into_iter().map(plural_polish).collect();
assert_eq!(forms, [2, 0, 1, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1]);
// Every Polish plural row has the three forms, and each count picks its own.
let (entries, _) = parse_entries(pl().0.source);
let mut count = 0;
for (ctx, src, tr) in &entries {
if ctx != "@plural" {
continue;
}
count += 1;
let forms: Vec<&str> = tr.split('|').collect();
assert_eq!(forms.len(), 3, "{src}");
let (one, other) = src.split_once('|').unwrap();
for (n, form) in [(1, 0), (3, 1), (5, 2), (21, 2), (22, 1)] {
assert_eq!(trn(pl(), n, one, other), forms[form].replace("{n}", &n.to_string()), "{src}: {n}");
}
}
assert!(count >= 13, "Polish plural rows missing");
}
/// Polish uses its vector-illustration vocabulary (obszar kompozycji, ścieżka, obrys…), resolves
/// every `pl-*` locale, counts with one/few/many and translates the reason inside a message too.
#[test]
fn polish_reads_as_polish() {
for tag in ["pl", "pl-PL", "pl_PL.UTF-8", "PL-pl"] {
assert_eq!(lang_from_tag(tag), Some(pl()), "{tag}");
}
for (en, want) in [
("File", "Plik"),
("Artboard Tool", "Narzędzie Obszar kompozycji"),
("Swatches", "Próbki"),
("Pathfinder", "Odnajdywanie ścieżek"),
("Stroke", "Obrys"),
("Fill", "Wypełnienie"),
("Workspace", "Obszar roboczy"),
] {
assert_eq!(tr(pl(), en), want);
}
for (n, word) in [(1, "warstwa"), (3, "warstwy"), (5, "warstw"), (22, "warstwy"), (21, "warstw")] {
assert_eq!(trn(pl(), n, "{n} Layer", "{n} Layers"), format!("{n} {word}"));
}
assert_eq!(tr_ctx(pl(), "axis", "Both"), "Obie");
assert_eq!(tr_id(pl(), "pathfinder.add", "Add"), "Połącz");
let nested = "command `object.group` is not available right now: nothing selected";
assert_eq!(message(pl(), nested), "polecenie `object.group` jest teraz niedostępne: nic nie zaznaczono");
}
+3 -2
View File
@@ -178,7 +178,8 @@ complete; `fr-FR`, `fr-BE`, `fr-CA`, `fr-CH` and every other `fr-*` locale resol
complete; `it-IT`, `it-CH` and every other `it-*` locale resolve to it), Russian (`ru`, complete; every `ru-*` complete; `it-IT`, `it-CH` and every other `it-*` locale resolve to it), Russian (`ru`, complete; every `ru-*`
locale such as `ru-RU`, `ru-BY` or `ru-KZ` resolves to it), Ukrainian (`uk`, complete; `uk-UA`, locale such as `ru-RU`, `ru-BY` or `ru-KZ` resolves to it), Ukrainian (`uk`, complete; `uk-UA`,
`uk_UA.UTF-8` and every other `uk-*` locale resolve to it; integer plurals use one/few/many), Hungarian (`hu`, complete; `uk_UA.UTF-8` and every other `uk-*` locale resolve to it; integer plurals use one/few/many), Hungarian (`hu`, complete;
`hu-HU` and every other `hu-*` locale resolve to it; a count takes the singular, so plurals have one form), Czech (`cs`, every menu label) and Brazilian Portuguese `hu-HU` and every other `hu-*` locale resolve to it; a count takes the singular, so plurals have one form), Polish (`pl`, complete; `pl-PL` and every other `pl-*`
locale resolve to it; plurals use one/few/many, with 21, 31 … taking many), Czech (`cs`, every menu label) and Brazilian Portuguese
(`pt-br`, every menu label and every `tl!` literal). Untranslated text falls back to English until its rows are added. (`pt-br`, every menu label and every `tl!` literal). Untranslated text falls back to English until its rows are added.
- `tl!("…")` translates a literal into the language the UI is drawn in; `i18n::t(s)` is the same for a - `tl!("…")` translates a literal into the language the UI is drawn in; `i18n::t(s)` is the same for a
@@ -206,7 +207,7 @@ locale such as `ru-RU`, `ru-BY` or `ru-KZ` resolves to it), Ukrainian (`uk`, com
control channel, MCP and tests read them) and are translated only where the status bar draws them control channel, MCP and tests read them) and are translated only where the status bar draws them
(`i18n::msg`). `@msg` catalog rows hold a whole message or a template such as (`i18n::msg`). `@msg` catalog rows hold a whole message or a template such as
`Couldn't open {name}: {e}`; `{_1}`, `{_2}` … stand for the format string's `{}`, and the values in the `Couldn't open {name}: {e}`; `{_1}`, `{_2}` … stand for the format string's `{}`, and the values in the
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Russian, Ukrainian and Vietnamese cover every placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Polish, Russian, Ukrainian and Vietnamese cover every
message literal the test scan finds (`complete_languages_translate_every_message`, languages listed in message literal the test scan finds (`complete_languages_translate_every_message`, languages listed in
`COMPLETE_MESSAGES`): a new `Err("…")`, `Other(…)`, `#[error(…)]` or `status(…)` message needs a row in each of those catalogs (`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui `COMPLETE_MESSAGES`): a new `Err("…")`, `Other(…)`, `#[error(…)]` or `status(…)` message needs a row in each of those catalogs (`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui
complete_languages_translate_every_message` lists them all). Other languages show messages in English complete_languages_translate_every_message` lists them all). Other languages show messages in English
+3 -1
View File
@@ -50,9 +50,10 @@ plus review. Native-speaker review is human work and isn't in the hours.
| Russian | `ru` | 4,009 / 4,020 | full | | Russian | `ru` | 4,009 / 4,020 | full |
| Ukrainian | `uk` | 4,009 / 4,020 (one/few/many plurals) | full | | Ukrainian | `uk` | 4,009 / 4,020 (one/few/many plurals) | full |
| Hungarian | `hu` | 4,524 rows: menus, `tl!` literals and messages complete (tests); one plural form | full | | Hungarian | `hu` | 4,524 rows: menus, `tl!` literals and messages complete (tests); one plural form | full |
| Polish | `pl` | 4,492 rows: menus, `tl!` literals and messages complete (tests); one/few/many plurals | full; native review pending |
| Czech | `cs` | 4,411 catalog entries, including 636 `@msg` rows (measured locally 2026-10-11); menu, literal and message coverage enforced by tests | catalog complete; render-site gaps reduced; native review pending | | Czech | `cs` | 4,411 catalog entries, including 636 `@msg` rows (measured locally 2026-10-11); menu, literal and message coverage enforced by tests | catalog complete; render-site gaps reduced; native review pending |
Fourteen non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen Fifteen non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
interface languages (including Polish, Dutch, Turkish, the Nordic languages, Hungarian, and Middle East interface languages (including Polish, Dutch, Turkish, the Nordic languages, Hungarian, and Middle East
editions with Arabic and Hebrew); it doesn't ship Hindi, Indonesian or Vietnamese. editions with Arabic and Hebrew); it doesn't ship Hindi, Indonesian or Vietnamese.
@@ -83,6 +84,7 @@ font for the web build.
| 2026-10-13 | minor | Czech: 128 more strings translated (tool names, PDF and Image Trace presets, pie graph options and others added since the catalog was completed; #1461); the entry count in the table is the 2026-10-11 measurement; native review still pending; scores and estimates unchanged | | 2026-10-13 | minor | Czech: 128 more strings translated (tool names, PDF and Image Trace presets, pie graph options and others added since the catalog was completed; #1461); the entry count in the table is the 2026-10-11 measurement; native review still pending; scores and estimates unchanged |
) )
| 2026-10-12 | minor | Korean (`ko`) catalog added, complete under the tests (menus, literals, messages); 12-language score 54% → 63%, hours 70–110 → 60–100 | | 2026-10-12 | minor | Korean (`ko`) catalog added, complete under the tests (menus, literals, messages); 12-language score 54% → 63%, hours 70–110 → 60–100 |
| 2026-10-12 | minor | Polish (`pl`) catalog added, complete under the tests (menus, literals, messages; one/few/many plurals) |
| 2026-10-12 | minor | Hungarian (`hu`) catalog added, complete under the tests | | 2026-10-12 | minor | Hungarian (`hu`) catalog added, complete under the tests |
| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged | | 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged |
| 2026-10-10 | major | First per-language table, measured from the catalog row counts against the complete French catalog | | 2026-10-10 | major | First per-language table, measured from the catalog row counts against the complete French catalog |