Skip to content

i18n: let the layers below hand their sentences over, so they can be translated - #478

Merged
echelon merged 7 commits into
storytold:mainfrom
feraremag2680:feat/translatable-engine-sentences
Oct 11, 2026
Merged

echelon merged 7 commits into
storytold:mainfrom
feraremag2680:feat/translatable-engine-sentences

Conversation

@feraremag2680

Copy link
Copy Markdown
Contributor

What this is

The interface is translated into ten languages, but a good part of what a person actually reads in the Accessibility panel, the PDF/A list, the signature details, the print dialog and the edit history was still English in every one of them. Those sentences are worded by the layers below the UI, and notify_error says why they were left alone:

Engine and OS messages are not translated: matching their English wording would break silently whenever it changes.

That is exactly right, so this doesn't try to match the wording from the outside. It has each layer hand its own wording over, apart from the document's own words, and lets the UI look it up.

The arrangement

Each layer that words sentences gains three things, in its own vocabulary:

  • pub const WORDINGS: &[&str] — every sentence it can say, written once, with holes in it ({n}, {p}, {}).
  • said(&self, words: Words) -> String — says one of them: look the wording up through the closure, then fill the holes with the document's values in a single pass, so a file name containing {n} goes in as written.
  • pub fn in_english(wording: &str) -> String — the closure that changes nothing, for the CLI, the automation layer and the tests.

The UI passes &crate::i18n::words, which is a catalog lookup. A wording with no catalog entry comes back as it stands — which is the English — so a catalog that is missing one shows an English sentence rather than a gap, and the nine catalogs this PR doesn't touch keep reading exactly as they do today.

Because the sentence is now data the layer publishes rather than a literal duplicated in the UI, it cannot drift: change the wording and the catalog entry stops matching, which is what the tests check.

What changed

Engine side. a11y::Finding, preflight::Issue, sign::pdf::Detail and print::PrintError carried pre-formatted English Strings; they now carry the document's values and name their wording. Edit::label() likewise returns a Label { wording, value }, listed in EDIT_LABELS / BATCH_LABELS. The lists are a11y::{WORDINGS, REPORT_WORDS}, preflight::WORDINGS, sign::pdf::{WORDINGS, kind::ALL}, print::WORDINGS and the engine's two label lists.

a11y::report_html takes a language tag and a words closure, so a saved report reads the way the panel read, down to its lang attribute.

print::PrintError now has a variant per thing a person can be wrong about (NoPages, OutOfRange, NotAPage), with the numbers apart from the words; select_listed used to format that same sentence into Invalid. Faults rather than mistakes — a file that cannot be read, a poisoned lock — keep their own text, and said passes them through.

UI side. a11y_ui.rs (findings and the saved report), standards_ui.rs, sign_ui.rs and print_ui.rs call said(&crate::i18n::words). No new tl! literal is introduced anywhere, so {simplified_chinese,french,russian}_covers_ui_literals are unaffected (a closure argument is not a literal).

Spanish. 107 entries added to es.tsv, covering all of the lists above, with its SHA-256 updated in ATTRIBUTION.toml.

Tests

  • spanish_covers_what_the_engine_says — one catalog entry per wording in all eight lists, so the catalog cannot fall behind the engine without a test saying so.
  • spanish_keeps_the_holes_in_what_the_engine_says — each translation keeps the same holes as the English, named ones included.
  • print::tests::wordings_are_said — said(&in_english) == to_string() for every mistake, one wording per mistake, a fault's text is never translated, and a value containing {count} is not re-read as a hole.
  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo xtask layers, cargo xtask assets, cargo xtask parity all pass.

Two unrelated, pre-existing failures show up on my machine (Windows, system language Spanish), both of them also on an unmodified checkout:

  • i18n::tests::windows_display_language_query_returns_a_locale_tag — windows_ui_language() returns the preferred-language list joined with \n, so the assertion fails on any Windows with two or more display languages. Described in sign: read the .p12 files Windows writes (BER, not DER) #453.
  • tests/accessibility.rs (both tests) — the kittest harness leaves the language on Auto, so on a machine whose system language has a catalog the shell is drawn translated and get_by_label("Accessibility Checker Options") finds nothing. LC_ALL=en_US.UTF-8 cargo test -p pdfcraft-ui-egui passes everything. Setting the language explicitly in the harness would fix it; I left it alone to keep this PR to one thing, and I'm happy to send it separately.

With the language pinned to English, cargo test --workspace is green apart from the first item above.

Adding another language

Translate the entries; nothing else is needed. docs/localization.md has a new section, Sentences the engine words, describing the arrangement, and the per-language gate is three lines if you want one.

🤖 Generated with Claude Code

…translated

The accessibility findings, the PDF/A problems, the signature details, the
page-range mistakes and the edit labels were English in all ten languages,
because matching their wording from the UI with a tl! literal would break
silently whenever the engine changed it.

So each layer now hands the wording over instead, apart from the document's
own words: a WORDINGS list of every sentence it can say, with holes in it, and
said(&words), which looks the wording up through the closure it is given and
fills the holes in a single pass. in_english changes nothing, for the CLI and
the tests; the UI passes &i18n::words, a catalog lookup. A wording with no
entry comes back as it stands, which is the English, so the nine catalogs this
does not touch read exactly as before.

a11y::report_html takes a language tag and a words closure, so a saved report
reads the way the panel read, down to its lang attribute. print::PrintError
gained a variant per thing a person can be wrong about, with the numbers apart
from the words; faults rather than mistakes keep their own text.

Spanish translates all of it (107 entries). spanish_covers_what_the_engine_says
requires an entry per wording in the eight lists and
spanish_keeps_the_holes_in_what_the_engine_says requires the holes to survive
the translation, so the catalog cannot fall behind the engine unnoticed.

No new tl! literal, so the Chinese, French and Russian UI-literal gates are
unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fernando Areán and others added 2 commits October 9, 2026 17:41
Three list files overlapped, and both sides are kept in each:

- `ATTRIBUTION.toml`: the Spanish catalog's SHA-256, recomputed over the
  merged `es.tsv` (`cargo xtask assets` is happy with it).
- `docs/localization.md`: main's font paragraph, which now names Bulgarian
  and German, with the paragraph about the sentences the engine words.
- `parity/acrobat-features.toml`, `misc.prefs-language`: main's notes and
  evidence, plus this branch's three tests and its paragraph; what is
  missing is the engine's sentences in the other ten catalogs, German
  included, so the count moved from nine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The signature code moved under us: `main` now validates adbe.x509.rsa_sha1,
notes what it read leniently on the way (BER lengths, segmented OCTET STRINGs,
RSA and ECDSA quirks), and says why a certificate that matched the issuer was
not used to build the chain. All of it arrived as `String`s built in place.

Every new sentence is now a wording the interface can say in its own language:

- `x509::signing_problem` and `x509::build_chain_noted` return `Detail`
  instead of `String`. The three chain refusals became whole sentences rather
  than a phrase stitched into one, so a language can put the two certificate
  names where it needs them; the English they render is unchanged.
- `Fill::Names` carries those two names. `Detail::said` cuts both holes out of
  the wording before filling either, so neither name can be taken for the
  other's hole.
- The lenient-reading notes and the weak-hash warnings live in `WORDINGS`,
  which `wordings_are_complete` now also reads out of `x509.rs`, `cms.rs`,
  `keys.rs` and `rsa_pad.rs` — the test keeps the list in step with the code.
- `change_kind` keeps main's arms, with our `kind::*` constants in place of the
  bare strings.
- The Spanish catalog gains the 19 new sentences and loses the one about a
  format PdfCraft does not check yet, which it now does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@feraremag2680

Copy link
Copy Markdown
Contributor Author

Noticed while merging main into this branch, and left alone so this PR stays about the sentences: docs/localization.md on main (line numbers below are main's) carries two paragraphs twice, each in an older and a newer form, which looks like a merge artifact rather than intent.

  • Lines 15 and 16 both begin "Dialogs, panels, menus, notices and history labels go through the catalog (tl!)". Line 15 lists Italian and Arabic among the translations and says Czech and Brazilian Portuguese cover the menus; line 16 is the earlier version, with Brazilian Portuguese still among the translations and only Czech covering the menus.
  • Lines 20 and 23 both begin "Japanese interface text uses BIZ UDPGothic from [craft-fonts]". The second one's list of languages that need only the bundled Latin faces is missing Italian.

Happy to send a separate PR deleting the two stale copies if that is useful.

…moved

Conflicts, each resolved so both sides survive:

- crates/a11y/src/structure.rs: main (storytold#926) rewrote the structure rules as
  a streaming visitor. The branch's wordings are ported onto it: the
  `{ty} element[ on page {p}]: {what}` frames via Finding::nested, the
  IRREGULAR wording with {min}/{max}, and the two heading sentences.
- crates/engine/src/lib.rs: the branch's Label arms, plus main's new
  FillAnnotation (comment properties) and MoveObjects ("Move object" /
  "Move objects", added to EDIT_LABELS; es.tsv already has them).
- crates/ui-egui/src/editing.rs: main's move_error message with the
  branch's edit_label.
- es.tsv, parity/acrobat-features.toml: both sides' entries; the parity
  entry has main's shortcut evidence and sentence plus the branch's.
- ATTRIBUTION.toml: main's, with es.tsv's sha256 recomputed.
- engine tests: main's new MoveObjects undo assertion compares as_str().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@echelon
echelon merged commit 513a1ec into storytold:main Oct 11, 2026
1 of 5 checks passed
@echelon

echelon commented Oct 11, 2026

Copy link
Copy Markdown
Member

Thank you, @feraremag2680! This has landed on main: "i18n: let the layers below hand their sentences over, so they can be translated". I merged main: the structure-tree rules had become a streaming visitor (#926), so your wordings were ported onto it, and the signature verifier now uses your kind::* constants with main's newer, stricter classification (#1099). Only Spanish translates the engine's sentences so far; other catalogs fall back to English.

Please join us on Discord if you haven't already: https://discord.gg/artcraft

jcl-bit added a commit to jcl-bit/printcraft that referenced this pull request Oct 11, 2026
Only ROADMAP.md conflicted (both sides added a progress-log line); both
lines are kept. storytold#478's translatable print errors leave spooler faults as
their own text, so the native path's messages are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@feraremag2680

Copy link
Copy Markdown
Contributor Author

Sent as #1778, for the catalog paragraph only. The font paragraph below it is duplicated three times rather than twice, and #1484 already folds those into one, so I left them to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants