M14: add Catalan interface and date localization

Register Catalan (`ca`) as an interface and date language with a
2,592-entry contributor-original catalog written from the meaning of the
English labels. Plurals use the one/other rule; every `ca-*` locale,
including Valencian (`ca_ES@valencia`), resolves to this catalog.

Tests cover registration and persistence, every command, menu, tool group
and section, every `tl!` literal in the UI source, and every key in the
other bundled catalogs. The Latin-script glyph test in tests/fonts.rs now
includes Catalan (the interpunct of l·l is covered by the bundled faces).

Catalan dates elide the preposition before a vowel (7 de març, 7 d'abril,
8 d'octubre), so the date language sets months_with_day, as Czech and Russian
do; a unit test pins it.

Update localization docs, the parity table and the roadmap log, and add the
ATTRIBUTION.toml entry (ATTRIBUTION.md regenerated). The contributor, a
native Catalan speaker, has reviewed the translation.

Validation: cargo fmt --check; clippy -D warnings and tests on
pdfcraft-ui-egui (i18n, fonts); pdfcraft-engine dates tests;
cargo xtask assets; cargo xtask layers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Guillem Vela
2026-10-15 16:51:02 +02:00
co-authored by Claude Sonnet 5.5
parent afa8e1843c
commit aad803caae
10 changed files with 2828 additions and 12 deletions
+2 -1
View File
@@ -4,7 +4,7 @@
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
## In this repository (272)
## In this repository (273)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
@@ -280,6 +280,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file |
| `crates/ui-egui/src/i18n/ca.tsv` | Catalan interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, written from their meaning; no proprietary localisation resources | Catalan UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
## Compiled in through dependencies (6)
+11
View File
@@ -3375,3 +3375,14 @@ licence_file = "vendor/vello_common/LICENSE-MIT"
kind = "image"
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697"
[[asset]]
path = "crates/ui-egui/src/i18n/ca.tsv"
title = "Catalan interface translation catalog"
author = "PdfCraft contributors"
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, written from their meaning; no proprietary localisation resources"
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Catalan UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0988fff524d2b76e21a977d078e92da67ea927c9422ac5b67a9ac476c0f441d5"
+1
View File
@@ -201,6 +201,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
- **2026-10-14 (M3, control-click candidate):** Coordinate clicks retain hover warmup and pair press/release in one input pass so slow frames do not turn a click into a hold. Text multi-click selection accepts the actual press event when egui has already cleared its origin. Regression cases cover slow-frame button activation, disabled controls and coalesced text selection; estimates unchanged.
- **2026-10-14 (community, M7, #128 Hebrew in Add text):** Add text and `page_add_text` draw Hebrew with the craft-fonts Hebr face (Noto Sans Hebrew), through the same shaped path as Arabic and Thai: right-to-left display order with numbers and Latin in place, points and cantillation kept on their letter as one Type 3 glyph, ToUnicode and per-line ActualText so extraction, search and the editor get typing order. Builds without the face refuse Hebrew rather than write `?`. Regular style only. Estimates unchanged.
- **2026-10-14 (M9, #1787):** Changes after signing are judged by the role an object held when signed for a page's resources too: a rewritten font, XObject, graphics state or resources dictionary is page content however its new version is labelled, so a `/FT` or `/Subtype /Widget` added by the attacker no longer reads as a permitted form fill. A category dictionary that only gained a name is judged as before.
- **2026-10-14 (M14, Catalan localization):** Added Catalan as another interface and date language, with a 2,592-entry clean-room catalog, one/other plurals and coverage tests for commands, tools, UI literals and every key in the other catalogs. A native Catalan speaker (the contributor) has reviewed the translation. Estimates unchanged.
- **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged.
- **2026-10-13 (community, M3, #531 touchpad momentum):** On Wayland a two-finger swipe keeps gliding after the fingers lift and slows to a stop, measured from the gesture's last 0.1 s and decaying with a 0.35 s time constant through the #759 1:1 path. Any wheel input, click, key, zoom or edge ends it (fingers resting on the touchpad don't yet: the vendored winit binds no Wayland hold gestures); single-page paging, macOS (which sends its own momentum), the web, and Animation effects Off never glide. Unit tests for the physics and two headless view tests. Estimates unchanged.
- **2026-10-13 (M3, native-open admission):** Later prepared replies stay queued while a modal or native picker owns the foreground. Picker completion, cancellation and abandonment wake retained opens. Regressions cover recipient ownership, exactly-once admission, source/history preservation and one-shot repaint requests. Estimates unchanged.
+35 -1
View File
@@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء",
/// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language):
/// the interface languages, in the same order and with the same names.
pub static DATE_LANGUAGES: [DateLanguage; 23] = [
pub static DATE_LANGUAGES: [DateLanguage; 24] = [
DateLanguage {
code: "en",
name: "English",
@@ -398,6 +398,29 @@ pub static DATE_LANGUAGES: [DateLanguage; 23] = [
days: ["Chủ Nhật", "Thứ Hai", "Thứ Ba", "Thứ Tư", "Thứ Năm", "Thứ Sáu", "Thứ Bảy"],
days_short: ["CN", "T2", "T3", "T4", "T5", "T6", "T7"],
},
DateLanguage {
code: "ca",
name: "Català",
months: ["gener", "febrer", "març", "abril", "maig", "juny", "juliol", "agost", "setembre", "octubre", "novembre", "desembre"],
// After a day the month carries its own preposition, elided before a vowel: `7 d'abril`.
months_with_day: Some([
"de gener",
"de febrer",
"de març",
"d'abril",
"de maig",
"de juny",
"de juliol",
"d'agost",
"de setembre",
"d'octubre",
"de novembre",
"de desembre",
]),
months_short: ["gen.", "febr.", "març", "abr.", "maig", "juny", "jul.", "ag.", "set.", "oct.", "nov.", "des."],
days: ["diumenge", "dilluns", "dimarts", "dimecres", "dijous", "divendres", "dissabte"],
days_short: ["dg.", "dl.", "dt.", "dx.", "dj.", "dv.", "ds."],
},
];
/// The date language for `code` (any case), or `None` when there's none.
@@ -718,6 +741,17 @@ mod tests {
assert_eq!(in_lang("dddd, mmmm", "cs"), "sobota, březen");
}
#[test]
fn catalan_months_elide_the_preposition_after_a_day() {
assert_eq!(in_lang("dddd d mmmm \\de yyyy", "ca"), "dissabte 7 de març de 2026");
assert_eq!(in_lang("mmmm yyyy", "ca"), "març 2026");
let ca = date_language("ca").unwrap();
assert_eq!(format_day("d mmmm \\de yyyy", (2026, 4, 23), ca), "23 d'abril de 2026");
assert_eq!(format_day("d mmmm", (2026, 8, 1), ca), "1 d'agost");
assert_eq!(format_day("d mmmm", (2026, 10, 8), ca), "8 d'octubre");
assert_eq!(format_day("mmmm", (2026, 10, 8), ca), "octubre", "no preposition without a day");
}
#[test]
fn the_session_writes_names_in_its_date_language() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
File diff suppressed because it is too large Load Diff
+86 -1
View File
@@ -147,7 +147,7 @@ fn plural_sl(n: u64) -> usize {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 23] = [
pub static LANGUAGES: [LangInfo; 24] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
@@ -210,6 +210,8 @@ pub static LANGUAGES: [LangInfo; 23] = [
// Vietnamese; every `vi-*` locale (`vi-VN`, `vi_VN.UTF-8`) resolves here. CLDR `vi` counts have
// one form ("other"), so `@plural` entries list a single form.
LangInfo { code: "vi", name: "Tiếng Việt", source: include_str!("vi.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
// Catalan; every `ca-*` locale (`ca-ES`, `ca_ES.UTF-8`, `ca_ES@valencia`) resolves here.
LangInfo { code: "ca", name: "Català", source: include_str!("ca.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
];
impl LangInfo {
@@ -2539,6 +2541,89 @@ mod tests {
assert!(unchanged.is_empty(), "unapproved English Vietnamese catalog entries: {unchanged:#?}");
}
#[test]
fn catalan_is_registered_and_persists() {
let ca = Lang::from_code("ca").expect("ca registered");
assert_eq!(ca.name(), "Català");
assert_eq!(normalize_pref("CA"), Some("ca"));
for tag in ["ca", "ca-ES", "ca_ES.UTF-8", "CA-es", "ca_ES@valencia"] {
assert_eq!(lang_from_tag(tag), Some(ca), "{tag}");
}
assert_eq!(first_supported("ca-ES\r\nen-US"), Some(ca));
assert_eq!(tr(ca, "File"), "Fitxer");
assert_eq!(tr(ca, "Save as…"), "Anomena i desa…");
assert_eq!(tr(ca, "Cancel"), "Cancel·la");
assert_eq!(tr_ctx(ca, "comment menu", "Edit"), "Edita");
assert_eq!(tr_ctx(ca, "signature pad", "Type"), "Escriu");
// one (1), other (0, 2+), as in English
assert_eq!((0..=3).map(|n| (ca.0.plural)(n)).collect::<Vec<_>>(), [1, 0, 1, 1]);
assert_eq!(trn(ca, 1, "{n} page", "{n} pages"), "1 pàgina");
assert_eq!(trn(ca, 3, "{n} page", "{n} pages"), "3 pàgines");
let mut app = crate::PdfCraftApp::default();
app.set_option("language", "CA").unwrap();
assert_eq!(app.language, "ca");
let mut restored = crate::PdfCraftApp::default();
restored.restore(&app.persist());
assert_eq!(restored.language, "ca");
assert!(restored.set_option("language", "xx").is_err());
assert_eq!(restored.language, "ca");
}
/// Catalan translates every registered command, menu, All tools group, section and item.
#[test]
fn catalan_covers_commands_and_catalogue() {
let ca = Lang::from_code("ca").expect("ca registered");
for command in pdfcraft_engine::commands::COMMANDS {
assert!(has(ca, command.label), "missing command: {}", command.label);
if let Some(menu) = command.menu {
assert!(has(ca, menu), "missing menu: {menu}");
}
}
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
assert!(has(ca, group.label), "missing group: {}", group.label);
for section in group.sections {
assert!(has(ca, section.title), "missing section: {}", section.title);
for item in section.items {
assert!(has(ca, item.label), "missing item: {}", item.label);
}
}
}
}
/// Catalan translates every `tl!("…")` literal in the UI source, so a new string can't ship in
/// English by accident.
#[test]
fn catalan_covers_ui_literals() {
let ca = Lang::from_code("ca").expect("ca registered");
let literals = ui_literals();
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
let missing: Vec<_> = literals.iter().filter(|label| !has(ca, label)).collect();
assert!(missing.is_empty(), "untranslated Catalan UI literals: {missing:#?}");
}
/// Catalan has every key the other bundled catalogs have (English keys, not their translations).
#[test]
fn catalan_covers_bundled_catalog_keys() {
let ca = Lang::from_code("ca").expect("ca registered");
let mut missing = std::collections::BTreeSet::new();
for reference in LANGUAGES.iter().filter(|language| language.code != "ca") {
let (entries, errors) = catalog::parse_entries(reference.source, reference.plural_forms());
assert!(errors.is_empty(), "{}: invalid reference catalog: {errors:?}", reference.code);
for entry in entries {
let translated = match entry.context.as_str() {
"" => ca.catalog().plain(&entry.source),
"@id" => ca.catalog().id(&entry.source),
"@plural" => entry.source.split_once('|').and_then(|(one, other)| ca.catalog().plural(one, other, 0)),
context => ca.catalog().contextual(context, &entry.source),
};
if translated.is_none() {
missing.insert((entry.context, entry.source));
}
}
}
assert!(missing.is_empty(), "Catalan misses bundled catalog keys: {missing:#?}");
}
#[test]
fn slovene_is_registered_and_persists() {
let sl = Lang::from_code("sl").expect("sl registered");
+2 -1
View File
@@ -290,7 +290,7 @@ fn ui_fonts_work_without_craft_fonts() {
theme::install_fonts(&ctx);
}
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian, Ukrainian, Italian, Slovene) are drawn entirely by the app's own faces
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian, Ukrainian, Italian, Slovene, Catalan) are drawn entirely by the app's own faces
/// (Inter, JetBrains Mono): no letter falls through to egui's defaults or a CJK fallback. (egui's
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
#[test]
@@ -314,6 +314,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
("sl", include_str!("../src/i18n/sl.tsv")),
("lt", include_str!("../src/i18n/lt.tsv")),
("vi", include_str!("../src/i18n/vi.tsv")),
("ca", include_str!("../src/i18n/ca.tsv")),
] {
let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺ");
for line in catalog.lines().filter(|l| !l.starts_with('#')) {
+4 -5
View File
@@ -70,13 +70,11 @@ none has translated help. Vietnamese's **To `full`** hours are therefore residua
| Greek | `el` | 2,341 entries | partial | no |
| Turkish | `tr` | 2,313 entries | partial | yes |
| Lithuanian | `lt` | 2,585 entries | partial; community native-speaker review ([#1401](https://github.com/storytold/pdfcraft/pull/1401)) | no |
| Catalan | `ca` | 2,592 entries | partial: no translated help, engine error details in English; native-speaker review by the contributor | no |
**14 other languages** ship (22 catalogs plus English = **23** interface languages in Preferences).
The entry counts without percentages were measured on 2026-10-12–13; they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review.
**15 other languages** ship (23 catalogs plus English = **24** interface languages in Preferences).
The entry counts without percentages were measured on 2026-10-12–14; they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review.
Acrobat languages PdfCraft lacks: Danish, Dutch, Finnish, Norwegian, Swedish, Hebrew. Vietnamese is shipped here but not by Acrobat.
**14 other languages** ship (21 catalogs plus English, 22 interface languages in Preferences).
The entry counts without percentages were measured on 2026-10-12 (Italian on 2026-10-14); they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review.
Acrobat languages PdfCraft lacks: Danish, Dutch, Finnish, Norwegian, Swedish, Hebrew.
## Revision history
@@ -84,5 +82,6 @@ Acrobat languages PdfCraft lacks: Danish, Dutch, Finnish, Norwegian, Swedish, He
|---|---|---|
| 2026-10-13 | minor | Vietnamese (`vi`) catalog: 2,591 entries (100% of current key union + `theme`/`Appearance`); twelve-language mean ≈ 68%; native-speaker community review; registered with `plural_none` |
| 2026-10-14 | minor | Italian: 2,528 entries after adding the 201 keys the other catalogs had and the code looks up |
| 2026-10-14 | minor | Added Catalan (`ca`, 2,592 entries, measured 2026-10-14); retained existing language assessments |
| 2026-10-12 | minor | Added current counts for Kazakh, Polish, Slovenian, Greek, Turkish and Lithuanian; retained existing language assessments and the original percentage snapshot |
| 2026-10-10 | major | Created. Catalog counts measured from `crates/ui-egui/src/i18n/*.tsv` against the 2,143-key union; Acrobat's localizations from its bundle's `.lproj` and Sequences folder names |
+5 -3
View File
@@ -1,10 +1,10 @@
# Interface language
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **Русский**, **Қазақша**, **Български**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe**, **Lietuvių** or **Tiếng Việt**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **Русский**, **Қазақша**, **Български**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe**, **Lietuvių**, **Tiếng Việt** or **Català**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
Displayed keyboard shortcuts use the same active interface language. For example, German shows `Strg+K` and `Strg+Umschalt+S` in the toolbar, menus, command palette, tooltips and Keyboard shortcuts dialog; macOS keeps its modifier symbols (`⌘`, `⇧`, `⌃`). Key names have their own `key` catalog context, so the Delete key can read `Entf` while the command remains `Löschen`. A missing key-name entry falls back to the English key name. Actual bindings and machine-facing command ids remain stable.
**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one, and Turkish locales (`tr`, `tr_TR`) the Turkish one, and Slovenian locales (`sl`, `sl_SI`) the Slovenian one, and Vietnamese locales (`vi`, `vi_VN`) the Vietnamese one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English.
**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one, and Turkish locales (`tr`, `tr_TR`) the Turkish one, and Slovenian locales (`sl`, `sl_SI`) the Slovenian one, and Vietnamese locales (`vi`, `vi_VN`) the Vietnamese one, and Catalan locales (`ca`, `ca_ES`, `ca_AD`, `ca_ES@valencia`, …) the Catalan one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English.
The control channel exposes the setting through `ui.set`:
@@ -12,7 +12,7 @@ The control channel exposes the setting through `ui.set`:
{"method":"ui.set","params":{"key":"language","value":"ja"}}
```
The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `pl`, `sl`, `el`, `tr`, `lt` or `vi`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `pl`, `sl`, `el`, `tr`, `lt` or `vi`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `pl`, `sl`, `el`, `tr`, `lt`, `vi` or `ca`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `de`, `es`, `fr`, `ru`, `kk`, `bg`, `te`, `hu`, `uk`, `it`, `ar`, `pl`, `sl`, `el`, `tr`, `lt`, `vi` or `ca`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Most shipped catalogs (Japanese, Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese, Arabic, Slovenian, Kazakh, Polish, Greek, Turkish, Lithuanian, Vietnamese and others) cover the current localizable surface (about 2,000–2,600 entries each); Czech still covers mainly the menus. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Document contents, filenames, CLI/MCP/control protocol strings and external help stay outside the catalog. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md).
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Brazilian Portuguese and Arabic translate them (about 2,000–2,140 entries each), and Italian has 2,528 entries; Czech and Turkish cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md).
@@ -24,6 +24,8 @@ The Ukrainian catalog has 2,047 entries, written from the meaning of the English
The Lithuanian catalog has 2,585 entries and three integer plural forms. Calendar headings use standalone month names; dates use genitive forms. Coverage tests check commands/tools, scanned literal and direct lookups, and source/context keys in the other bundled catalogs. Calendar, inspector and password-dialog UI regressions check dynamic labels and preserve document values. A native Lithuanian-speaking contributor has reviewed the translation.
The Catalan catalog has 2,592 entries, written from the meaning of the English labels in standard Catalan computing vocabulary. Commands use the second-person imperative (`Desa`, `Obre`, `Cancel·la`) and notices use impersonal or informal wording. Catalan has two plural forms like English (1 is singular; 0 and every other count take the plural: `1 pàgina`, `3 pàgines`). Every `ca-*` locale, including Valencian (`ca_ES@valencia`), uses this catalog. The interpunct of `l·l` is U+00B7, which the bundled Latin faces cover (tested in `tests/fonts.rs`). The date picker has Catalan month and weekday names; dates are day-first like French (`dddd d mmmm yyyy`), but after a day the month carries its own preposition, elided before a vowel (`7 de març`, `7 d'abril`, `8 d'octubre`), as Czech and Russian change theirs through `months_with_day`; the year takes `\de` from the pattern (`d mmmm \de yyyy`). A native Catalan speaker (the contributor) has reviewed the translation.
The Vietnamese catalog has 2,591 entries and one plural form (`plural_none`). Preferences theme `Appearance` uses context `theme` → `Giao diện`; object/comment/signature appearance stays plain `Diện mạo`. Form fields use `Trường`; redaction apply wording states permanent removal. Calendar months are numeric (`Tháng 1`…`12` / `tháng 1`…`12`). Coverage tests require every current command, UI literal, bundled key and credit label. A native Vietnamese-speaking contributor has reviewed the translation. Status stays `partial`: no translated help, and engine/OS details stay English inside a translated frame.
Edit text reads Hebrew lines in typing order and writes them back in drawing order (`crates/edit/src/bidi.rs`); letters the page's own font lacks are drawn with the craft-fonts `Hebr` face (Noto Sans Hebrew) as Type 3 glyphs, and a build without one says so. Hebrew points (niqqud) can't be written yet in Edit text. Add text (#128) draws Hebrew with the same face through the shaped path Arabic and Thai use: right to left with numbers and Latin in place, points kept on their letter, and typing order for copy and search. The craft-fonts revision pinned for CI and releases has no `Hebr` face yet, so until one is added releases report that error instead of drawing the missing letters, and refuse Hebrew in Add text.
+1
View File
@@ -192,6 +192,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
| 2026-10-14 | minor | M5: reconciled PR #1457 with upstream main, preserving comment submission shortcuts and upstream roadmap progress. Estimates unchanged. |
| 2026-10-13 | minor | M2/M6: images with at most 16 bits of samples a pixel (8-bit Separation/DeviceN, 1-4-bit gray or indexed) convert once per pixel value (hayro-interpret patch 28), so spot-colour form backgrounds draw in milliseconds instead of seconds; widgets without `/P` are placed in one pass; the Fields panel no longer searches the form per row and draws only the rows in view. Estimates unchanged. |
| 2026-10-13 | minor | M3: native-open admission preserves modal/picker ownership; regressions cover queued replies and one-shot picker wakes. Estimates unchanged |
| 2026-10-14 | minor | M14: Catalan interface and date language (2,592-entry catalog, coverage tests). Estimates unchanged |
| 2026-10-12 | minor | M13: CLI check directory identity, with overlap and symlink regressions; estimates unchanged |
| 2026-10-12 | minor | M4: shared Type 3 glyph resources survive extraction, with save/reopen and independent raster regressions; estimates unchanged |
| 2026-10-12 | minor | M2: packed TIFF-predictor RGB row boundaries, with decoding and page-render regressions; estimates unchanged |