i18n: Simplified Chinese UI language option (#112)

* i18n: Czech interface language

Adds Czech (Čeština, code `cs`) next to English and Japanese:
Preferences ▸ Interface language, `ui.set language cs`, `ui.state`
and persisted preferences all accept it.

- i18n: `Language::Cs`; `tr()` picks the language's table with a match
  (English has none), panic-free. The CZECH table (83 entries) covers
  every label passed to `.tr("…")` (main menu, View zoom/theme/side-panel
  entries, Preferences dialog), every registered File/Edit/Pages/View/Help
  command shown by `registry_menu`, and every key of the Japanese table.
  Written clean-room from the English meaning, not from any Acrobat
  localisation.
- Tests: Czech covers every registered menu title and label, every
  `.tr("…")` literal found by scanning the crate's sources, and the
  Japanese keys; nothing is left identical except an explicit KEEP_AS_IS
  list ("OK"); ellipses match the source; keys are unique per language;
  `cs` round-trips through set_option, ui.set/ui.state and persistence.
- theme: the font setup is split into `font_definitions()`, and a test
  checks with skrifa (dev-dependency, already in the tree via egui) that
  Inter Regular/Medium/SemiBold and JetBrains Mono draw all Czech letters
  and every Czech translation, so nothing falls back to Shippori Mincho.
- The `language` error lists `cs`; docs/localization.md, the parity note
  for misc.prefs-language and the ROADMAP log mention Czech.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* M14: translation catalogs, same system as PhotoCraft

Move the interface translations to PhotoCraft's i18n system: one TSV
catalog per language (crates/ui-egui/src/i18n/ja.tsv), the tl! macro,
entries keyed by command id, context or plural forms, and catalog tests
(format, duplicates, placeholders, command ids).

The 49 existing Japanese strings move over unchanged, plus "Auto". The
language preference now defaults to "auto" and follows the system
language (English when it has no catalog); "en" and "ja" still persist
and unknown values are rejected.

Update docs/localization.md, ROADMAP.md and the misc.prefs-language
parity entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* M14: add Traditional Chinese (Taiwan) UI localization

Co-Authored-By: Codex <noreply@openai.com>

* M14: translate shortcuts and expose language preferences in Edit menu

Add command-comma and verify language switching through the actual selector.

Co-authored-by: Codex <noreply@openai.com>

* M14: expand Traditional Chinese dialogs, panels and diagnostics

Preserve opaque values in translated templates and keep application diagnostics at the UI boundary. Extend native control and template regression coverage.

Co-authored-by: Codex <noreply@openai.com>

* M14: finish Traditional Chinese signature prompts and history labels

Translate generated action labels without changing filenames or field names. Complete checkbox styles, resolution labels and native picker filters.

Co-authored-by: Codex <noreply@openai.com>

* M14: complete Japanese dialogs, panels, shortcuts and diagnostics

Expand Japanese from 49 to 1830 interface entries, expose language preferences in the Edit menu, preserve opaque template and history values, and localize known application diagnostics only at the UI boundary. Add control, persistence, template and desktop/web glyph regressions.

Co-authored-by: Codex <noreply@openai.com>

* i18n: Simplified Chinese UI language option

Adds Language::Zh alongside en/ja (--language zh, preferences switch,
persisted): full Chinese translation table covering every user-visible
label, generic Hans font fallback plumbing, and a build.rs skip-guard
plus test so Noto CJK is never embedded (AGENTS.md 1.1). Pairs with
storytold/craft-fonts#5 (ChillRoundF Regular, OFL-1.1 Adobe-free
chain), which supplies the Hans glyphs. Also fixes a startup deadlock
from translating inside lazy widget_info closures.

* i18n: add Brazilian Portuguese (pt-br) UI translation

Adds Language::PtBr (code pt-br, endonym Português (Brasil)) with serde rename so it persists as "pt-br", registers it in Language::ALL (Preferences ▸ Interface language and ui.set/ui.state) and translates the same 49 menu/command keys as the Japanese table. Extends the i18n tests (table parity, codes, persistence) and updates docs/localization.md.

* M14: strict catalog validation, one-pass fmt, translation assets

Fold in the infrastructure parts of #175:
- `fmt` fills placeholders in one pass, so inserted values (a file name
  containing `{n}`) are never substituted again.
- Catalog validation rejects unknown escapes, unknown `@` contexts,
  placeholder, ellipsis and plural-form mismatches and duplicates. The
  bundled catalogs must have no errors (tests); at run time a bad line is
  still only skipped and logged, so the rest of the catalog loads.
- `.tsv` files count as assets in `cargo xtask assets`, with a
  `translation` kind that may never come from Adobe; ja.tsv gets its
  ATTRIBUTION.toml entry.
- Control-channel test: switching the language keeps documents, their
  dirty state and command ids.

Also make the drawing language thread-local, as in PhotoCraft, so
parallel kittest harnesses can't change each other's language.

Co-Authored-By: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M14: Brazilian Portuguese as a TSV catalog

Move #165's Portuguese table into crates/ui-egui/src/i18n/pt-br.tsv
(same 49 strings, plus Auto) and register it in LANGUAGES with
PhotoCraft's Portuguese plural rule (0 and 1 singular). Any Portuguese
locale (pt_BR, pt_PT, pt) follows the Brazilian catalog, as in
PhotoCraft. The catalog is an attributed asset (ATTRIBUTION.toml), and
the docs and parity notes list the new code.

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

* M14: Czech as a TSV catalog

Move #68's Czech table into crates/ui-egui/src/i18n/cs.tsv (PrintCraft
labels renamed PdfCraft, plus Auto) and register it with PhotoCraft's
Czech plural rule (one / few / other). #68's tests become catalog
tests: Czech covers every registered menu title and menu command label,
keeps ellipses, uses no three-dot ellipsis or stray whitespace, and only
leaves "OK" untranslated; the code persists and `cz` is rejected. The
source-scanning `.tr("…")` test is dropped: strings now go through tl!
and the catalog tests check consistency.

The font-coverage test moves to tests/fonts.rs and covers both Latin
catalogs (cs, pt-br) against Inter and JetBrains Mono. It keeps skrifa
as a dev-dependency (the version pdfcraft-fonts already uses): egui's
has_glyphs can't answer it, since a family with only the primary face
also uses that face as its replacement face.

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

* M14: route every UI string through the catalog; Japanese as ja.tsv

Port #141's wiring to the catalog API from land/106:
- `crate::i18n::tr(ui, s)` becomes `tl!(s)`, `tr_fmt`/`tr_template`
  become `i18n::fmt(tl!(..), ..)`, and `Language::get`/`store` (the
  egui temp-data copy of the language) are gone: the drawing language
  is the per-thread `i18n::current()`, set from the preference at the
  start of each frame. Worker threads (export, OCR, batch actions) take
  the UI's language with them for their summaries.
- ja.rs (1,830 pairs) becomes ja.tsv, merged with the existing entries
  and renamed PrintCraft -> PdfCraft. The 29 diagnostic templates of
  errors.rs are dropped.
- `Language::diagnostic` and errors.rs are dropped: matching engine and
  OS messages against English templates breaks silently whenever the
  wording changes. Error details are shown as they are, inside the
  translated frame ("{label} failed: {e}").
- History labels (`command_label`/`action_label`) move into i18n and keep
  captured file and field names; registry menus prefer `@id` entries.
- Preferences: Auto plus every registered language, relabelled at once.
- Tests: Japanese covers every command and All tools entry; Japanese
  labels have glyphs with the desktop faces and with the web subset
  (with craft-fonts); history labels; #141's control tests, with the
  error-detail expectation updated.

Co-Authored-By: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M14: Preferences in the Edit menu, Cmd/Ctrl-comma

Split out of #141: register app.preferences in the Edit menu with the
platform's usual shortcut (Command-comma on macOS, Ctrl-comma
elsewhere), so the interface language can be found and changed with no
document open. Shortcut keys may now be punctuation (egui's key name
","). #141's control test drives the menu item, the language selector
and the shortcut in Japanese and English.

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

* M14: Traditional Chinese as zh-hant.tsv

Reduce #140 to its translations: zh_tw.rs becomes
crates/ui-egui/src/i18n/zh-hant.tsv (PrintCraft renamed PdfCraft; the
29 errors.rs diagnostic templates dropped, as for Japanese) and one
registry row, code `zh-hant` (Taiwan vocabulary). Locales zh_TW, zh_HK,
zh_MO and zh-Hant-* resolve to it. #140's own copy of the UI wiring and
its errors.rs diagnostics are not taken: land/141 carries the wiring.
Tests: registration, locale mapping, every command label translated.
The catalog is an attributed asset; docs, ROADMAP and parity list it.

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

* M14: Simplified Chinese as zh-hans.tsv

Reduce #112's translations to crates/ui-egui/src/i18n/zh-hans.tsv
(PrintCraft renamed PdfCraft) and one registry row, code `zh-hans`
(#112 used `zh`; `zh`, `zh_CN`, `zh_SG` and `zh-Hans` locales still
resolve to it through the system-language match). #112's own copy of
the UI wiring is superseded by land/141. Tests: registration, locale
mapping, menu coverage. The catalog is an attributed asset; docs,
ROADMAP and parity list it.

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

* M14: CJK font order follows the UI language

From #112: `ui_cjk_fonts(prefer_hans)` orders the craft-fonts CJK faces
for the interface, and `theme::font_definitions_for` installs them. In
Simplified Chinese the `Hans` faces come first; otherwise the Japanese
ones (the previous behaviour). egui draws each character with the first
face that has it, so with Japanese first a Simplified-only character
(欢) would land in another face and sit on a different baseline. The
fonts are reinstalled when the language crosses that line (same family
names, so named fonts stay valid while the new set loads).

Changes from #112: within each group BIZ UDPGothic still leads, so a
serif listed first in the manifest (Shippori Mincho) never becomes the
Chinese-mode UI fallback; the unused `document_chinese_font` is left
out; and the crates/fonts/build.rs special case that skipped
"Noto Sans CJK SC" (and its test assertion and README line) is not
taken. Without CRAFT_FONTS_DIR, or without a `Hans` face, the order
logic yields only what exists: Chinese characters then show replacement
boxes and nothing panics (tests/fonts.rs, the fonts crate test, and the
control test switching ja -> zh-hans -> en).

Also document `--language` on the app's command line.

Co-Authored-By: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Petr Leo Compel <petrleocompel@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Simon Leblanc <contact@leblanc-simon.eu>
Co-authored-by: sakamoto824 <sakamoto824@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-authored-by: RicSchonfelder <ricschonfelder@icloud.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
This commit is contained in:
Crino 2026-10-08 10:17:00 +08:00 committed by GitHub
parent ab4c87926a
commit 355de9038e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1812 additions and 22 deletions

View File

@ -4,10 +4,11 @@
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 (246)
## In this repository (247)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
| `crates/ui-egui/src/i18n/zh-hans.tsv` | Simplified Chinese 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 | Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/zh-hant.tsv` | Traditional Chinese (Taiwan) 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 | Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/cs.tsv` | Czech 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 | Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese 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 | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |

View File

@ -10,6 +10,17 @@
# licence: SPDX expression from the allowlist in AGENTS.md §1.2. adobe_data = true marks the closed list of
# Adobe-authored non-visual technical data allowed by AGENTS.md §1.1.
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hans.tsv"
title = "Simplified Chinese interface translation catalog"
author = "PdfCraft contributors"
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources"
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1a39d1886fed368cc5e23c376505074b3955fd8bcbcd0678209126ce07a10a95"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
title = "Traditional Chinese (Taiwan) interface translation catalog"

View File

@ -103,7 +103,7 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 165–290 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 20% | 160–305 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, measure, search index, XFA |
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 33–66 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools`, UI control channel with drag. Missing: Action Wizard, AI providers |
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese and Traditional Chinese complete for them (≈1,800 entries each), Czech and Brazilian Portuguese for menus. Missing: more catalogs filled in, Windows language detection, installers, performance budgets |
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese and Traditional Chinese complete for them (≈1,800 entries each), Simplified Chinese ≈1,600, Czech and Brazilian Portuguese for menus. Missing: more catalogs filled in, Windows language detection, installers, performance budgets |
| | **Total (original plan sizing)** | **2,085–3,840** | **≈ 35%** | **≈ 1,350–2,500 at the planned rate; ≈ 600–1,100 at the measured rate** | |
**Overall progress: about 30–35% of the effort (49.5% of features shipped).** The viewer and the core are far ahead of the editing features, because the viewer was built first so progress could be seen; rendering itself is still borrowed from `hayro`. See [Honest assessment](#honest-assessment-2026-10-05).
@ -135,6 +135,7 @@ Newest first. One line per session: the date, what moved, and the new overall pe
- **2026-10-07 (fork, default-workspace-mode):** Preferences ▸ Documents and view remembers the workspace used for newly opened PDFs. All Tools remains the default; explicit CLI/control mode overrides last for the session and PDF Initial View metadata stays independent. Overall estimate unchanged (≈ 30–35%).
- **2026-10-07 (community, M14, #112):** Simplified Chinese (简体中文, `zh-hans`) as a catalog of about 1,600 entries; `zh`, `zh_CN` and `zh-Hans` locales follow it. In Simplified Chinese the craft-fonts `Hans` faces come before the Japanese ones, so Simplified-only characters don't sit on a different baseline.
- **2026-10-07 (community, M14, #140):** Traditional Chinese (繁體中文, `zh-hant`, Taiwan vocabulary) as a catalog of about 1,800 entries; `zh_TW`, `zh_HK`, `zh_MO` and `zh-Hant` locales follow it. Drawn with the craft-fonts CJK faces (missing characters show replacement boxes).
- **2026-10-07 (community, M14, #141):** Every dialog, panel, notice, shortcut list and native-picker title goes through the catalog (`tl!`, `i18n::fmt`), and Japanese grows from 49 to about 1,800 entries (`ja.tsv`); worker threads (export, OCR, actions) write their summaries in the UI language; history labels keep captured file and field names; the command palette searches translated and English labels and ids. Engine and OS error text is no longer matched against English templates: it is shown as is, inside a translated frame. Localization remains partial for other languages.
- **2026-10-07 (session 15):** Five community issues, each with a regression test: in two-page view, Next page moves a whole spread (#77, #70); Export to Word opens in Word again, with control characters dropped and receipt-length pages kept within Word's 22 in, checked in Microsoft Word (#78, #72); macOS opens PDFs from Finder, Open With and the Dock (Apple events through the audited `fmv-macos-events`, PDF declared in the Info.plist), and the packaging no longer describes the image editor it was copied from (#82, #73); Add text keeps the box being typed when you click elsewhere, with Done and Discard beside it, reproduced and checked with real OS input (#86, #74); sharper, higher-contrast text, most visible on Windows, with every text colour at WCAG AA in both themes (#84, #76; the console window was already gone in 0.2.1). Still open: #79 (layered "not verified" signature appearances), #80 (native macOS menu bar), #81 (Homebrew and Scoop). P0 88%, P1 53%; 805 features 50.2% shipped (53.7% weighted). ≈ 30–35%.

View File

@ -4,7 +4,7 @@
//!
//! View options (applied after the files open; also the seed of the UI control channel):
//! `--page N --zoom 150 --layout continuous|two-up|single --panel comments|bookmarks|pages|fields|layers|attachments|none
//! --theme light|dark --mode all|read|edit|convert|sign --tool <catalogue id> --left open|closed
//! --theme light|dark --language auto|<code> --mode all|read|edit|convert|sign --tool <catalogue id> --left open|closed
//! --organize on --fields on --dialog properties|shortcuts|about --palette <query> --home on`
//!
//! `--control <file>` enables the UI control channel (off by default): the app listens on a random

View File

@ -17,12 +17,18 @@ build input. `build.rs` embeds them as `CRAFT_FONTS` when the build sets
set). Without that variable `CRAFT_FONTS` is empty and everything below copes:
- `ui_japanese_fonts`: the `Jpan` faces for the interface, BIZ UDPGothic first.
- `ui_chinese_fonts`: the `Hans` faces for the interface, in manifest order (without one,
Chinese characters the Japanese faces lack show the replacement glyph).
- `ui_cjk_fonts(prefer_hans)`: both in fallback order for the UI language (Chinese group first
in Chinese mode, so one line never mixes faces with different vertical metrics).
- `document_japanese_font` / `japanese_glyph`: the face (Shippori Mincho, then BIZ UDMincho) whose
outlines become the Type 3 fallback font for Japanese text written into PDFs. Without it,
`japanese_glyph` returns `GlyphError::NoFont` and the editor reports a clear error.
- `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`.
wasm32 builds embed only BIZ UDPGothic Regular, to keep the web build small. Font files are never
wasm32 builds embed only BIZ UDPGothic Regular, to keep the web build small: the web build
currently has no Chinese face, so Chinese there still shows the replacement glyph.
Font files are never
committed here (`AGENTS.md` §1.4; team members: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md), internal).
The full font subsystem (parsing, shaping, subsetting and embedding) arrives with M2.2/M7.

View File

@ -36,6 +36,35 @@ pub fn ui_japanese_fonts() -> Vec<&'static CraftFont> {
fonts
}
/// The `Hans` craft-fonts faces for Simplified Chinese interface text, in manifest order.
/// Empty when built without craft-fonts (Chinese text then shows the font system's
/// replacement glyph, the same degraded mode as Japanese without craft-fonts).
pub fn ui_chinese_fonts() -> Vec<&'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.covers("Hans")).collect()
}
/// Interface CJK faces in fallback order for the UI language: Simplified Chinese first when
/// `prefer_hans`, otherwise Japanese first (the historical default).
///
/// The order matters beyond glyph shapes. egui renders each character with the FIRST face that
/// has its glyph, so with Japanese first a Simplified-only character (e.g. U+6B22 欢, absent
/// from Japanese faces) lands in a different face than its neighbours; the mixed vertical
/// metrics then sink it below the line. Preferring the UI language's face keeps one line in
/// one face with one baseline.
pub fn ui_cjk_fonts(prefer_hans: bool) -> Vec<&'static CraftFont> {
order_cjk(CRAFT_FONTS.iter(), prefer_hans)
}
fn order_cjk<'a>(faces: impl IntoIterator<Item = &'a CraftFont>, prefer_hans: bool) -> Vec<&'a CraftFont> {
let mut out: Vec<&'a CraftFont> = faces.into_iter().filter(|f| f.covers("Hans") || f.covers("Jpan")).collect();
let first = if prefer_hans { "Hans" } else { "Jpan" };
// The preferred script's faces first (a face covering both counts as preferred); within each
// group the Japanese UI face (BIZ UDPGothic, not a serif) leads, otherwise manifest order
// (the sort is stable).
out.sort_by_key(|f| (!f.covers(first), f.family != "BIZ UDPGothic"));
out
}
/// The face for Japanese text written into PDFs (serif document text): Shippori Mincho, then
/// BIZ UDMincho, then any other regular `Jpan` face. `None` without craft-fonts.
pub fn document_japanese_font() -> Option<&'static CraftFont> {
@ -82,6 +111,34 @@ pub static SHIPPORI_MINCHO: Option<&[u8]> = find("Shippori Mincho", "Regular");
mod tests {
use super::*;
#[test]
fn cjk_fallback_order_follows_the_ui_language() {
// Synthetic faces: order_cjk must not depend on the real build input.
static BYTES: &[u8] = b"fake";
static LATN: &[&str] = &["Latn"];
static JPAN_LATN: &[&str] = &["Jpan", "Latn"];
static HANS_LATN: &[&str] = &["Hans", "Latn"];
let biz_bold = CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: JPAN_LATN, bytes: BYTES };
let biz = CraftFont { family: "BIZ UDPGothic", style: "Regular", scripts: JPAN_LATN, bytes: BYTES };
let hans = CraftFont { family: "FakeHans", style: "Regular", scripts: HANS_LATN, bytes: BYTES };
let inter = CraftFont { family: "Inter", style: "Regular", scripts: LATN, bytes: BYTES };
let faces = [biz_bold, biz, hans, inter];
// Chinese mode: the Hans group first (manifest order), then Japanese.
let zh: Vec<&str> = order_cjk([&faces[0], &faces[2], &faces[1]], true).iter().map(|f| f.family).collect();
assert_eq!(zh, ["FakeHans", "BIZ UDPGothic", "BIZ UDPGothic"]);
// Japanese mode keeps the historical default: the Japanese UI face first.
let ja: Vec<&str> = order_cjk([&faces[0], &faces[2], &faces[1]], false).iter().map(|f| f.family).collect();
assert_eq!(ja, ["BIZ UDPGothic", "BIZ UDPGothic", "FakeHans"]);
// Latin-only faces are never interface CJK fallbacks.
assert!(order_cjk(&faces, true).iter().all(|f| f.covers("Hans") || f.covers("Jpan")));
// A serif listed first in the manifest never leads the Japanese group, in either mode.
let mincho = CraftFont { family: "Shippori Mincho", style: "Regular", scripts: JPAN_LATN, bytes: BYTES };
let zh: Vec<&str> = order_cjk([&mincho, &faces[2], &faces[1]], true).iter().map(|f| f.family).collect();
assert_eq!(zh, ["FakeHans", "BIZ UDPGothic", "Shippori Mincho"]);
let ja: Vec<&str> = order_cjk([&mincho, &faces[2], &faces[1]], false).iter().map(|f| f.family).collect();
assert_eq!(ja, ["BIZ UDPGothic", "Shippori Mincho", "FakeHans"]);
}
#[test]
fn craft_fonts_are_optional_and_consistent() {
// Holds with or without CRAFT_FONTS_DIR.
@ -89,9 +146,11 @@ mod tests {
assert_eq!(document_japanese_font().is_some(), CRAFT_FONTS.iter().any(|f| f.covers("Jpan")));
let ui = ui_japanese_fonts();
assert_eq!(ui.len(), CRAFT_FONTS.iter().filter(|f| f.covers("Jpan")).count());
let ui_zh = ui_chinese_fonts();
assert_eq!(ui_zh.len(), CRAFT_FONTS.iter().filter(|f| f.covers("Hans")).count());
if CRAFT_FONTS.is_empty() {
eprintln!("built without craft-fonts (CRAFT_FONTS_DIR unset): no Japanese faces, as expected");
assert!(SHIPPORI_MINCHO.is_none() && ui.is_empty());
assert!(SHIPPORI_MINCHO.is_none() && ui.is_empty() && ui_zh.is_empty());
return;
}
assert!(CRAFT_FONTS.iter().all(|f| !f.bytes.is_empty()));

View File

@ -9,7 +9,7 @@ mod craft;
mod encodings;
pub mod pdf;
mod script;
pub use craft::{CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, ui_japanese_fonts};
pub use craft::{CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts};
pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, script_outline};
/// Approximate advance of `s` in Helvetica (or Arial) at `size` points.

View File

@ -70,9 +70,11 @@ fn plural_pt(n: u64) -> usize {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 5] = [
pub static LANGUAGES: [LangInfo; 6] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, catalog: OnceLock::new() },
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, catalog: OnceLock::new() },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
LangInfo { code: "zh-hans", name: "简体中文", source: include_str!("zh-hans.tsv"), plural: plural_none, catalog: OnceLock::new() },
// Traditional Chinese in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*`
// locales all resolve here (see `candidates`).
LangInfo { code: "zh-hant", name: "繁體中文", source: include_str!("zh-hant.tsv"), plural: plural_none, catalog: OnceLock::new() },
@ -361,6 +363,8 @@ mod tests {
assert_eq!(lang_from_tag("POSIX"), Some(Lang::EN));
assert_eq!(lang_from_tag("fr_FR"), None);
assert_eq!(lang_from_tag("zh-TW"), Lang::from_code("zh-hant"));
assert_eq!(lang_from_tag("zh_CN.UTF-8"), Lang::from_code("zh-hans"));
assert_eq!(lang_from_tag("zh"), Lang::from_code("zh-hans"));
assert_eq!(lang_from_tag("zh_HK.UTF-8"), Lang::from_code("zh-hant"));
assert_eq!(lang_from_tag("zh-Hant-MO"), Lang::from_code("zh-hant"));
assert_eq!(lang_from_tag(""), None);
@ -538,7 +542,7 @@ mod tests {
let web_name = web.name();
let mut web_only = crate::theme::font_definitions();
for family in web_only.families.values_mut() {
family.retain(|name| !craft.iter().any(|face| face.name() == *name) || *name == web_name);
family.retain(|name| !pdfcraft_fonts::CRAFT_FONTS.iter().any(|face| face.name() == *name) || *name == web_name);
}
use egui::epaint::text::{Fonts, TextOptions};
for (build, defs) in [("desktop", crate::theme::font_definitions()), ("web", web_only)] {
@ -653,6 +657,19 @@ mod tests {
assert_eq!(restored.language, "cs");
}
#[test]
fn simplified_chinese_is_registered() {
let zh = Lang::from_code("zh-hans").expect("zh-hans registered");
assert_eq!(zh.name(), "简体中文");
assert_eq!(normalize_pref("zh-Hans"), Some("zh-hans"));
assert_eq!(normalize_pref("zh"), None, "only exact codes are preferences");
assert_eq!(tr(zh, "File"), "文件");
assert_eq!(tr(zh, "报告.pdf"), "报告.pdf");
for menu in ["File", "Edit", "Pages", "View", "Help", "Preferences…", "Interface language"] {
assert!(has(zh, menu), "missing {menu}");
}
}
#[test]
fn traditional_chinese_is_registered() {
let zh = Lang::from_code("zh-hant").expect("zh-hant registered");

File diff suppressed because it is too large Load Diff

View File

@ -437,6 +437,8 @@ pub struct PdfCraftApp {
pending_theme: Option<ThemeKind>,
styled: bool,
fonts_ready: bool,
/// The installed fonts put the Simplified Chinese faces first (see `theme::font_definitions_for`).
fonts_hans: bool,
/// The window title last sent to the platform.
pub window_title: String,
/// The UI control channel, when enabled (`--control`; off by default).
@ -613,6 +615,7 @@ impl PdfCraftApp {
pending_theme: None,
styled: false,
fonts_ready: false,
fonts_hans: false,
control: None,
bookmark_rename: None,
last_opened_url: None,
@ -1294,14 +1297,23 @@ impl eframe::App for PdfCraftApp {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.ctx = Some(ctx.clone());
// Notices raised outside `ui` (opened files, OS events, the control channel) translate too.
i18n::set_current(i18n::Lang::from_pref(&self.language));
let lang = i18n::Lang::from_pref(&self.language);
i18n::set_current(lang);
// Simplified Chinese wants its own faces before the Japanese ones (one baseline per line).
let hans = lang.code() == "zh-hans";
if !self.styled {
egui_extras::install_image_loaders(ctx);
theme::install_fonts(ctx);
theme::install_fonts_for(ctx, hans);
self.fonts_hans = hans;
theme::apply(ctx, self.theme);
self.styled = true;
} else {
self.fonts_ready = true;
if hans != self.fonts_hans {
// Same family names as before, so named fonts stay valid while the new set loads.
theme::install_fonts_for(ctx, hans);
self.fonts_hans = hans;
}
}
if let Some(k) = self.pending_theme.take() {
self.set_theme(ctx, k);

View File

@ -108,10 +108,25 @@ pub fn install_fonts(ctx: &egui::Context) {
ctx.set_fonts(font_definitions());
}
/// Install the interface fonts with the CJK fallback order for the UI language (Chinese
/// first for Simplified Chinese, Japanese first otherwise). Call it when the language
/// changes; the new faces take effect next frame.
pub fn install_fonts_for(ctx: &egui::Context, prefer_hans: bool) {
ctx.set_fonts(font_definitions_for(prefer_hans));
}
/// The interface fonts: Inter (and JetBrains Mono for code) first, then egui's defaults, then
/// the Japanese faces of the optional craft-fonts build input (BIZ UDPGothic first) as the last
/// fallback in every family. Without craft-fonts there is no Japanese face.
/// the CJK faces of the optional craft-fonts build input as the last fallback in every family.
/// Without craft-fonts there is no Japanese or Chinese face.
pub fn font_definitions() -> FontDefinitions {
font_definitions_for(false)
}
/// [`font_definitions`] with the CJK fallback order for the UI language. Simplified Chinese
/// must come first in Chinese mode: otherwise shared characters render in the Japanese face
/// while Simplified-only characters (e.g. U+6B22 欢) fall through to the Chinese face, and
/// the mixed vertical metrics sink them below the line.
pub fn font_definitions_for(prefer_hans: bool) -> FontDefinitions {
let mut fonts = FontDefinitions::default();
let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| {
fonts.font_data.insert(name.to_owned(), Arc::new(FontData::from_static(bytes)));
@ -122,10 +137,13 @@ pub fn font_definitions() -> FontDefinitions {
add(&mut fonts, "JetBrainsMono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"));
fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "Inter".to_owned());
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "JetBrainsMono".to_owned());
// The same static bytes pdfcraft-fonts uses for Japanese text in PDFs: one copy, not two.
for face in pdfcraft_fonts::ui_japanese_fonts() {
// The same static bytes pdfcraft-fonts uses for Japanese/Chinese text in PDFs: one copy, not two.
for face in pdfcraft_fonts::ui_cjk_fonts(prefer_hans) {
let name = face.name();
add(&mut fonts, &name, face.bytes);
// Japanese and Chinese faces have distinct family names, so no collision here.
if !fonts.font_data.contains_key(&name) {
add(&mut fonts, &name, face.bytes);
}
for family in [FontFamily::Proportional, FontFamily::Monospace] {
fonts.families.entry(family).or_default().push(name.clone());
}

View File

@ -79,7 +79,7 @@ fn language_switch_preserves_document_and_command_ids() {
let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone();
assert_eq!(documents[0]["dirty"], true);
let commands = ok(&mut h, &c, "ui.commands", json!({}));
for code in ["ja", "en"] {
for code in ["ja", "zh-hans", "en"] {
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": code }));
h.run_steps(2);
let state = ok(&mut h, &c, "ui.state", json!({}));

View File

@ -5,6 +5,7 @@ use egui::{Color32, FontFamily, FontId};
use pdfcraft_ui_egui::theme;
const JAPANESE: &str = "日本語の文字";
const CHINESE: &str = "简体中文欢迎";
fn families() -> Vec<FontId> {
vec![FontId::proportional(13.0), FontId::monospace(13.0), theme::medium(13.0), theme::semibold(17.0)]
@ -42,6 +43,35 @@ fn japanese_ui_text_uses_craft_fonts() {
}
}
/// Chinese mode orders the CJK fallback with the `Hans` group first, so one line never
/// mixes faces with different vertical metrics. Without an allowed `Hans` face in the
/// build input the order part still holds; glyph coverage follows the craft-fonts checkout.
#[test]
fn chinese_ui_text_prefers_the_chinese_face() {
if pdfcraft_fonts::ui_chinese_fonts().is_empty() {
eprintln!(
"skipping chinese_ui_text_prefers_the_chinese_face: no Hans face bundled (set CRAFT_FONTS_DIR with an allowed Chinese face to run it)"
);
return;
}
let defs = theme::font_definitions_for(true);
let hans: Vec<String> = pdfcraft_fonts::ui_chinese_fonts().iter().map(|f| f.name()).collect();
for family in [FontFamily::Proportional, FontFamily::Monospace] {
let stack = &defs.families[&family];
let first_zh = stack.iter().position(|n| hans.iter().any(|h| h == n)).expect("a Hans face is a fallback");
let first_ja = stack.iter().position(|n| n.starts_with("BIZ UDPGothic")).expect("BIZ UDPGothic is a fallback");
let own = stack.iter().position(|n| n == "Inter" || n == "JetBrainsMono").expect("the app's own font");
assert!(own < first_zh && first_zh < first_ja, "{family:?}: {stack:?}");
}
let mut fonts = Fonts::new(TextOptions::default(), defs);
for id in families() {
assert!(fonts.has_glyphs(&id, CHINESE), "{id:?} lacks {CHINESE}");
}
for w in layout_widths(&mut fonts, CHINESE) {
assert!(w > 13.0 * 0.8 * CHINESE.chars().count() as f32, "{w}");
}
}
/// Without craft-fonts the interface fonts still install and lay out any text (Japanese falls
/// back to egui's replacement glyph) without panicking; Latin text is unaffected.
#[test]

View File

@ -1,8 +1,8 @@
# Interface language
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **繁體中文**, **Čeština** or **Português (Brasil)**. 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** or **Português (Brasil)**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
**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, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one. A system language without a catalog, such as French, shows English. Windows has no system-language detection yet, so **Auto** shows English there unless `LANG` is set.
**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, 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. A system language without a catalog, such as French, shows English. Windows has no system-language detection yet, so **Auto** shows English there unless `LANG` is set.
The control channel exposes the setting through `ui.set`:
@ -10,11 +10,11 @@ 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-hant`, `cs`, `pt-br`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hant`, `cs` or `pt-br`). 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`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs` or `pt-br`). 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!`). Japanese and Traditional Chinese translate them (about 1,800 entries each); Czech and Brazilian Portuguese 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.
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese and Traditional Chinese translate them (about 1,800 entries each), Simplified Chinese most of them (about 1,600); Czech and Brazilian Portuguese 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.
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/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. Traditional Chinese is drawn with the same craft-fonts CJK faces, so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Czech and Brazilian Portuguese need only the bundled Latin faces (tested in `tests/fonts.rs`).
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/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 is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Czech and Brazilian Portuguese need only the bundled Latin faces (tested in `tests/fonts.rs`).
## How translations work

View File

@ -9807,7 +9807,7 @@ status = "partial"
commands = ["app.preferences"]
tools = []
evidence = ["crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names"]
notes = "Preferences switches Auto (system language)/English/Japanese/Traditional Chinese/Czech/Brazilian Portuguese and persists; ui.set language auto|en|ja|zh-hant|cs|pt-br and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese and Traditional Chinese cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. Engine/OS error details stay untranslated. Missing: the other catalogs filled in, Windows system-language detection, a full localization audit."
notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese and Traditional Chinese cover commands, dialogs, panels, shortcuts and history labels, Simplified Chinese most of them; Czech and Brazilian Portuguese the menus. Engine/OS error details stay untranslated. Missing: the other catalogs filled in, Windows system-language detection, a full localization audit."
[[feature]]
id = "misc.prefs-reading"