* M9: redraw named-layer signature status layers - /N drawing one Form that holds n1 and n3: n1, n3 and n4 redrawn for the state - copies made once per original and state; originals never edited - Lucide icons; captions in Helvetica, embedded Inter, or craft-fonts outlines Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: draw signature states in the display copy, exports and print - marks follow the bytes shown; a changed verdict redraws after a metadata edit - background opens pick up the trust and captions current at commit Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: status captions in the interface language - the three captions in all 22 catalogues - Signatures panel colours from sign::status_rgb Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: signature badge in the widget's corner - a quarter of the shorter side, 48pt cap; outline only under 32pt - dropped for a drawn valid state; bottom-right where the appearance draws one - sign_list reports verdict_shown; parity and roadmap Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: keep Arabic signature captions in logical order Pass raw RTL catalogue text to the signature appearance shaper so UAX #9 is applied exactly once. This keeps the rendered word order and exported /ActualText consistent. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M2: read /ActualText over painted paths and pictures - A replacement that marks no glyph reads where it paints, cut to what is visible and along the way it is drawn. - A span the page content leaves open ends before the annotations. - OCR and scan field detection still count such a page as having no text of its own. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M8: redaction strips /ActualText around paint it clips - A path or shading written again under a clip counts as changed, so a partly redacted word drawn as outlines leaves no name in the file. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: recognise only layer forms that draw n1 and n3 - Settle on the one form holding both first, then read its content once per call. - Skip an /N that is its own layer form; copies keep the layer's /OC. - A verdict counts as drawn when any visible widget shows it. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: keep the signature badge and redraw wherever a state changes - The badge stays for every state; its mark sits bottom-right as the widget lies on the page. - An edit that changes a verdict redraws every page; captions apply before startup files open. - Print, the CLI's render and the repair log follow the display copy. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: restore the attribution usage the rebase dropped - Usage lines for the three icons, Inter and the craft-fonts input; ATTRIBUTION.md rebuilt from them. - Roadmap and localization notes for this round. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: pin signature field names on a nested form Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: bound the signature field walk - kids share their parent's name and type instead of each copying them - a widget stays listed under every field that claims it, as one shared copy - a field written in place in a shared /Kids array is walked once - caps of 20,000 fields, 100,000 entries and 4096-byte names; no states drawn past them Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: keep a document signed when the field walk stops early - list_cached adds a signed row of unknown validity for the fields the walk didn't read - Save As and actions then append, and Optimize, Sanitize and full rewrites are refused, as for any signed file Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M8: removing hidden content no longer strips the names of shadings - With no regions to clip, a shading is left alone instead of wrapped and marked clipped - clean_replacements skips scopes already marked, so deep nesting stays linear Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M2: read a picture's /ActualText over the whole picture - draw_image's transform maps pixels, not a unit square, so the text sat on one pixel in the corner; the test now draws a 2x2 picture - Painted grows in place instead of being copied through an Option Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: cap the bytes of names the signature field walk keeps - A kid's name repeats its parent's, so one long name over 20,000 kids came to 80 MB; the walk now stops at 4 MiB of names and counts the rest as unread - Cover the widget written into a shared /Kids array and the field cap among kids, which no test caught - sig_fields is private again, and the cap docs say what happens past each cap Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: skip status-layer work that changes nothing; fix tests that could not fail - show_status_layers and has_status_layers return before reading the form when nothing is signed, so an unsigned form no longer walks every field on each edit - revalidate and draw_captions leave documents with no drawn state alone, and no longer copy the signature list to update it - Scan form detection reads the page's text once; comment and badge tips share one hover rule - Tests: drop a table row that repeated another, stop taking an expectation from the code under test, stop comparing an Arc with itself, give bare asserts messages Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: test the signature field name cap at the engine boundary - A 4,096-byte name is read and blocks a full rewrite; at 4,097 bytes the list holds the unread-fields row and a full rewrite is still refused Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: test signature states beside fields the viewer draws - One display copy holds both a field's drawn appearance and the signature's state. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: draw Thai and Devanagari status captions in their own script - script_of names Thai and Devanagari, so a craft-fonts face meant for either draws the caption as outlines; without one it stays English. - Localization, parity and the craft-fonts usage line list the new scripts. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M2: paint left out by an exclusion mixes its /ActualText replacement - A replacement placed by outlines or pictures, some of them excluded, fails filtered extraction as one with excluded glyphs does. - Glyphs still place a replacement that has them. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: a long-named field that can't hold a signature leaves the walk whole - A leaf of another type passed over for its name is no signature field and has none below it, so an unsigned form stays unsigned. - A signature field, or any field with kids, still counts as unread. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: status captions use document faces only - Each script takes the face PdfCraft writes it into documents with, so Chinese stays English although the pinned craft-fonts' interface-only Chinese face is meant for Hani. - The craft-fonts usage line names Devanagari: the pinned revision has a face for it, and Hindi captions are drawn in it. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: refuse a new signature field the field walk can't read back - Signing adds a field only when the walk reads the whole form with it: past a cap the signature would be listed as unread fields, and its name could repeat an unread field's. - A field the walk already reads is signed as before. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: refuse a new signature field with a blank name - The walk lists no field without a name, so signing now also requires the walk to list the new field: one named only with spaces or NULs would be a signature nothing shows. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: an unnamed signed field or an unreadable form counts as unread - A signed field with no usable /T, or an /AcroForm or /Fields of the wrong type, cuts the field walk short: the document keeps its unread-fields row and stays signed. - Signing refuses a blank new field name before it adds the field, so the refusal still says why. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: no display copy when no signature state is drawn - display_bytes_for makes a copy only when it added an appearance or drew a state, as the app does. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: the verdict mark stays on the page - A widget past the crop box takes its mark in the part on the page, or none when too little of it is there. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: typed values stay drawn while a signature redraws every page - A form edit that changes a signature's state puts its values back in unbaked after the pages are invalidated. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: the action, Save As and shading tests check their effects - The signed action and Save As tests reopen the output for the title and the comment; the shading test checks its clip. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: values typed earlier stay drawn while a signature redraws every page - A form edit that changes a signature's state keeps every value still waiting on its page, not only its own. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: comment and content edits keep typed values drawn too - An edit to one page's comments or content that changes a signature's state moves no page, so the values still waiting on their pages stay pending, as after a form edit. - The pending-value test runs a field deletion and a comment after a fill, under a certification that allows the fill; its messages say what it checks. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: the language-change test skips without catalogs - Like the other language tests, it returns early when the build has no craft-i18n catalogs. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: signed fields behind kids without a name count - A kid with no /T but kids of its own is walked as a field node, so a signed field under it is listed by its full name. - An unnamed kid claiming a signature its field doesn't have cuts the walk short, so the document stays signed and keeps its unread-fields row. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> --------- Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
25 KiB
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ų, Tiếng Việt, Suomi, Shqip, Català, Română or Norsk bokmål. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. The status caption PdfCraft draws into a named-layer signature appearance ("Signature valid" and its siblings) follows it too, on screen, in exports and in print; it is never saved. It is drawn in Helvetica, or shaped in embedded Inter, or shaped in a craft-fonts face for Arabic, Japanese, Telugu, Thai and Devanagari when the build has one (drawn as outlines), and otherwise in English, as Chinese always is; saving never writes it.
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, Vietnamese locales (vi, vi_VN) the Vietnamese one, Albanian locales (sq, sq_AL, sq_XK, …) the Albanian one, Catalan locales (ca, ca_ES, ca_AD, ca_ES@valencia, …) the Catalan one, and Romanian locales (ro, ro_RO, ro-RO, ro_MD) the Romanian one, and Persian locales (fa, fa_IR, fa_IR.UTF-8, fa-AF, ...) the Persian one. Norwegian Bokmål locales (nb, nb_NO) use the Bokmål catalog, and so do general Norwegian (no, no_NO) and Nynorsk (nn, nn_NO), which have no catalog of their own. 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:
{"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, ckb, pl, sl, el, tr, lt, vi, fi, sq, ca, ro, fa or nb), 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, ckb, pl, sl, el, tr, lt, vi, fi, sq, ca, ro, fa or nb). 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 carry most of the current localizable surface (roughly 2,400–2,800 entries each); Czech (2,794 entries) has all but four keys, and Finnish, Vietnamese, Catalan, Brazilian Portuguese, Spanish, Lithuanian and Romanian have all 2,798. Persian has 2,785. 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.
Whole sentences that the layers below word — what the accessibility checker found, what the PDF/A verifier reports, what a signature's details say, what is wrong with the pages to print, what an edit is called in the history — are translated too, because those layers hand the sentence over apart from the document's own words. See Sentences the engine words.
The Ukrainian catalog has 2,530 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three CLDR Ukrainian integer forms: 1 сторінка, 2 сторінки, 5 сторінок; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (9 жовтня 2026), while headings use standalone names (Жовтень 2026). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover.
The Lithuanian catalog has 2,798 entries and three integer plural forms. Calendar headings use standalone month names; dates use genitive forms. Calendar, inspector and password-dialog UI regressions check dynamic labels and preserve document values. A native Lithuanian-speaking contributor has reviewed the translation. The Scan & OCR review strings added afterwards (M10: the OCR Verify screen, the batch options and the engine choice) were written without native review.
The Vietnamese catalog has 2,798 entries and one plural form (plural_none), covering the current catalog-key union. 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). 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.
The Finnish catalog has 2,798 entries: every key of the other bundled catalogs, written from the English labels in ordinary Finnish computing vocabulary and sentence case. Finnish has the English one/other plural rule, with the partitive singular after any number but 1 (1 sivu, 2 sivua, 0 sivua). Calendar headings use the nominative month (Lokakuu 2026); dates use the partitive (14. lokakuuta 2026), and Finnish is also a date language for month and weekday names. Any fi-* locale selects it; sv-FI keeps English until PdfCraft has a Swedish catalog. The bundled Latin faces cover ä ö å š ž. The translation is AI-assisted (Claude Code) and has not yet been proofread line by line by a native speaker.
The Catalan catalog has 2,798 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/it/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 Romanian catalog (ro, Română) has 2,798 entries and three plural forms (plural_ro, CLDR ro): one for 1, few for 0 and counts whose last two digits are 1–19 ("2 pagini", "101 pagini"), other for the rest, written with "de" ("20 de pagini"). Commands use the noun form common in Romanian software ("Salvare", "Deschidere…"); messages address the user formally. Redaction is "Anonimizare" ("redactare" means editing in Romanian), a watermark "Filigran". Calendar month names are lowercase inside dates ({day} {month} {y}). The bundled Inter faces cover ă, â, î, ș, ț (comma below). Not yet reviewed by a second native speaker; status partial: no translated help, and engine/OS details stay English inside a translated frame.
The Norwegian Bokmål catalog has 2,707 entries, all but 91 of the current key union, with the English one/other plural rule, Norwegian key names (Mellomrom; Shift keeps its name) and month and weekday names for Fill & Sign dates. It was written from the meaning of the English labels; a native-speaking contributor reviewed its terminology (key names, Opacity, Redact, Highlight, Fill & Sign), not yet every line.
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 includes the Hebr face, so releases draw the missing letters and accept Hebrew in Add text.
Japanese interface text uses BIZ UDPGothic from craft-fonts, an optional build input that every release includes (CRAFT_FONTS_DIR; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese uses the pinned craft-fonts input too. On native builds, fonts/simplified-chinese-manifest.txt adds LXGW WenKai Lite as a Hans face (at most 16 MiB, never embedded on the web); Simplified Chinese puts the Hans faces first, so a line never mixes faces with different baselines. Traditional Chinese has no embedded Hant face: the desktop app places a Traditional Chinese face already installed on the machine (Microsoft JhengHei on Windows, PingFang TC on macOS, a Noto CJK TC or WenQuanYi Zen Hei on Linux) before the Japanese faces, even when LXGW is bundled, because a Simplified-only face cannot supply Traditional-only characters. Without an embedded Hans face, Simplified Chinese likewise uses an installed Simplified Chinese face (Microsoft YaHei, PingFang SC, …) placed before Japanese (#826). The web build has no installed fonts to fall back on, so it shows a replacement box for characters the Japanese face lacks. Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian, Italian, Slovenian, Turkish, Lithuanian, Norwegian, Finnish, Albanian, Catalan and Romanian need only the bundled Latin faces (tested in tests/it/fonts.rs). Slovenian has four plural forms: one (1, 101, 201…), two (2, 102…), few (3, 4, 103…) and other (everything else, including 0, 5–20 and 21+), so @plural entries list four forms (1 stran, 2 strani, 3 strani, 5 strani). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (1 oldal, 2 oldal). A build supplied with a craft-fonts Telu face draws Telugu and retains that face in the web payload; egui shapes its conjuncts. The release-pinned craft-fonts input has no Telugu face, so its Telugu labels show replacement boxes.
The Brazilian Portuguese catalog has 2,798 entries and covers the current union of catalog keys. It began with the ~1,826-entry gist André Cardoso posted in #288, with labels added since and the existing wording reviewed. Portuguese, like French, treats 0 and 1 as singular (1 página, 2 páginas).
The Albanian catalog (craft-i18n locales/sq/, interface name Shqip) is written in Standard Albanian with the imperative for commands (Ruaj, Hap, Fshi). Albanian uses two plural forms like English: 1 is singular and every other count, including 0, is plural (1 faqe, 2 faqe). Month and weekday names are lowercase (mars, e shtunë), and the letters ë and ç are covered by the bundled Latin faces. It has not been reviewed by a native Albanian speaker yet.
Arabic and right-to-left text
The Arabic catalog has 2,783 entries in Modern Standard Arabic with Western digits. Counts use the six CLDR Arabic forms (0, 1, 2, 3–10, 11–99, the rest), and dates use the Arabic names of the Gregorian months.
egui 0.37's epaint shapes every line with the Unicode bidirectional algorithm (UAX #9). Catalog entries stay in logical (typing) order and are never preordered: tl!, tr and trn return logical text, bidi::display_rtl and bidi::visual exist only for the tests that prove the old preordering is gone, and epaint joins and reorders Arabic on every line. Interface labels use an explicit TextDirection::RightToLeft in an Arabic, Kurdish or Persian interface, so a label whose first strong character is Latin or a {placeholder} still reads right to left. File and path labels use TextDirection::LeftToRight, so an Arabic file name keeps .pdf at its visual end; document-authored text uses Auto. Stock chrome is mirrored through Style::right_to_left (menus, combo boxes, scroll bars, headers), and i18n::fmt inserts values in one pass. The command search matches a query of several Arabic words word by word.
Wrapped paragraphs break between words in logical order and epaint reorders each resulting line, so a wrapped label reads in the right direction from the first line down. Text typed into a field is shaped and reordered while editing, and the stored value stays logical.
Central Kurdish (Sorani, ckb) takes the same path: its catalog is written in logical order and rtl: true selects the explicit right-to-left direction; epaint reorders it, so it behaves as Arabic does above. Counts use the two CLDR ckb forms (one, other); Sorani keeps the noun singular after a numeral, so the two @plural forms usually read the same. Dates use the Kurdish names of the Gregorian months. The Arab face (Noto Sans Arabic) covers the Kurdish-only letters (ێ ڵ ڕ ۆ ە ڤ ژ چ گ پ).
The Persian catalog (craft-i18n locales/fa/, interface name فارسی) has 2,785 entries, covering 2,785 of the current 2,798-key union; the 13 keys added after it was written still show English. It is written in contemporary software Persian from the meaning of the English labels. Counts use the two CLDR fa forms (one for 0 and 1, other for every other count), so @plural entries list two forms. It is right to left like Arabic and Kurdish: rtl: true selects the explicit right-to-left direction, the entries stay in logical order and epaint reorders each line, and the stock chrome is mirrored as it is for Arabic. Compound words use the zero-width non-joiner (نیمفاصله, U+200C), the Persian comma ، and question mark ؟, and Persian ی/ک rather than the Arabic ي/ك. Dates use the Persian names of the Gregorian months. The Arab face (Noto Sans Arabic) covers Persian letters, so it needs no Japanese face. The translation is AI-assisted and has not yet been proofread line by line by a native speaker; status partial: no translated help, and engine/OS error details stay English inside a translated frame.
Arabic glyphs come from the craft-fonts Arab face (Noto Sans Arabic, kept in the web build too). A build without craft-fonts shows Arabic as replacement boxes on the web; on desktop it falls back to one installed font (below). The craft-fonts revision pinned for CI and releases (.github/workflows/) has the Arabic and Hebrew faces. Its Noto Sans CJK SC face is 16.4 MB and is left out by the general CJK cap; native builds take the separately pinned Simplified Chinese face (LXGW WenKai Lite) from fonts/simplified-chinese-manifest.txt, and the web build embeds only BIZ UDPGothic Regular plus the small Arabic, Hebrew, Thai and Devanagari faces.
File names and document titles use the craft-fonts Arab face (Noto Sans Arabic) for Arabic and the Hebr face (Noto Sans Hebrew) for Hebrew when those faces are included in the build input; on desktop, a character no embedded face has is drawn with one font already installed on the machine (PDFCRAFT_SYSTEM_FONTS=0 turns this off). epaint reorders each line with UAX #9, while file and path labels keep an explicit left-to-right base so the extension stays at the visual end; the tab strip, the recent-files list and Properties show such names in display order.
Catalog coverage does not guarantee glyph coverage. Native builds embed LXGW WenKai Lite as the Simplified Chinese face from the pinned craft-fonts input (at most 16 MiB), while the web build embeds only BIZ UDPGothic Regular plus the small Arabic, Hebrew, Thai and Devanagari faces, so Chinese characters none of those faces has show replacement boxes on the web. Traditional Chinese has no embedded face and uses a Hant face installed on the machine, placed before the Japanese faces even when LXGW is bundled.
German labels keep nouns capitalized, including tool names inserted into notices. The certificate viewer uses the certificate context for Subject (the certificate holder), while document properties keep the plain translation (the topic). Search hints reserve room for the shortcut, and Home cards limit their title and description to the available rows; hovering shows the full text when it is shortened.
How translations work
PdfCraft reads its translations from craft-i18n, shared by the Crafting Apps, through the craft-i18n crate (crates/ui-egui/src/i18n.rs is a thin wrapper):
- Strings in the code stay in English and serve as lookup keys. At draw time,
tl!("Save")returns the text in the current language, and falls back to English when there is no entry. - Each language has two catalogs in craft-i18n:
locales/<code>/common.tsv(rows every app shares) andlocales/<code>/pdfcraft.tsv(PdfCraft's own rows, which win over shared ones). One entry per line:context<TAB>English<TAB>translation. An empty context marks a plain string.@idkeys an entry by command id (for examplefile.saveAs); PdfCraft reads@idrows only frompdfcraft.tsv.@pluralholds plural forms, chosen by CLDR rules. Any other context disambiguates an English word with several meanings (tl_ctx!in UI code). craft-i18n'sdocs/integration.mddocuments the format and escapes. - The build embeds the catalogs from
CRAFT_I18N_DIR, a craft-i18n checkout at the commit incraft-i18n.rev(CI and the flatpak manifests check it out there). Unset, the build is English only;CRAFT_I18N_REQUIRED=1(release builds) makes that an error. Tests that need the catalogs say they are skipped. - Placeholders (
{name}) must appear in both columns. A trailing…must be kept.i18n::fmtfills placeholders in one pass, so a value such as a file name containing{n}is inserted as written. - A missing entry never fails a build or a test: that string shows in English.
cargo xtask i18n-report, a step ofcargo xtask ci, prints for each language how many strings it has no translation for and the first few. It counts every command, All tools label, literal passed totl!ori18n::tin the UI source, engine wording (below) and key of the embedded PdfCraft catalogs. - craft-i18n validates rows strictly (unknown escapes, unknown
@contexts, placeholder or ellipsis mismatches, wrong plural form counts, duplicates). At run time a bad row is skipped (and logged) and that string shows in English;bundled_catalogs_are_consistentprints them. ATTRIBUTION.tomlrecords the translations as one[[build_input]]; craft-i18n'sATTRIBUTION.mdcredits the translators.- Translations are clean-room: write them from the meaning of the English text, never from another product's string tables.
Sentences the engine words
Some sentences are not the interface's own. The accessibility checker, the PDF/A verifier, the signature reader, the page-range parser and the edit history all word their own findings, and matching that wording in the UI with a tl! literal would break silently whenever the engine changed it. So those layers hand the wording over instead, apart from the document's own words:
- Each of them has a
WORDINGSconstant listing every sentence it can say, as&'static strwith holes in it ({n},{p},{}). The sentence is written once, in the layer that means it. said(&words)says one of them: the layer looks its own wording up through the closure it is given, then fills the holes with the document's values — a page number, a font name, what the person typed — in one pass, so a value containing{n}is inserted as written.- The UI passes
&crate::i18n::words, which is a catalog lookup, so the sentence reads in the interface language.in_englishis the same thing in English, and a wording with no entry comes back as it stands, which is the English — a catalog missing one shows an English sentence rather than a gap. - The lists are
a11y::{WORDINGS, REPORT_WORDS},preflight::WORDINGS,sign::pdf::{WORDINGS, kind::ALL},print::WORDINGSand the engine's{EDIT_LABELS, BATCH_LABELS}.a11y::report_htmltakes a language tag and awordsclosure, so a saved report reads the way the panel read, down to itslangattribute. - The translation gaps report (
cargo xtask i18n-report) counts every wording in those lists, so a catalog that lacks one shows there; until it is added, that sentence shows in English. Catalog validation requires each translation to keep the same holes, andspanish_keeps_the_holes_in_what_the_engine_sayschecks the Spanish ones against the lists themselves.
Error details that come from the operating system, and faults rather than mistakes (a file that cannot be read, a lock that was poisoned), still carry their own text and are shown as they are.
Adding a language
- In craft-i18n, add
locales/xx/pdfcraft.tsvwith its@id language.namerow, and translate entries. - Add one row to
LANGUAGESincrates/ui-egui/src/i18n.rs(code and whether it is right-to-left), one toenglish_name, and one toDATE_LANGUAGESincrates/engine/src/dates.rs. - Bump
craft-i18n.rev(and thecommit:of the craft-i18n source in both flatpak manifests) to a craft-i18n commit with the catalog.
The Preferences dropdown, the system-language match and the translation gaps report then pick it up.
The egui 0.37 migration pins craft-fonts b076417b5880bd4154bd22f00695b1d28c6bea83. That manifest supplies Arabic, Hebrew, Thai and Devanagari faces selected for the web payload, but contains no Telugu face. The build code accepts a Telu row when a future input provides one; it does not establish Telugu glyph coverage with the current pin.