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 LewandowskiandClaude Opus 5.5 authored and bflatastic committed 2026-10-13 06:34:12 +07:00
1 parent f3040e3762
commit db54eefeae
7 files changed
+4598 -10

No files matched your search

+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.
[Scope and limits](crates/affinity/README.md); [source audit and remaining gaps](docs/affinity-validation.md).
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.
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.
+1 -1
View File
@@ -739,7 +739,7 @@ mod tests {
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");
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();
for s in [
"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.
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() },
// 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`).
@@ -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
// `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() },
// 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
// `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).
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.
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
/// 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]);
}
/// 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
/// face that draws missing glyphs, the first one, as missing.)
#[test]
@@ -996,7 +996,7 @@ fn latin_script_glyphs_are_available_without_system_fonts() {
let first = first.unwrap();
let mut font = fonts.fonts.font(&family);
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}");
}
@@ -1163,7 +1163,7 @@ fn select_key_param_error_is_translated() {
}
/// 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.
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"));
}
}
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-*`
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;
`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.
- `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
(`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
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
`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
+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 |
| 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 |
| 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 |
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
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-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-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 |