Merge #829: Polish UI language

# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/chrome.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/menus.rs
#	docs/localization-parity.md
#	docs/target-app-parity.md
This commit is contained in:
aiongg
2026-10-13 20:06:53 +00:00
7 changed files with 1829 additions and 10 deletions
+1 -1
View File
@@ -98,7 +98,7 @@ Interface strings translated, of 1,724 measured. Detail: [docs/localization-pari
| Korean | ko | none | 0% |
| Vietnamese | vi | none | 0% |
Also shipped: Czech (99%), Italian (85%), Ukrainian (83%).
Also shipped: Czech (99%), Italian (85%), Ukrainian (83%), Polish (99%).
## Upcoming
+5
View File
@@ -29,6 +29,11 @@ sync_github_name = false
[people.emanuelebonaventura]
display_name = "insanityxoff"
[people.lewandowskit]
real_name = "Tomasz Lewandowski"
display_name = "Tomasz Lewandowski"
sync_github_name = false
[people.moppeeer]
sync_github_name = true
+57 -5
View File
@@ -10,6 +10,7 @@ mod de;
mod fr;
mod it;
mod ja;
mod pl;
mod pt_br;
mod ru;
mod uk;
@@ -28,6 +29,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[
("uk", "Українська"),
("ru", "Русский"),
("cs", "Čeština"),
("pl", "Polski"),
];
/// English → [German, French, Spanish, Japanese, Simplified Chinese].
@@ -4100,6 +4102,7 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
static UKRAINIAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static PORTUGUESE_BR: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static ITALIAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static POLISH: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static RUSSIAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static CZECH: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static FRENCH: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
@@ -4130,6 +4133,9 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
if lang == "it" {
return ITALIAN.get_or_init(|| it::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
if lang == "pl" {
return POLISH.get_or_init(|| pl::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
if lang == "uk" {
return UKRAINIAN.get_or_init(|| uk::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
@@ -4165,11 +4171,11 @@ pub(crate) fn russian_catalog() -> Vec<&'static str> {
ru::TABLE.iter().map(|(_, text)| *text).chain(ru::CONTEXT.iter().map(|(_, _, text)| *text)).collect()
}
/// Ukrainian and Russian have several plural forms, so their count captions put the category
/// before the value ("сторінок: 5", "страниц: 5") and every number stays grammatical; other
/// languages keep the existing value-first caption.
/// Ukrainian, Russian and Polish have several plural forms, so their count captions put the
/// category before the value ("сторінок: 5", "страниц: 5", "Strony: 5") and every number stays
/// grammatical; other languages keep the existing value-first caption.
pub fn category_first(lang: &str) -> bool {
matches!(lang, "uk" | "ru")
matches!(lang, "uk" | "ru" | "pl")
}
/// Caption for a counted category, e.g. `count_label("ru", "pages", 5)` is "страниц: 5". Czech
@@ -4253,8 +4259,11 @@ mod tests {
for (key, expected) in fr::TABLE {
assert_eq!(tr("fr", key), *expected, "fr: {key}");
}
for (key, expected) in pl::TABLE {
assert_eq!(tr("pl", key), *expected, "pl: {key}");
}
let unknown = String::from("A user-defined untranslated label");
for lang in ["cs", "uk", "ru", "ar", "pt-br", "it", "zh", "ja", "de", "fr", "es", "", "unknown"] {
for lang in ["pl", "cs", "uk", "ru", "ar", "pt-br", "it", "zh", "ja", "de", "fr", "es", "", "unknown"] {
assert!(std::ptr::eq(tr(lang, &unknown), unknown.as_str()));
}
}
@@ -4454,6 +4463,49 @@ mod tests {
}
}
#[test]
fn polish_catalog_covers_existing_keys_and_preserves_placeholders() {
let catalog: HashMap<_, _> = pl::TABLE.iter().copied().collect();
assert_eq!(catalog.len(), pl::TABLE.len(), "duplicate Polish keys");
let mut keys = std::collections::HashSet::new();
keys.extend(TABLE.iter().map(|(key, _)| *key));
for table in [ar::TABLE, pt_br::TABLE, ja::TABLE, it::TABLE, uk::TABLE, de::TABLE, fr::TABLE, ru::TABLE, cs::TABLE] {
keys.extend(table.iter().map(|(key, _)| *key));
}
// A new English string falls back to English until it is translated; only stale entries fail.
let stale: Vec<_> = catalog.keys().filter(|k| !keys.contains(*k)).collect();
assert!(stale.is_empty(), "Polish entries for unknown keys: {stale:?}");
let placeholders =
|s: &str| s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name.to_owned())).collect::<Vec<_>>();
for (key, translation) in pl::TABLE {
assert!(!translation.trim().is_empty(), "empty {key}");
let mut source = placeholders(key);
let mut translated = placeholders(translation);
source.sort();
translated.sort();
assert_eq!(source, translated, "placeholders: {key}");
assert_eq!(key.ends_with('…'), translation.ends_with('…'), "ellipsis: {key}");
}
assert!(LANGUAGES.contains(&("pl", "Polski")));
assert!(!is_rtl("pl"));
assert_eq!(tr("pl", "File"), "Plik");
assert_eq!(tr("pl", "New Document…"), "Nowy dokument…");
assert_eq!(tr("pl", "Spread"), "Rozkładówka");
assert_eq!(tr("pl", "Unknown label"), "Unknown label");
assert_eq!(style_name("pl", "[Basic Paragraph]"), "[Podstawowy akapit]");
assert_eq!(workspace_name("pl", "Essentials"), "Podstawowe");
}
#[test]
fn polish_counts_use_category_captions_for_every_number() {
for n in [0, 1, 2, 5, 12, 22, 25] {
assert_eq!(count_label("pl", "pages", n), format!("Strony: {n}"));
assert_eq!(count_label("pl", "objects", n), format!("Obiekty: {n}"));
assert_eq!(count_label("pl", "errors", n), format!("Błędy: {n}"));
}
assert_eq!(tr("pl", "{pages} Pages in {spreads} Spreads"), "Strony: {pages}, rozkładówki: {spreads}");
}
#[test]
fn czech_catalog_has_no_stale_keys_and_keeps_placeholders_and_markers() {
let catalog: HashMap<_, _> = cs::TABLE.iter().copied().collect();
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -3525,6 +3525,26 @@ mod tests {
assert!(crate::i18n::LANGUAGES.contains(&("cs", "Čeština")), "Preferences lists Czech by its own name");
}
#[test]
fn polish_language_is_selectable_persisted_and_keeps_document_data() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"pages": 2, "facingPages": true})).unwrap();
app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "text", "text": "Zażółć gęślą jaźń"})).unwrap();
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
run_ui(&mut app, "app.language", &json!({"lang": "pl"})).unwrap().unwrap();
assert_eq!(app.ui.language, "pl");
assert_eq!(checked(&app, "app.language", &json!({"lang": "pl"})), Some(true));
assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap());
for (title, _) in menu_tree() {
assert_ne!(crate::i18n::tr("pl", title), title, "{title}");
}
let bytes = serde_json::to_vec(&app.ui).unwrap();
let restored: crate::UiState = serde_json::from_slice(&bytes).unwrap();
assert_eq!(restored.language, "pl");
assert_eq!(crate::i18n::tr(&restored.language, "File"), "Plik");
assert!(crate::i18n::LANGUAGES.contains(&("pl", "Polski")), "Preferences lists Polish by its own name");
}
#[test]
fn arabic_dialog_and_language_switch_keep_finite_widget_geometry() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
@@ -3552,7 +3572,7 @@ mod tests {
crate::dialogs::confirm(&mut app).unwrap();
frame(&mut app);
assert_eq!(app.session.documents().len(), 1);
for lang in ["uk", "ru", "zh", "", "ar", "pt-br", "it", "cs"] {
for lang in ["uk", "ru", "zh", "", "ar", "pt-br", "it", "cs", "pl"] {
app.run("app.language", json!({"lang": lang})).unwrap();
frame(&mut app);
}
+4 -2
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Japanese print, import and table controls localized; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (Polish interface; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6)
Per-language status of the DesignCraft interface and of document typesetting in each script.
Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-typography.md),
@@ -55,8 +55,9 @@ craft-fonts. The language totals above retain their dated measurement; this is a
| Czech | cs | 1,722 (99%), plural categories handled | partial |
| Italian | it | 1,464 (85%) | partial |
| Ukrainian | uk | 1,432 (83%), plural categories handled | partial |
| Polish | pl | 1,717 (99%), count captions category-first; AI-drafted, awaiting native review | partial |
DesignCraft ships **3** interface languages beyond the twelve; InDesign ships 17 beyond its
DesignCraft ships **4** interface languages beyond the twelve; InDesign ships 17 beyond its
eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, tr, uk, zh_TW).
## Document languages
@@ -86,3 +87,4 @@ locale shaping. See [typography-parity.md](typography-parity.md).
| 2026-10-11 | minor | Localized Japanese print, import and table controls; checked Japanese and English renders |
| 2026-10-10 | major | Created: catalogs measured (1,724 strings), the twelve key languages plus Italian and Ukrainian |
| 2026-10-11 | minor | Czech (cs) added: 1,722 strings, plural categories handled |
| 2026-10-13 | minor | Polish interface added (1,717 strings) |
+1 -1
View File
@@ -374,7 +374,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. Interactive PDF export (`file.exportPdf {media: true}`, File ▸ Export PDF (Interactive)) embeds each file and plays it in a Screen annotation through a rendition action (controls, loop, play on page load). No in-app playback.
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default; IDML Kashidas): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left.
- Interface language (Preferences ▸ General ▸ Interface Language, `app.language`; each language listed by its own name): German, French, Spanish, Italian, Japanese, Simplified Chinese and Arabic menu titles, common menu items and panel names (our own translations). Common dialog, panel and tooltip labels are also localized, with English fallback for untranslated strings. Arabic translations cover every key in the shared interface translation table, including menu commands and utility labels. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Arabic labels use Unicode bidi run ordering and contextual shaping; welcome and new-document form layouts follow RTL direction independently of document binding. CJK and Arabic glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic and Noto Sans CJK SC for the UI (Chinese forms first in the Chinese interface), Noto Sans Arabic, and Mincho for Japanese documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts. Ukrainian covers all 1,430 existing catalog keys, including publishing dialogs and built-in names, with persisted selection, grammatical count captions, English fallback and bundled Cyrillic UI glyph coverage.
- Interface language (Preferences ▸ General ▸ Interface Language, `app.language`; each language listed by its own name): German, French, Spanish, Italian, Japanese, Simplified Chinese and Arabic menu titles, common menu items and panel names (our own translations). Common dialog, panel and tooltip labels are also localized, with English fallback for untranslated strings. Arabic translations cover every key in the shared interface translation table, including menu commands and utility labels. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Arabic labels use Unicode bidi run ordering and contextual shaping; welcome and new-document form layouts follow RTL direction independently of document binding. CJK and Arabic glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic and Noto Sans CJK SC for the UI (Chinese forms first in the Chinese interface), Noto Sans Arabic, and Mincho for Japanese documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts. Ukrainian covers all 1,430 existing catalog keys, including publishing dialogs and built-in names, with persisted selection, grammatical count captions, English fallback and bundled Cyrillic UI glyph coverage. Polish covers 1,717 catalog keys with the same category-first count captions and English fallback.
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with the working CMYK space's profile (by default craft-cms's Coated FOGRA39 profile, identifier FOGRA39; Generic CMYK and Device CMYK embed our Generic CMYK profile, lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, `pdf:Trapped` in XMP as in Info, RGB images without a profile tagged sRGB, no image interpolation (none in PDF/A either), and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
- EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted.