mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 18:52:36 +00:00
i18n: interface translations from craft-i18n (#6055)
* build(i18n): embed the craft-i18n catalogs from a pinned checkout
- `craft-i18n.rev` pins storytold/craft-i18n at e45ddf7 (the PdfCraft cutover sync of
pdfcraft@ecdd1e6); `.gitattributes` keeps it LF.
- The `craft-i18n` crate (craft-libs 787a7a0, PdfCraft's existing pin) is a direct dependency
of the UI crate; it was already in the tree through craft-ui.
- `crates/ui-egui/build.rs` writes `CRAFT_I18N` from `CRAFT_I18N_DIR`: one (locale, common.tsv,
pdfcraft.tsv) entry per locale with a `pdfcraft.tsv`, paths written with `{:?}` and never
canonicalized. Unset it is empty (English only); `CRAFT_I18N_REQUIRED=1` makes a missing or
invalid checkout an error.
- Both flatpak manifests name the craft-i18n commit and `CRAFT_I18N_DIR`; a test keeps both
commits equal to the trimmed pin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(i18n): look strings up through craft-i18n behind a thin Lang wrapper
`i18n.rs` keeps PdfCraft's API (tr, tr_ctx, tr_id, trn, key_name, fmt,
Lang, LANGUAGES, detection) and hands lookups to the craft-i18n crate:
`get` for key names, `trn_plain` for plurals (no isolates, no digit
grouping), PdfCraft's own `fmt` (no FSI isolates). `@id` rows and the
"Undo {action}" template are read only where PdfCraft had them, so menus
and history labels read exactly as before.
Detection is unchanged (LC_ALL/LC_MESSAGES/LANG, macOS defaults,
sys_locale); the crate's match_tag would drop nb for no/nn, so PdfCraft's
own tag mapping stays, with one parity test per path.
Completeness tests become reports; tests that need catalogs skip with a
note when the build has none. Font coverage tests read the embedded
catalogs (shared and PdfCraft rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(i18n): remove the in-repo catalogs; craft-i18n holds them
The 34 `crates/ui-egui/src/i18n/*.tsv` catalogs were migrated into
craft-i18n at pdfcraft@ecdd1e6 and are now embedded from the pinned
checkout. ATTRIBUTION.toml drops their 34 entries for one craft-i18n
`[[build_input]]` (craft-i18n's ATTRIBUTION.md credits the translators);
AGENTS.md and docs/localization.md describe the new system and how to
add a language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(i18n): skip catalog tests without craft-i18n; drop the catalog hash exemption
Tests that switch to a non-English language skip with a note when the
build embeds no catalogs (CRAFT_I18N_DIR unset), so the English-only
build tests green. `skip_without_catalogs` moves to the integration
test support module.
With no catalogs left in the repository, `cargo xtask assets` no longer
exempts `crates/ui-egui/src/i18n/*.tsv` from hashing: every `[[asset]]`
carries a sha256 again, and a `.tsv` added back would need one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(i18n): watch the locales directory before reading it
A CRAFT_I18N_DIR that doesn't exist yet now reruns the build script once it appears (review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(i18n): pin craft-i18n dfbe955, the merge of storytold/craft-i18n#129
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(i18n): skip an @id row that changes the label's placeholders or ellipsis
craft-i18n checks placeholders and the trailing ellipsis on every row whose
source is English text, but not on @id rows, whose source is the command id.
The in-repo catalog test used to fail on such a row; since the cutover it only
prints it, while tr_id and menu_label still showed it. Both now use PdfCraft's
own @id row only when it keeps the label's shape (same_shape, the rule
craft-i18n applies elsewhere), and otherwise show the label's translation, as
bundled_catalogs_are_consistent says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(i18n): reject an @id row with an open brace; list unused @id rows in the gaps report
same_shape skipped a `{` with no `}`, so an @id row such as "Öffnen {name…"
for "Open…" was still shown. It now counts as a changed shape.
bundled_catalogs_are_consistent printed the @id rows PdfCraft doesn't use, but
CI captures test output, so nobody saw them. translation_gaps_report, which
`cargo xtask i18n-report` runs with --nocapture, now lists them
(unused_id_rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(i18n): reject an @id row with a `}` that closes no placeholder
same_shape counted only the `}` that close a `{`, so "Öffnen name}…" for
"Open…" or "{name}} öffnen" for "Open {name}" still passed. A row whose `}`
count differs from its placeholders now counts as a changed shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(i18n): no braces in the assert messages of the brace test
assert! reads its message as a format string, so "a `}` without its `{`"
did not compile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
70 files changed
+1054
-94610
No files matched your search
@@ -3,6 +3,9 @@
|
|||||||
# signatures) and assets whose SHA-256 ATTRIBUTION.toml records (cargo xtask assets).
|
# signatures) and assets whose SHA-256 ATTRIBUTION.toml records (cargo xtask assets).
|
||||||
* -text
|
* -text
|
||||||
|
|
||||||
|
# The craft-i18n pin: CI and the build read it as one SHA, so it stays LF everywhere (readers trim it too).
|
||||||
|
craft-i18n.rev text eol=lf
|
||||||
|
|
||||||
# Not text at all: no textual diffs or merges either.
|
# Not text at all: no textual diffs or merges either.
|
||||||
*.pdf binary
|
*.pdf binary
|
||||||
*.p12 binary
|
*.p12 binary
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ Anything else is forbidden, including assets whose licence you cannot prove from
|
|||||||
- path or URL, title, author;
|
- path or URL, title, author;
|
||||||
- source (URL, with version or commit);
|
- source (URL, with version or commit);
|
||||||
- SPDX licence, and the licence file in the repo;
|
- SPDX licence, and the licence file in the repo;
|
||||||
- SHA-256, except for translation catalogs (see below);
|
- SHA-256;
|
||||||
- how it is used.
|
- how it is used.
|
||||||
- **Translation catalogs** (`crates/ui-egui/src/i18n/*.tsv`, `kind = "translation"`) are project files under the project licence. Their entries carry no `sha256`: git history records every change and who made it. Every other kind (fonts, images, data) is hashed.
|
- **Translations** live in craft-i18n (`locales/<code>/common.tsv` and `pdfcraft.tsv`), embedded at build time from the checkout pinned in `craft-i18n.rev`; PdfCraft keeps no catalogs. `ATTRIBUTION.toml` records them as one `[[build_input]]` (`kind = "translation"`); craft-i18n's own `ATTRIBUTION.md` credits the translators. Every `[[asset]]` (fonts, images, data) is hashed.
|
||||||
- **`ATTRIBUTION.md`** is generated from it (`cargo xtask assets --write`). Never edit it by hand.
|
- **`ATTRIBUTION.md`** is generated from it (`cargo xtask assets --write`). Never edit it by hand.
|
||||||
- **`NOTICE`** keeps the licence notices that the licences require.
|
- **`NOTICE`** keeps the licence notices that the licences require.
|
||||||
- **`cargo xtask assets`** runs in `cargo xtask ci`. It fails if:
|
- **`cargo xtask assets`** runs in `cargo xtask ci`. It fails if:
|
||||||
- an asset file in the repository has no entry;
|
- an asset file in the repository has no entry;
|
||||||
- an entry's hash is missing or does not match (translation catalogs carry none);
|
- an entry's hash is missing or does not match;
|
||||||
- a licence is not on the allowlist;
|
- a licence is not on the allowlist;
|
||||||
- a licence file is missing;
|
- a licence file is missing;
|
||||||
- an image, icon or font names Adobe as author or source, except a font licensed under OFL-1.1 alone.
|
- an image, icon or font names Adobe as author or source, except a font licensed under OFL-1.1 alone.
|
||||||
|
|||||||
+4
-37
@@ -2,40 +2,12 @@
|
|||||||
|
|
||||||
<!-- Generated from ATTRIBUTION.toml by `cargo xtask assets --write`. Do not edit by hand. -->
|
<!-- Generated from ATTRIBUTION.toml by `cargo xtask assets --write`. Do not edit by hand. -->
|
||||||
|
|
||||||
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 the SHA-256 of every asset but the translation catalogs, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
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 the SHA-256 of every asset, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||||
|
|
||||||
## In this repository (289)
|
## In this repository (255)
|
||||||
|
|
||||||
| Asset | Title | Author | Licence | Source | Used for |
|
| Asset | Title | Author | Licence | Source | Used for |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `crates/ui-egui/src/i18n/el.tsv` | Greek interface translation catalog | Camthalion77 and contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft English UI labels; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Greek UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n.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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n.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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check and standard-identification wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Czech UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese interface translation catalog | André Cardoso (andrecards), PdfCraft contributors | MIT OR Apache-2.0 | Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table limit messages are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/es.tsv` | Spanish 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; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Spanish UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/fr.tsv` | French 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | French UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/de.tsv` | German 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | German UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/lo.tsv` | Lao interface translation catalog | @bounkirdni-2025 | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Lao UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/te.tsv` | Telugu 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Telugu UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/it.tsv` | Italian 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Italian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/pl.tsv` | Polish 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Polish UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/ru.tsv` | Russian 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Russian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/kk.tsv` | Kazakh interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Kazakh UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/ja.tsv` | Japanese 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Japanese UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/ar.tsv` | Arabic 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Arabic UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/ckb.tsv` | Central Kurdish (Sorani) 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Central Kurdish UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/fa.tsv` | Persian (Farsi) 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; 2026-10-25 initial catalog covering the current 2,776-entry key union, AI-assisted translations from English, not separately native-reviewed | Persian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/tr.tsv` | Turkish 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Turkish UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/lt.tsv` | Lithuanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/hi.tsv` | Hindi 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Hindi UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/id.tsv` | Indonesian 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Indonesian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/ko.tsv` | Korean 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Korean UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/ro.tsv` | Romanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; AI-assisted and not yet reviewed line by line by a native Romanian speaker; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Romanian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/fi.tsv` | Finnish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: AI-assisted (Claude Code) clean-room translations of PdfCraft's English UI labels and user-facing messages; not yet proofread by a native Finnish speaker; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 calculation-order, text-format, print, thumbnail, layer, comment-reshape and signature labels (previously English placeholders) are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Finnish UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
@@ -286,17 +258,11 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
|||||||
| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile |
|
| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile |
|
||||||
| `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/it/pdf.rs): the legacy adbe.x509.rsa_sha1 format |
|
| `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/it/pdf.rs): the legacy adbe.x509.rsa_sha1 format |
|
||||||
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | 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 |
|
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | 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/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n.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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Slovene UI strings, compiled in by crates/ui-egui/src/i18n.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 |
|
| `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/nb.tsv` | Norwegian Bokmål 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
| `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
| `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
| `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
| `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||||
| `crates/ui-egui/src/i18n/sq.tsv` | Albanian 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-appearance labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Albanian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `crates/ui-egui/src/i18n/th.tsv` | Thai 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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Thai UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
| `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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Catalan UI strings, compiled in by crates/ui-egui/src/i18n.rs |
|
|
||||||
|
|
||||||
## Compiled in through dependencies (6)
|
## Compiled in through dependencies (6)
|
||||||
|
|
||||||
@@ -338,10 +304,11 @@ Fonts are fetched by `cargo xtask demo-pdf` into `target/demo-fonts/`, OCR model
|
|||||||
| `text-detection.onnx` | Ocrs text detection model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0) | Scan & OCR: finds words on page images (cargo xtask models) |
|
| `text-detection.onnx` | Ocrs text detection model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0) | Scan & OCR: finds words on page images (cargo xtask models) |
|
||||||
| `text-recognition.onnx` | Ocrs text recognition model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0) | Scan & OCR: reads the words it finds (cargo xtask models) |
|
| `text-recognition.onnx` | Ocrs text recognition model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0) | Scan & OCR: reads the words it finds (cargo xtask models) |
|
||||||
|
|
||||||
## Optional build inputs (1)
|
## Optional build inputs (2)
|
||||||
|
|
||||||
Not in this repository and never downloaded by it: compiled in only when the build sets the option (official releases do). Each input attributes its own files.
|
Not in this repository and never downloaded by it: compiled in only when the build sets the option (official releases do). Each input attributes its own files.
|
||||||
|
|
||||||
| Input | Option | Title | Author | Licence | Source | Attribution | Used for |
|
| Input | Option | Title | Author | Licence | Source | Attribution | Used for |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| `craft-i18n` | `CRAFT_I18N_DIR` | Interface translations: the craft-i18n catalogs (locales/<code>/common.tsv and pdfcraft.tsv) of the 34 languages PdfCraft offers besides English | The craft-i18n translators, including every translator of PdfCraft's former in-repo catalogs (crates/ui-egui/src/i18n/*.tsv, migrated at pdfcraft@ecdd1e6) | MIT OR Apache-2.0 | https://github.com/storytold/craft-i18n (pinned by commit in craft-i18n.rev, which CI, releases and the flatpak manifests read) | https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md | Interface text in languages other than English, embedded by crates/ui-egui/build.rs and looked up through craft-libs' craft-i18n crate; a build without CRAFT_I18N_DIR is English only. Clean-room translations from PdfCraft's English interface labels |
|
||||||
| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1 |
|
| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1 |
|
||||||
+12
-341
@@ -1,296 +1,15 @@
|
|||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/el.tsv"
|
|
||||||
title = "Greek interface translation catalog"
|
|
||||||
author = "Camthalion77 and contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft English UI labels; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
|
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
|
||||||
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
|
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
|
||||||
#
|
#
|
||||||
# [[asset]] a file in this repository (committed or vendored); `sha256` must match the file.
|
# [[asset]] a file in this repository (committed or vendored); `sha256` must match the file.
|
||||||
# Translation catalogs (kind = "translation", crates/ui-egui/src/i18n/*.tsv) are project files
|
|
||||||
# under the project licence and carry no `sha256`: git history records their provenance.
|
|
||||||
# [[bundled]] a file compiled into PdfCraft by a Cargo dependency (its crate version must match Cargo.lock).
|
# [[bundled]] a file compiled into PdfCraft by a Cargo dependency (its crate version must match Cargo.lock).
|
||||||
# [[fetched]] downloaded at build time by xtask (never committed), pinned by `sha256`.
|
# [[fetched]] downloaded at build time by xtask (never committed), pinned by `sha256`.
|
||||||
# [[build_input]] an optional build input outside this repository (e.g. craft-fonts via CRAFT_FONTS_DIR);
|
# [[build_input]] an optional build input outside this repository (craft-fonts via CRAFT_FONTS_DIR, craft-i18n via CRAFT_I18N_DIR);
|
||||||
# never committed or downloaded here, its own repository attributes each file.
|
# never committed or downloaded here, its own repository attributes each file.
|
||||||
#
|
#
|
||||||
# licence: SPDX expression from the allowlist in AGENTS.md §1.2. adobe_data = true marks the closed list of
|
# 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.
|
# 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; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
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.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
|
||||||
title = "Traditional Chinese (Taiwan) interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/cs.tsv"
|
|
||||||
title = "Czech interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check and standard-identification wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
|
||||||
title = "Brazilian Portuguese interface translation catalog"
|
|
||||||
author = "André Cardoso (andrecards), PdfCraft contributors"
|
|
||||||
source = "Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table limit messages are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
|
||||||
title = "Spanish interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
|
||||||
title = "French interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/de.tsv"
|
|
||||||
title = "German interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/lo.tsv"
|
|
||||||
title = "Lao interface translation catalog"
|
|
||||||
author = "@bounkirdni-2025"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Lao UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
|
||||||
title = "Telugu interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/it.tsv"
|
|
||||||
title = "Italian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
|
||||||
title = "Polish interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
|
||||||
title = "Russian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/hu.tsv"
|
|
||||||
title = "Hungarian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
|
||||||
title = "Bulgarian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/kk.tsv"
|
|
||||||
title = "Kazakh interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
|
||||||
title = "Japanese interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/uk.tsv"
|
|
||||||
title = "Ukrainian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/ar.tsv"
|
|
||||||
title = "Arabic interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/ckb.tsv"
|
|
||||||
title = "Central Kurdish (Sorani) interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Central Kurdish UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/fa.tsv"
|
|
||||||
title = "Persian (Farsi) interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-25 initial catalog covering the current 2,776-entry key union, AI-assisted translations from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Persian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/tr.tsv"
|
|
||||||
title = "Turkish interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/lt.tsv"
|
|
||||||
title = "Lithuanian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/hi.tsv"
|
|
||||||
title = "Hindi interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Hindi UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/id.tsv"
|
|
||||||
title = "Indonesian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Indonesian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/ko.tsv"
|
|
||||||
title = "Korean interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Korean UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/ro.tsv"
|
|
||||||
title = "Romanian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; AI-assisted and not yet reviewed line by line by a native Romanian speaker; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Romanian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/fi.tsv"
|
|
||||||
title = "Finnish interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: AI-assisted (Claude Code) clean-room translations of PdfCraft's English UI labels and user-facing messages; not yet proofread by a native Finnish speaker; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 calculation-order, text-format, print, thumbnail, layer, comment-reshape and signature labels (previously English placeholders) are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Finnish UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
[[asset]]
|
||||||
path = "assets/icons/accessibility.svg"
|
path = "assets/icons/accessibility.svg"
|
||||||
title = "Lucide icon \"accessibility\""
|
title = "Lucide icon \"accessibility\""
|
||||||
@@ -3381,6 +3100,17 @@ kind = "model"
|
|||||||
usage = "Scan & OCR: reads the words it finds (cargo xtask models)"
|
usage = "Scan & OCR: reads the words it finds (cargo xtask models)"
|
||||||
sha256 = "86c145c2edb96c8caed5b1ebb8f44d706408922211451c309c625157dd6061c5"
|
sha256 = "86c145c2edb96c8caed5b1ebb8f44d706408922211451c309c625157dd6061c5"
|
||||||
|
|
||||||
|
[[build_input]]
|
||||||
|
name = "craft-i18n"
|
||||||
|
option = "CRAFT_I18N_DIR"
|
||||||
|
title = "Interface translations: the craft-i18n catalogs (locales/<code>/common.tsv and pdfcraft.tsv) of the 34 languages PdfCraft offers besides English"
|
||||||
|
author = "The craft-i18n translators, including every translator of PdfCraft's former in-repo catalogs (crates/ui-egui/src/i18n/*.tsv, migrated at pdfcraft@ecdd1e6)"
|
||||||
|
source = "https://github.com/storytold/craft-i18n (pinned by commit in craft-i18n.rev, which CI, releases and the flatpak manifests read)"
|
||||||
|
licence = "MIT OR Apache-2.0"
|
||||||
|
attribution = "https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md"
|
||||||
|
kind = "translation"
|
||||||
|
usage = "Interface text in languages other than English, embedded by crates/ui-egui/build.rs and looked up through craft-libs' craft-i18n crate; a build without CRAFT_I18N_DIR is English only. Clean-room translations from PdfCraft's English interface labels"
|
||||||
|
|
||||||
[[build_input]]
|
[[build_input]]
|
||||||
name = "craft-fonts"
|
name = "craft-fonts"
|
||||||
option = "CRAFT_FONTS_DIR"
|
option = "CRAFT_FONTS_DIR"
|
||||||
@@ -3414,26 +3144,6 @@ kind = "icon"
|
|||||||
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
||||||
sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428"
|
sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428"
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/vi.tsv"
|
|
||||||
title = "Vietnamese interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/sl.tsv"
|
|
||||||
title = "Slovene interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
[[asset]]
|
||||||
|
|
||||||
path = "vendor/vello_common/assets/probe.png"
|
path = "vendor/vello_common/assets/probe.png"
|
||||||
@@ -3446,16 +3156,6 @@ kind = "image"
|
|||||||
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
|
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
|
||||||
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697"
|
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697"
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/nb.tsv"
|
|
||||||
title = "Norwegian Bokmål interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
[[asset]]
|
||||||
path = "assets/icons/superscript.svg"
|
path = "assets/icons/superscript.svg"
|
||||||
title = "Lucide icon \"superscript\""
|
title = "Lucide icon \"superscript\""
|
||||||
@@ -3500,32 +3200,3 @@ kind = "icon"
|
|||||||
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
||||||
sha256 = "864ab2f19254e6ac7240bc6a98323197332e8f714d4d3f94831f41309e5ef2f7"
|
sha256 = "864ab2f19254e6ac7240bc6a98323197332e8f714d4d3f94831f41309e5ef2f7"
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/sq.tsv"
|
|
||||||
title = "Albanian interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-appearance labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Albanian UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[asset]]
|
|
||||||
path = "crates/ui-egui/src/i18n/th.tsv"
|
|
||||||
title = "Thai interface translation catalog"
|
|
||||||
author = "PdfCraft contributors"
|
|
||||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Thai UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
|
|
||||||
[[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; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed"
|
|
||||||
licence = "MIT OR Apache-2.0"
|
|
||||||
licence_file = "LICENSE-MIT"
|
|
||||||
kind = "translation"
|
|
||||||
usage = "Catalan UI strings, compiled in by crates/ui-egui/src/i18n.rs"
|
|
||||||
Generated
+1
@@ -4659,6 +4659,7 @@ dependencies = [
|
|||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"craft-i18n",
|
||||||
"craft-keymap",
|
"craft-keymap",
|
||||||
"craft-menu",
|
"craft-menu",
|
||||||
"craft-quit",
|
"craft-quit",
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ craft-quit = { git = "https://github.com/storytold/craft-libs", version = "0.1.0
|
|||||||
# One menu definition for the native macOS menu bar and the in-window bar elsewhere.
|
# One menu definition for the native macOS menu bar and the in-window bar elsewhere.
|
||||||
craft-menu = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
craft-menu = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
||||||
craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff", default-features = false }
|
craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff", default-features = false }
|
||||||
|
# Interface translations: lookup, fallback, CLDR plurals and language names over the craft-i18n
|
||||||
|
# catalogs that `crates/ui-egui/build.rs` embeds from `CRAFT_I18N_DIR` (storytold/craft-i18n, at
|
||||||
|
# the commit in `craft-i18n.rev`). Already in the tree through craft-ui.
|
||||||
|
craft-i18n = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
||||||
# Shared system spooler; document composition remains in pdfcraft-print.
|
# Shared system spooler; document composition remains in pdfcraft-print.
|
||||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
||||||
# Whether a path stored in a document may be opened (GoToR link targets).
|
# Whether a path stored in a document may be opened (GoToR link targets).
|
||||||
|
|||||||
@@ -663,6 +663,29 @@ fn the_desktop_entry_names_the_window_class() {
|
|||||||
assert_eq!(class, Some(super::APP_ID));
|
assert_eq!(class, Some(super::APP_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both flatpak manifests build with the craft-i18n commit `craft-i18n.rev` pins: CI and
|
||||||
|
/// releases read the file, and the manifests have to name the commit themselves. The pin may
|
||||||
|
/// have a trailing newline or CRLF (a Windows checkout), so it is trimmed, as every reader does.
|
||||||
|
#[test]
|
||||||
|
fn the_flatpak_manifests_pin_the_craft_i18n_rev() {
|
||||||
|
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
let rev = std::fs::read_to_string(root.join("craft-i18n.rev")).unwrap();
|
||||||
|
let rev = rev.trim();
|
||||||
|
assert!(rev.len() == 40 && rev.bytes().all(|b| b.is_ascii_hexdigit()), "craft-i18n.rev holds one full SHA: {rev:?}");
|
||||||
|
for manifest in ["ai.storyteller.pdfcraft.yml", "ai.storyteller.pdfcraft.bundle.yml"] {
|
||||||
|
let text = std::fs::read_to_string(root.join("packaging/linux/flatpak").join(manifest)).unwrap();
|
||||||
|
let source =
|
||||||
|
text.split("url: https://github.com/storytold/craft-i18n").nth(1).unwrap_or_else(|| panic!("{manifest}: a craft-i18n git source"));
|
||||||
|
let source = source.split("- type:").next().unwrap_or(source);
|
||||||
|
let commit = source
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("commit:"))
|
||||||
|
.unwrap_or_else(|| panic!("{manifest}: the craft-i18n source pins a commit"));
|
||||||
|
assert_eq!(commit.trim().trim_matches(['\'', '"']), rev, "{manifest}");
|
||||||
|
assert!(text.contains("CRAFT_I18N_DIR: /run/build/pdfcraft/craft-i18n"), "{manifest}: the build names the catalogs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Regression test for #246: `eprintln!` panics when standard error is a broken pipe (a GUI
|
// Regression test for #246: `eprintln!` panics when standard error is a broken pipe (a GUI
|
||||||
// launch on Windows), and a panic inside the panic hook aborts the process. The start-up
|
// launch on Windows), and a panic inside the panic hook aborts the process. The start-up
|
||||||
// path and the hook must write to standard error without it.
|
// path and the hook must write to standard error without it.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
dfbe955b1ba4aa84bb34c80c45b9d917e8da4704
|
||||||
@@ -14,6 +14,8 @@ craft-ui = { workspace = true }
|
|||||||
# The menus: native on macOS, an egui bar elsewhere, from one definition (menus.rs).
|
# The menus: native on macOS, an egui bar elsewhere, from one definition (menus.rs).
|
||||||
craft-menu = { workspace = true }
|
craft-menu = { workspace = true }
|
||||||
craft-keymap = { workspace = true }
|
craft-keymap = { workspace = true }
|
||||||
|
# Interface translations over the embedded craft-i18n catalogs (`build.rs`, `src/i18n.rs`).
|
||||||
|
craft-i18n = { workspace = true }
|
||||||
# Quit from the menus asks the window to close, so the unsaved-changes check runs; on macOS a window
|
# Quit from the menus asks the window to close, so the unsaved-changes check runs; on macOS a window
|
||||||
# close also quits through `terminate:` (the Touch Bar crash, #1136).
|
# close also quits through `terminate:` (the Touch Bar crash, #1136).
|
||||||
craft-quit = { workspace = true, features = ["egui"] }
|
craft-quit = { workspace = true, features = ["egui"] }
|
||||||
|
|||||||
+84
-1
@@ -1,4 +1,5 @@
|
|||||||
//! Bake the contributor credits into the binary as static Rust tables. Nothing is read at run time.
|
//! Bake the contributor credits into the binary as static Rust tables, and embed the craft-i18n
|
||||||
|
//! interface catalogs. Nothing is read at run time.
|
||||||
//!
|
//!
|
||||||
//! - contributors/contributors.json: commit stats per GitHub username, with a mark for whose people.toml
|
//! - contributors/contributors.json: commit stats per GitHub username, with a mark for whose people.toml
|
||||||
//! entry is verified.
|
//! entry is verified.
|
||||||
@@ -6,6 +7,12 @@
|
|||||||
//! JSON says the entry was verified (`people_entry`).
|
//! JSON says the entry was verified (`people_entry`).
|
||||||
//!
|
//!
|
||||||
//! A missing or malformed file gives empty tables (or no names) and a warning.
|
//! A missing or malformed file gives empty tables (or no names) and a warning.
|
||||||
|
//!
|
||||||
|
//! - `CRAFT_I18N_DIR`, a craft-i18n checkout (storytold/craft-i18n, at the SHA in
|
||||||
|
//! `craft-i18n.rev`): `craft_i18n.rs` holds `CRAFT_I18N`, one (locale, shared catalog, PdfCraft
|
||||||
|
//! catalog) entry per locale that has a `pdfcraft.tsv`. Unset, or not a checkout (a warning),
|
||||||
|
//! it is empty and the interface is English only; with `CRAFT_I18N_REQUIRED=1` (CI and releases)
|
||||||
|
//! that is an error instead. Recipe: craft-i18n's `docs/integration.md`.
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
@@ -23,6 +30,82 @@ fn main() {
|
|||||||
if let Err(e) = std::fs::write(&out, generate(&json, &people)) {
|
if let Err(e) = std::fs::write(&out, generate(&json, &people)) {
|
||||||
println!("cargo::warning=writing {}: {e}", out.display());
|
println!("cargo::warning=writing {}: {e}", out.display());
|
||||||
}
|
}
|
||||||
|
let out = out.with_file_name("craft_i18n.rs");
|
||||||
|
if let Err(e) = std::fs::write(&out, craft_i18n()) {
|
||||||
|
println!("cargo::error=writing {}: {e}", out.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The catalog this app reads in each locale: `locales/<locale>/pdfcraft.tsv`.
|
||||||
|
const APP_CATALOG: &str = "pdfcraft.tsv";
|
||||||
|
|
||||||
|
/// `CRAFT_I18N` from `CRAFT_I18N_DIR`: the `Embedded` list the craft-i18n crate reads.
|
||||||
|
fn craft_i18n() -> String {
|
||||||
|
println!("cargo::rerun-if-env-changed=CRAFT_I18N_DIR");
|
||||||
|
println!("cargo::rerun-if-env-changed=CRAFT_I18N_REQUIRED");
|
||||||
|
let required = std::env::var("CRAFT_I18N_REQUIRED").is_ok_and(|v| v.trim() == "1");
|
||||||
|
let entries = match std::env::var_os("CRAFT_I18N_DIR").filter(|d| !d.is_empty()) {
|
||||||
|
// Not canonicalized: on Windows that gives `\\?\C:\…`, which `include_str!` can't read.
|
||||||
|
Some(dir) => match locales(&std::path::PathBuf::from(dir).join("locales")) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(e) => {
|
||||||
|
let level = if required { "error" } else { "warning" };
|
||||||
|
println!("cargo::{level}=CRAFT_I18N_DIR: {e}; the interface is English only");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
if required {
|
||||||
|
println!("cargo::error=CRAFT_I18N_REQUIRED=1 but CRAFT_I18N_DIR is unset: a release must embed the translations");
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut o = String::from("/// (locale, shared catalog, PdfCraft's catalog), one entry per locale; `\"\"` for a missing file.\n");
|
||||||
|
o.push_str("pub static CRAFT_I18N: &craft_i18n::Embedded = &[\n");
|
||||||
|
for (code, shared, app) in entries {
|
||||||
|
// `{:?}` escapes the path (Windows backslashes) as a Rust string literal.
|
||||||
|
let shared = shared.map_or_else(|| "\"\"".to_string(), |p| format!("include_str!({p:?})"));
|
||||||
|
let _ = writeln!(o, " ({code:?}, {shared}, include_str!({app:?})),");
|
||||||
|
}
|
||||||
|
o.push_str("];\n");
|
||||||
|
o
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One embedded locale: its code, its `common.tsv` if it has one, and its PdfCraft catalog.
|
||||||
|
type Locale = (String, Option<std::path::PathBuf>, std::path::PathBuf);
|
||||||
|
|
||||||
|
/// Every locale under `dir` with PdfCraft's catalog, by code. An error when `dir` isn't a
|
||||||
|
/// craft-i18n `locales/` directory.
|
||||||
|
fn locales(dir: &std::path::Path) -> Result<Vec<Locale>, String> {
|
||||||
|
// Build scripts run in the crate's directory and `include_str!` resolves against the
|
||||||
|
// generated file, so a relative path would mean two different places.
|
||||||
|
if !dir.is_absolute() {
|
||||||
|
return Err(format!("{} is not an absolute path", dir.display()));
|
||||||
|
}
|
||||||
|
// Before read_dir, so a checkout that appears later reruns the script.
|
||||||
|
println!("cargo::rerun-if-changed={}", dir.display());
|
||||||
|
let list = std::fs::read_dir(dir).map_err(|e| format!("{} is not a craft-i18n checkout ({e})", dir.display()))?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for entry in list.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let Some(code) = path.file_name().and_then(|n| n.to_str()).map(str::to_string) else { continue };
|
||||||
|
let app = path.join(APP_CATALOG);
|
||||||
|
if !app.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("cargo::rerun-if-changed={}", app.display());
|
||||||
|
let shared = Some(path.join("common.tsv")).filter(|p| p.is_file());
|
||||||
|
if let Some(p) = &shared {
|
||||||
|
println!("cargo::rerun-if-changed={}", p.display());
|
||||||
|
}
|
||||||
|
out.push((code, shared, app));
|
||||||
|
}
|
||||||
|
if out.is_empty() {
|
||||||
|
return Err(format!("{} has no locale with a {APP_CATALOG}", dir.display()));
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read<T>(path: &std::path::Path, fallback: &str, parse: impl Fn(&str) -> Result<T, String>) -> Option<T> {
|
fn read<T>(path: &std::path::Path, fallback: &str, parse: impl Fn(&str) -> Result<T, String>) -> Option<T> {
|
||||||
|
|||||||
@@ -878,6 +878,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn localized_shortcut_labels_follow_language_and_platform() {
|
fn localized_shortcut_labels_follow_language_and_platform() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let previous = crate::i18n::current();
|
let previous = crate::i18n::current();
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
for os in [OperatingSystem::Windows, OperatingSystem::Nix] {
|
for os in [OperatingSystem::Windows, OperatingSystem::Nix] {
|
||||||
|
|||||||
@@ -1092,6 +1092,9 @@ mod revert_draft_tests {
|
|||||||
/// interface language; a drawable value says nothing.
|
/// interface language; a drawable value says nothing.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_value_its_font_cannot_draw_is_kept_and_explained() {
|
fn a_value_its_font_cannot_draw_is_kept_and_explained() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut app = app();
|
let mut app = app();
|
||||||
let fill =
|
let fill =
|
||||||
|app: &mut PdfCraftApp, text: &str| app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text(text.into()) });
|
|app: &mut PdfCraftApp, text: &str| app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text(text.into()) });
|
||||||
|
|||||||
@@ -317,6 +317,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn translated_card_description_leaves_room_for_the_action() {
|
fn translated_card_description_leaves_room_for_the_action() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let de = crate::i18n::Lang::from_code("de").unwrap();
|
let de = crate::i18n::Lang::from_code("de").unwrap();
|
||||||
crate::i18n::set_current(de);
|
crate::i18n::set_current(de);
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
|
|||||||
@@ -799,6 +799,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hotbar_search_matches_translated_and_canonical_labels() {
|
fn hotbar_search_matches_translated_and_canonical_labels() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let ink = command("comment.ink").unwrap();
|
let ink = command("comment.ink").unwrap();
|
||||||
crate::i18n::set_current(crate::i18n::lang_from_tag("fr").unwrap());
|
crate::i18n::set_current(crate::i18n::lang_from_tag("fr").unwrap());
|
||||||
assert_eq!(tl!(ink.label), "Dessiner à main levée");
|
assert_eq!(tl!(ink.label), "Dessiner à main levée");
|
||||||
|
|||||||
+231
-279
@@ -1,19 +1,28 @@
|
|||||||
//! UI localisation. Strings in code stay English and are the default lookup keys; a per-language
|
//! UI localisation. Strings in code stay English and are the default lookup keys; the craft-i18n
|
||||||
//! catalog (`*.tsv`, see `ja.tsv` for the format) maps them to display text at render time. Command
|
//! catalogs map them to display text at render time. Command ids, document text, file names, the
|
||||||
//! ids, document text, file names, the control channel, the CLI and MCP never see translated text,
|
//! control channel, the CLI and MCP never see translated text, so agents and scripts are
|
||||||
//! so agents and scripts are unaffected. A string without a translation is shown in English, so
|
//! unaffected. A string without a translation is shown in English, so coverage can grow
|
||||||
//! coverage can grow incrementally.
|
//! incrementally, and a missing translation never fails a build, a test or a release.
|
||||||
//!
|
//!
|
||||||
//! The system is the one PhotoCraft uses (`photocraft/crates/ui-egui/src/i18n`), so catalogs and
|
//! The catalogs live in [storytold/craft-i18n](https://github.com/storytold/craft-i18n)
|
||||||
//! habits carry over between the two apps.
|
//! (`locales/<code>/common.tsv`, shared by the Crafting Apps, and `pdfcraft.tsv`), not in this
|
||||||
|
//! repository. `build.rs` embeds them from `CRAFT_I18N_DIR` (CI and releases check out the commit
|
||||||
|
//! in `craft-i18n.rev`); without it the build is English only. Lookup, fallback (PdfCraft's rows
|
||||||
|
//! over shared ones), plural forms (CLDR) and language names are the `craft-i18n` crate's
|
||||||
|
//! (craft-libs). Which languages are offered, how the system locale picks one, right to left and
|
||||||
|
//! `{name}` placeholders stay here.
|
||||||
|
//!
|
||||||
|
//! # Adding or changing a translation
|
||||||
|
//! In craft-i18n: add rows to `locales/<code>/pdfcraft.tsv` (its `docs/integration.md`). They show
|
||||||
|
//! here once `craft-i18n.rev` moves to a commit that has them. A feature PR here adds the English
|
||||||
|
//! only.
|
||||||
//!
|
//!
|
||||||
//! # Adding a language
|
//! # Adding a language
|
||||||
//! 1. Add `xx.tsv` next to `ja.tsv` (copy its header; translate from the *meaning* of the English
|
//! 1. craft-i18n gets `locales/<code>/pdfcraft.tsv` with its `@id language.name` row.
|
||||||
//! text, clean-room, see `ja.tsv`).
|
//! 2. Add one row to [`LANGUAGES`] (code, right to left), and its English name to
|
||||||
//! 2. Add one row to [`LANGUAGES`] (code, native name, catalog, plural rule).
|
//! [`Lang::english_name`].
|
||||||
//!
|
//!
|
||||||
//! That is all: the Preferences dropdown, the system-locale match and the catalog tests (parse,
|
//! The Preferences dropdown and the system-locale match pick it up from there.
|
||||||
//! placeholders, plural forms) pick it up from the registry.
|
|
||||||
//!
|
//!
|
||||||
//! # Looking strings up
|
//! # Looking strings up
|
||||||
//! - `tl!` / [`t`]: a plain string in the current language. [`tr`]: in a given one.
|
//! - `tl!` / [`t`]: a plain string in the current language. [`tr`]: in a given one.
|
||||||
@@ -23,323 +32,173 @@
|
|||||||
//! - [`trn`]: plural-aware (`{n}` is filled in). [`fmt`]: fill `{name}` placeholders after [`tr`];
|
//! - [`trn`]: plural-aware (`{n}` is filled in). [`fmt`]: fill `{name}` placeholders after [`tr`];
|
||||||
//! translators may reorder placeholders freely.
|
//! translators may reorder placeholders freely.
|
||||||
|
|
||||||
mod catalog;
|
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::sync::OnceLock;
|
use std::collections::HashSet;
|
||||||
|
use std::sync::{LazyLock, OnceLock};
|
||||||
|
|
||||||
use catalog::Catalog;
|
use craft_i18n::I18n;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/craft_i18n.rs"));
|
||||||
|
|
||||||
|
/// The embedded catalogs, offering exactly the languages in [`LANGUAGES`]. Built on first use; a
|
||||||
|
/// locale is parsed on its first lookup.
|
||||||
|
static I18N: LazyLock<I18n> = LazyLock::new(|| {
|
||||||
|
let offered: Vec<&str> = LANGUAGES.iter().map(|l| l.code).collect();
|
||||||
|
let (i18n, problems) = I18n::new(CRAFT_I18N, &offered);
|
||||||
|
for problem in problems {
|
||||||
|
log::warn!("interface catalogs: {problem}");
|
||||||
|
}
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
/// The `language` preference value that follows the system locale.
|
/// The `language` preference value that follows the system locale.
|
||||||
pub const AUTO: &str = "auto";
|
pub const AUTO: &str = "auto";
|
||||||
|
|
||||||
/// One supported UI language.
|
/// One UI language PdfCraft offers, when its catalogs are embedded: what the app needs beyond
|
||||||
|
/// them. Its name and translations come from craft-i18n.
|
||||||
pub struct LangInfo {
|
pub struct LangInfo {
|
||||||
/// BCP 47 code, lowercase (`ja`, `zh-hans`, `pt-br`). Also the `language` preference value.
|
/// BCP 47 code, lowercase (`ja`, `zh-hans`, `pt-br`). Also the `language` preference value and
|
||||||
|
/// the craft-i18n locale.
|
||||||
pub code: &'static str,
|
pub code: &'static str,
|
||||||
/// The language's name in itself, shown in the Preferences dropdown.
|
|
||||||
pub name: &'static str,
|
|
||||||
/// Catalog file contents (empty for the built-in English).
|
|
||||||
pub source: &'static str,
|
|
||||||
/// Plural form index for a count (English: 0 = one, 1 = other; Japanese: always 0). A catalog's
|
|
||||||
/// `@plural` entries list one form per index.
|
|
||||||
pub plural: fn(u64) -> usize,
|
|
||||||
/// The language is written right to left. Catalogs stay in logical order: the egui 0.37 fork
|
/// The language is written right to left. Catalogs stay in logical order: the egui 0.37 fork
|
||||||
/// owns bidi shaping/reordering (E5/E12), so preordering here would reverse Arabic twice.
|
/// owns bidi shaping/reordering (E5/E12), so preordering here would reverse Arabic twice.
|
||||||
pub rtl: bool,
|
pub rtl: bool,
|
||||||
catalog: OnceLock<Catalog>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plural_one_other(n: u64) -> usize {
|
/// How many languages [`LANGUAGES`] lists.
|
||||||
usize::from(n != 1)
|
const LANGUAGE_COUNT: usize = 35;
|
||||||
}
|
|
||||||
|
|
||||||
fn plural_none(_: u64) -> usize {
|
/// The languages PdfCraft offers. English first: it is the fallback and the source language.
|
||||||
0
|
pub static LANGUAGES: [LangInfo; LANGUAGE_COUNT] = [
|
||||||
}
|
LangInfo { code: "en", rtl: false },
|
||||||
|
LangInfo { code: "ja", rtl: false },
|
||||||
/// Romanian (CLDR `ro`): 1 is `one`; 0 and numbers whose last two digits are 1–19 (2, 19, 101,
|
|
||||||
/// 119) are `few`; everything else (20, 100, 120) is `other`, the form with "de" ("20 de pagini").
|
|
||||||
fn plural_ro(n: u64) -> usize {
|
|
||||||
if n == 1 {
|
|
||||||
0
|
|
||||||
} else if n == 0 || (1..=19).contains(&(n % 100)) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lithuanian: one (ending in 1 except 11), few (ending in 2–9 except 12–19), other otherwise.
|
|
||||||
fn plural_lithuanian(n: u64) -> usize {
|
|
||||||
match (n % 10, n % 100) {
|
|
||||||
(1, 11) => 2,
|
|
||||||
(1, _) => 0,
|
|
||||||
(2..=9, 12..=19) => 2,
|
|
||||||
(2..=9, _) => 1,
|
|
||||||
_ => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Czech: 1 → one, 2–4 → few, everything else (0, 5+) → other.
|
|
||||||
fn plural_cs(n: u64) -> usize {
|
|
||||||
match n {
|
|
||||||
1 => 0,
|
|
||||||
2..=4 => 1,
|
|
||||||
_ => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Portuguese: 0 and 1 take the singular, everything else the plural.
|
|
||||||
fn plural_pt(n: u64) -> usize {
|
|
||||||
usize::from(n > 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// French (CLDR `fr`): 0 and 1 take the singular, everything else the plural.
|
|
||||||
fn plural_fr(n: u64) -> usize {
|
|
||||||
usize::from(n > 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Russian: 1 (but not 11) is `one`, 2–4 (but not 12–14) `few`, everything else `many`.
|
|
||||||
fn plural_russian(n: u64) -> usize {
|
|
||||||
match n % 100 {
|
|
||||||
11..=14 => 2,
|
|
||||||
_ => match n % 10 {
|
|
||||||
1 => 0,
|
|
||||||
2..=4 => 1,
|
|
||||||
_ => 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ukrainian (CLDR `uk`, integer counts): one (1, 21), few (2–4, 22–24), many (0, 5–19).
|
|
||||||
/// Counts ending in 11–14 always take many, regardless of the last digit.
|
|
||||||
fn plural_ukrainian(n: u64) -> usize {
|
|
||||||
match n % 100 {
|
|
||||||
11..=14 => 2,
|
|
||||||
_ => match n % 10 {
|
|
||||||
1 => 0,
|
|
||||||
2..=4 => 1,
|
|
||||||
_ => 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Arabic (CLDR `ar`, integer counts): zero (0), one (1), two (2), few (3–10, 103–110),
|
|
||||||
/// many (11–99, 111–199), other (100–102, 200–202).
|
|
||||||
fn plural_arabic(n: u64) -> usize {
|
|
||||||
match (n, n % 100) {
|
|
||||||
(0, _) => 0,
|
|
||||||
(1, _) => 1,
|
|
||||||
(2, _) => 2,
|
|
||||||
(_, 3..=10) => 3,
|
|
||||||
(_, 11..=99) => 4,
|
|
||||||
_ => 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Polish integer counts: 1 is one; endings 2–4 except 12–14 are few; the rest are many.
|
|
||||||
fn plural_polish(n: u64) -> usize {
|
|
||||||
match (n, n % 100, n % 10) {
|
|
||||||
(1, _, _) => 0,
|
|
||||||
(_, 12..=14, _) => 2,
|
|
||||||
(_, _, 2..=4) => 1,
|
|
||||||
_ => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Slovene (CLDR `sl`): one (1, 101, 201 ...) | two (2, 102 ...) | few (3, 4, 103 ...) | other.
|
|
||||||
fn plural_sl(n: u64) -> usize {
|
|
||||||
match n % 100 {
|
|
||||||
1 => 0,
|
|
||||||
2 => 1,
|
|
||||||
3 | 4 => 2,
|
|
||||||
_ => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The registry. English first: it is the fallback and the source language.
|
|
||||||
pub static LANGUAGES: [LangInfo; 35] = [
|
|
||||||
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
|
||||||
LangInfo { code: "ja", name: "日本語", source: include_str!("i18n/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`).
|
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
|
||||||
LangInfo {
|
LangInfo { code: "zh-hans", rtl: false },
|
||||||
code: "zh-hans",
|
|
||||||
name: "简体中文",
|
|
||||||
source: include_str!("i18n/zh-hans.tsv"),
|
|
||||||
plural: plural_none,
|
|
||||||
rtl: false,
|
|
||||||
catalog: OnceLock::new(),
|
|
||||||
},
|
|
||||||
// Traditional Chinese in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*`
|
// Traditional Chinese in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*`
|
||||||
// locales all resolve here (see `candidates`).
|
// locales all resolve here (see `candidates`).
|
||||||
LangInfo {
|
LangInfo { code: "zh-hant", rtl: false },
|
||||||
code: "zh-hant",
|
LangInfo { code: "cs", rtl: false },
|
||||||
name: "繁體中文",
|
|
||||||
source: include_str!("i18n/zh-hant.tsv"),
|
|
||||||
plural: plural_none,
|
|
||||||
rtl: false,
|
|
||||||
catalog: OnceLock::new(),
|
|
||||||
},
|
|
||||||
LangInfo { code: "cs", name: "Čeština", source: include_str!("i18n/cs.tsv"), plural: plural_cs, rtl: false, catalog: OnceLock::new() },
|
|
||||||
// Brazilian Portuguese; `pt`, `pt-BR` and `pt-PT` locales all resolve here (see `candidates`).
|
// Brazilian Portuguese; `pt`, `pt-BR` and `pt-PT` locales all resolve here (see `candidates`).
|
||||||
LangInfo {
|
LangInfo { code: "pt-br", rtl: false },
|
||||||
code: "pt-br",
|
|
||||||
name: "Português (Brasil)",
|
|
||||||
source: include_str!("i18n/pt-br.tsv"),
|
|
||||||
plural: plural_pt,
|
|
||||||
rtl: false,
|
|
||||||
catalog: OnceLock::new(),
|
|
||||||
},
|
|
||||||
// German (informal "du"); every `de-*` locale (`de-DE`, `de-AT`, `de-CH` ...) resolves here.
|
// German (informal "du"); every `de-*` locale (`de-DE`, `de-AT`, `de-CH` ...) resolves here.
|
||||||
LangInfo { code: "de", name: "Deutsch", source: include_str!("i18n/de.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "de", rtl: false },
|
||||||
// Spanish (European vocabulary); every `es-*` locale (`es-ES`, `es-MX`, `es-419` ...) resolves here.
|
// Spanish (European vocabulary); every `es-*` locale (`es-ES`, `es-MX`, `es-419` ...) resolves here.
|
||||||
LangInfo { code: "es", name: "Español", source: include_str!("i18n/es.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "es", rtl: false },
|
||||||
// French; every `fr-*` locale (`fr-FR`, `fr-CA`, `fr-BE` ...) resolves here.
|
// French; every `fr-*` locale (`fr-FR`, `fr-CA`, `fr-BE` ...) resolves here.
|
||||||
LangInfo { code: "fr", name: "Français", source: include_str!("i18n/fr.tsv"), plural: plural_fr, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "fr", rtl: false },
|
||||||
// Russian; every `ru-*` locale (`ru-RU`, `ru-BY`, `ru-KZ` ...) resolves here.
|
// Russian; every `ru-*` locale (`ru-RU`, `ru-BY`, `ru-KZ` ...) resolves here.
|
||||||
LangInfo {
|
LangInfo { code: "ru", rtl: false },
|
||||||
code: "ru", name: "Русский", source: include_str!("i18n/ru.tsv"), plural: plural_russian, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Kazakh; every `kk-*` locale (`kk-KZ`, `kk-KZ.UTF-8`) resolves here.
|
// Kazakh; every `kk-*` locale (`kk-KZ`, `kk-KZ.UTF-8`) resolves here.
|
||||||
LangInfo {
|
LangInfo { code: "kk", rtl: false },
|
||||||
code: "kk", name: "Қазақша", source: include_str!("i18n/kk.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Bulgarian; every `bg-*` locale (`bg-BG`) resolves here.
|
// Bulgarian; every `bg-*` locale (`bg-BG`) resolves here.
|
||||||
LangInfo {
|
LangInfo { code: "bg", rtl: false },
|
||||||
code: "bg", name: "Български", source: include_str!("i18n/bg.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Telugu; every `te-*` locale (`te-IN`) resolves here.
|
// Telugu; every `te-*` locale (`te-IN`) resolves here.
|
||||||
LangInfo {
|
LangInfo { code: "te", rtl: false },
|
||||||
code: "te", name: "తెలుగు", source: include_str!("i18n/te.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Hungarian; every `hu-*` locale (`hu-HU`) resolves here.
|
// Hungarian; every `hu-*` locale (`hu-HU`) resolves here.
|
||||||
LangInfo { code: "hu", name: "Magyar", source: include_str!("i18n/hu.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "hu", rtl: false },
|
||||||
// Ukrainian; every `uk-*` locale (`uk-UA`, `uk_UA.UTF-8`) resolves here.
|
// Ukrainian; every `uk-*` locale (`uk-UA`, `uk_UA.UTF-8`) resolves here.
|
||||||
LangInfo {
|
LangInfo { code: "uk", rtl: false },
|
||||||
code: "uk", name: "Українська", source: include_str!("i18n/uk.tsv"), plural: plural_ukrainian, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Italian; every `it-*` locale (`it-IT`) resolves here.
|
// Italian; every `it-*` locale (`it-IT`) resolves here.
|
||||||
LangInfo { code: "it", name: "Italiano", source: include_str!("i18n/it.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "it", rtl: false },
|
||||||
// Arabic (Modern Standard, Western digits); every `ar-*` locale (`ar-MA`, `ar-EG`, `ar-SA` ...)
|
// Arabic (Modern Standard, Western digits); every `ar-*` locale (`ar-MA`, `ar-EG`, `ar-SA` ...)
|
||||||
// resolves here. Right to left: see `LangInfo::rtl`.
|
// resolves here. Right to left: see `LangInfo::rtl`.
|
||||||
LangInfo { code: "ar", name: "العربية", source: include_str!("i18n/ar.tsv"), plural: plural_arabic, rtl: true, catalog: OnceLock::new() },
|
LangInfo { code: "ar", rtl: true },
|
||||||
// Central Kurdish (Sorani); every `ckb-*` locale (`ckb-IQ`, `ckb-IR`) resolves here. Right to
|
// Central Kurdish (Sorani); every `ckb-*` locale (`ckb-IQ`, `ckb-IR`) resolves here. Right to
|
||||||
// left like Arabic: see `LangInfo::rtl`. CLDR `ckb` counts have two forms (one, other).
|
// left like Arabic: see `LangInfo::rtl`. CLDR `ckb` counts have two forms (one, other).
|
||||||
LangInfo {
|
LangInfo { code: "ckb", rtl: true },
|
||||||
code: "ckb",
|
|
||||||
name: "کوردیی ناوەندی",
|
|
||||||
source: include_str!("i18n/ckb.tsv"),
|
|
||||||
plural: plural_one_other,
|
|
||||||
rtl: true,
|
|
||||||
catalog: OnceLock::new(),
|
|
||||||
},
|
|
||||||
// Persian (Farsi); every `fa-*` locale (`fa-IR`, `fa_IR.UTF-8`, `fa-AF` ...) resolves here. Right
|
// Persian (Farsi); every `fa-*` locale (`fa-IR`, `fa_IR.UTF-8`, `fa-AF` ...) resolves here. Right
|
||||||
// to left like Arabic: see `LangInfo::rtl`. CLDR `fa` counts have two forms (one: i = 0 or n = 1,
|
// to left like Arabic: see `LangInfo::rtl`. CLDR `fa` counts have two forms (one: i = 0 or n = 1,
|
||||||
// other), which for whole counts is the French rule, so `@plural` entries list two forms.
|
// other), which for whole counts is the French rule, so `@plural` entries list two forms.
|
||||||
LangInfo { code: "fa", name: "فارسی", source: include_str!("i18n/fa.tsv"), plural: plural_fr, rtl: true, catalog: OnceLock::new() },
|
LangInfo { code: "fa", rtl: true },
|
||||||
// Polish; every `pl-*` locale (`pl-pl`, `pl_pl.UTF-8`) resolves here.
|
// Polish; every `pl-*` locale (`pl-pl`, `pl_pl.UTF-8`) resolves here.
|
||||||
LangInfo { code: "pl", name: "Polski", source: include_str!("i18n/pl.tsv"), plural: plural_polish, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "pl", rtl: false },
|
||||||
// Slovene; every `sl-*` locale (`sl-SI`) resolves here. Four plural forms (one, two, few, other).
|
// Slovene; every `sl-*` locale (`sl-SI`) resolves here. Four plural forms (one, two, few, other).
|
||||||
LangInfo { code: "sl", name: "Slovenščina", source: include_str!("i18n/sl.tsv"), plural: plural_sl, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "sl", rtl: false },
|
||||||
LangInfo {
|
LangInfo { code: "el", rtl: false },
|
||||||
code: "el", name: "Ελληνικά", source: include_str!("i18n/el.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new()
|
// Turkish; every `tr-*` locale (`tr-TR`, `tr_TR.UTF-8`) resolves here. CLDR `tr` has two forms
|
||||||
},
|
// (one, other); the catalog's counted nouns read the same in both, so its `@plural` rows repeat
|
||||||
// Turkish; every `tr-*` locale (`tr-TR`, `tr_TR.UTF-8`) resolves here.
|
// the one form.
|
||||||
LangInfo { code: "tr", name: "Türkçe", source: include_str!("i18n/tr.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "tr", rtl: false },
|
||||||
// Lithuanian; every `lt-*` locale (`lt-LT`, `lt_LT.UTF-8`) resolves here. Three plural forms (one, few, other).
|
// Lithuanian; every `lt-*` locale (`lt-LT`, `lt_LT.UTF-8`) resolves here. Three plural forms (one, few, other).
|
||||||
LangInfo { code: "lt", name: "Lietuvių", source: include_str!("i18n/lt.tsv"), plural: plural_lithuanian, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "lt", rtl: false },
|
||||||
// Indonesian; every `id-*` locale (`id-ID`, `id_ID.UTF-8`) resolves here. CLDR `id` counts have
|
// Indonesian; every `id-*` locale (`id-ID`, `id_ID.UTF-8`) resolves here. CLDR `id` counts have
|
||||||
// one form ("other"), so `@plural` entries list a single form.
|
// one form ("other"), so `@plural` entries list a single form.
|
||||||
LangInfo { code: "id", name: "Bahasa Indonesia", source: include_str!("i18n/id.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "id", rtl: false },
|
||||||
// Vietnamese; every `vi-*` locale (`vi-VN`, `vi_VN.UTF-8`) resolves here. CLDR `vi` counts have
|
// 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.
|
// one form ("other"), so `@plural` entries list a single form.
|
||||||
LangInfo { code: "vi", name: "Tiếng Việt", source: include_str!("i18n/vi.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "vi", rtl: false },
|
||||||
// Finnish; every `fi-*` locale (`fi-FI`, `fi_FI.UTF-8`) resolves here. Two plural forms (one, other):
|
// Finnish; every `fi-*` locale (`fi-FI`, `fi_FI.UTF-8`) resolves here. Two plural forms (one, other):
|
||||||
// the partitive singular follows any number but 1 (`1 sivu`, `2 sivua`).
|
// the partitive singular follows any number but 1 (`1 sivu`, `2 sivua`).
|
||||||
LangInfo { code: "fi", name: "Suomi", source: include_str!("i18n/fi.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "fi", rtl: false },
|
||||||
// Albanian; every `sq-*` locale (`sq-AL`, `sq_AL.UTF-8`, `sq-XK`) resolves here. One/other plural, like English.
|
// Albanian; every `sq-*` locale (`sq-AL`, `sq_AL.UTF-8`, `sq-XK`) resolves here. One/other plural, like English.
|
||||||
LangInfo { code: "sq", name: "Shqip", source: include_str!("i18n/sq.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "sq", rtl: false },
|
||||||
// Thai; every `th-*` locale (`th-TH`, `th_TH.UTF-8`) resolves here. No grammatical plural.
|
// Thai; every `th-*` locale (`th-TH`, `th_TH.UTF-8`) resolves here. No grammatical plural.
|
||||||
LangInfo { code: "th", name: "ไทย", source: include_str!("i18n/th.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "th", rtl: false },
|
||||||
// Catalan; every `ca-*` locale (`ca-ES`, `ca_ES.UTF-8`, `ca_ES@valencia`) resolves here.
|
// Catalan; every `ca-*` locale (`ca-ES`, `ca_ES.UTF-8`, `ca_ES@valencia`) resolves here.
|
||||||
LangInfo { code: "ca", name: "Català", source: include_str!("i18n/ca.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "ca", rtl: false },
|
||||||
// Romanian; every `ro-*` locale (`ro-RO`, `ro_RO.UTF-8`, `ro-MD`) resolves here. Three plural forms (one, few, other).
|
// Romanian; every `ro-*` locale (`ro-RO`, `ro_RO.UTF-8`, `ro-MD`) resolves here. Three plural forms (one, few, other).
|
||||||
LangInfo { code: "ro", name: "Română", source: include_str!("i18n/ro.tsv"), plural: plural_ro, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "ro", rtl: false },
|
||||||
// Lao; every `lo-*` locale (`lo-LA`, `lo_LA.UTF-8` ...) resolves here.
|
// Lao; every `lo-*` locale (`lo-LA`, `lo_LA.UTF-8` ...) resolves here.
|
||||||
LangInfo {
|
LangInfo { code: "lo", rtl: false },
|
||||||
code: "lo", name: "ພາສາລາວ", source: include_str!("i18n/lo.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Hindi; every `hi-*` locale (`hi-IN`, `hi_IN.UTF-8`) resolves here. CLDR `hi` has "one"
|
// Hindi; every `hi-*` locale (`hi-IN`, `hi_IN.UTF-8`) resolves here. CLDR `hi` has "one"
|
||||||
// (0 and 1) and "other", but the catalog's nouns are the same in both ("{n} पृष्ठ"), so
|
// (0 and 1) and "other", but the catalog's nouns are the same in both ("{n} पृष्ठ"), so its
|
||||||
// `@plural` entries list a single form.
|
// `@plural` rows repeat the one form.
|
||||||
LangInfo {
|
LangInfo { code: "hi", rtl: false },
|
||||||
code: "hi", name: "हिन्दी", source: include_str!("i18n/hi.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new()
|
|
||||||
},
|
|
||||||
// Korean; every `ko-*` locale (`ko-KR`, `ko_KR.UTF-8`) resolves here. CLDR `ko` counts have
|
// Korean; every `ko-*` locale (`ko-KR`, `ko_KR.UTF-8`) resolves here. CLDR `ko` counts have
|
||||||
// one form ("other"), so `@plural` entries list a single form.
|
// one form ("other"), so `@plural` entries list a single form.
|
||||||
LangInfo { code: "ko", name: "한국어", source: include_str!("i18n/ko.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
LangInfo { code: "ko", rtl: false },
|
||||||
// Norwegian Bokmål; every `nb-*` locale resolves here, and so do general Norwegian (`no-*`) and
|
// Norwegian Bokmål; every `nb-*` locale resolves here, and so do general Norwegian (`no-*`) and
|
||||||
// Nynorsk (`nn-*`), which have no catalog of their own (see `candidates`).
|
// Nynorsk (`nn-*`), which have no catalog of their own (see `candidates`).
|
||||||
LangInfo {
|
LangInfo { code: "nb", rtl: false },
|
||||||
code: "nb",
|
|
||||||
name: "Norsk bokmål",
|
|
||||||
source: include_str!("i18n/nb.tsv"),
|
|
||||||
plural: plural_one_other,
|
|
||||||
rtl: false,
|
|
||||||
catalog: OnceLock::new(),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
impl LangInfo {
|
/// Per language of [`LANGUAGES`], the (context, source) keys of PdfCraft's own catalog as written
|
||||||
/// How many plural forms the language's `@plural` entries list.
|
/// (escaped), read on first use ([`Lang::has_own`]).
|
||||||
fn plural_forms(&self) -> usize {
|
static OWN_ROWS: [OnceLock<HashSet<(&'static str, &'static str)>>; LANGUAGE_COUNT] = [const { OnceLock::new() }; LANGUAGE_COUNT];
|
||||||
(0..=1000).map(self.plural).max().unwrap_or(0).saturating_add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the parsed catalogue, building it on first use and warning about entries that fall back to
|
/// A language the UI can be shown in: English, or one of [`LANGUAGES`] whose catalogs are
|
||||||
/// English.
|
/// embedded.
|
||||||
fn catalog(&self) -> &Catalog {
|
|
||||||
self.catalog.get_or_init(|| {
|
|
||||||
let (catalog, errors) = Catalog::parse(self.source, self.plural_forms());
|
|
||||||
for error in errors {
|
|
||||||
log::warn!("{} interface catalog: {error}; that entry shows in English", self.code);
|
|
||||||
}
|
|
||||||
// Catalogs keep logical text: the fork shapes and reorders each run once. Accessibility
|
|
||||||
// names, copied text and search therefore see the same logical string as the source.
|
|
||||||
catalog
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A language the UI can be shown in (a handle into [`LANGUAGES`]).
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Lang(&'static LangInfo);
|
pub struct Lang {
|
||||||
|
lang: craft_i18n::Lang,
|
||||||
|
/// The catalogs it is looked up in: the embedded ones ([`I18N`]), or a test fixture's.
|
||||||
|
catalogs: Option<&'static I18n>,
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Lang {
|
impl std::fmt::Debug for Lang {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "Lang({})", self.0.code)
|
write!(f, "Lang({})", self.code())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Lang {
|
impl PartialEq for Lang {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.0.code == other.0.code
|
self.code() == other.code()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Lang {}
|
impl Eq for Lang {}
|
||||||
|
|
||||||
impl Lang {
|
impl Lang {
|
||||||
pub const EN: Lang = Lang(&LANGUAGES[0]);
|
pub const EN: Lang = Lang { lang: craft_i18n::Lang::EN, catalogs: None };
|
||||||
|
|
||||||
pub fn code(self) -> &'static str {
|
fn i18n(self) -> &'static I18n {
|
||||||
self.0.code
|
self.catalogs.unwrap_or_else(|| LazyLock::force(&I18N))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A language by its exact code.
|
fn info(self) -> Option<&'static LangInfo> {
|
||||||
|
let code = self.code();
|
||||||
|
LANGUAGES.iter().find(|l| l.code == code)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code(self) -> &'static str {
|
||||||
|
self.i18n().code(self.lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A language by its exact code (any case): one of [`LANGUAGES`] whose catalogs are embedded.
|
||||||
pub fn from_code(code: &str) -> Option<Lang> {
|
pub fn from_code(code: &str) -> Option<Lang> {
|
||||||
LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code)).map(Lang)
|
let info = LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code))?;
|
||||||
|
I18N.from_code(info.code).map(|lang| Lang { lang, catalogs: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the `language` preference: a language code, or [`AUTO`] (and anything unknown,
|
/// Resolve the `language` preference: a language code, or [`AUTO`] (and anything unknown,
|
||||||
@@ -348,9 +207,10 @@ impl Lang {
|
|||||||
Lang::from_code(pref).unwrap_or_else(system_lang)
|
Lang::from_code(pref).unwrap_or_else(system_lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every registered language.
|
/// Every language the interface can be shown in: [`LANGUAGES`] in order, those whose catalogs
|
||||||
|
/// are embedded (English alone in a build without craft-i18n).
|
||||||
pub fn all() -> impl Iterator<Item = Lang> {
|
pub fn all() -> impl Iterator<Item = Lang> {
|
||||||
LANGUAGES.iter().map(Lang)
|
LANGUAGES.iter().filter_map(|l| Lang::from_code(l.code))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every registered language in the order a picker lists them: English first (the source
|
/// Every registered language in the order a picker lists them: English first (the source
|
||||||
@@ -372,8 +232,10 @@ impl Lang {
|
|||||||
std::iter::once(Lang::EN).chain(rest).collect()
|
std::iter::once(Lang::EN).chain(rest).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The language's name in itself (its `@id language.name` row), shown in the Preferences
|
||||||
|
/// dropdown.
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
self.0.name
|
self.i18n().name(self.lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The language's name in English, used by [`picker_name`](Self::picker_name) when this
|
/// The language's name in English, used by [`picker_name`](Self::picker_name) when this
|
||||||
@@ -417,7 +279,7 @@ impl Lang {
|
|||||||
"ko" => "Korean",
|
"ko" => "Korean",
|
||||||
// Only reached if a language is added to `LANGUAGES` without an English name; the
|
// Only reached if a language is added to `LANGUAGES` without an English name; the
|
||||||
// code still tells the reader which entry it is.
|
// code still tells the reader which entry it is.
|
||||||
_ => self.0.code,
|
code => code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,12 +294,46 @@ impl Lang {
|
|||||||
/// Whether the language is written right to left. Lookups stay logical; the egui fork
|
/// Whether the language is written right to left. Lookups stay logical; the egui fork
|
||||||
/// shapes and reorders each run for display (see [`LangInfo::rtl`]).
|
/// shapes and reorders each run for display (see [`LangInfo::rtl`]).
|
||||||
pub fn rtl(self) -> bool {
|
pub fn rtl(self) -> bool {
|
||||||
self.0.rtl
|
self.info().is_some_and(|l| l.rtl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn catalog(self) -> &'static Catalog {
|
/// Whether PdfCraft's own catalog (`pdfcraft.tsv`, not the shared `common.tsv`) has the row
|
||||||
self.0.catalog()
|
/// (`context`, `source`). The shared catalog's `@id` rows are keyed by the other apps' command
|
||||||
|
/// ids, which can name another command (in Bulgarian and Indonesian its `file.close` row holds
|
||||||
|
/// an id rather than a label), so command-id lookups ask this; every `@id` row PdfCraft had is
|
||||||
|
/// in its own catalog. A test fixture has no shared catalog: its rows are its own.
|
||||||
|
fn has_own(self, context: &str, source: &str) -> bool {
|
||||||
|
if let Some(fixture) = self.catalogs {
|
||||||
|
return fixture.get(self.lang, context, source).is_some();
|
||||||
|
}
|
||||||
|
let code = self.code();
|
||||||
|
let Some(slot) = LANGUAGES.iter().position(|l| l.code == code).and_then(|i| OWN_ROWS.get(i)) else { return false };
|
||||||
|
let rows: &HashSet<(&str, &str)> = slot.get_or_init(|| {
|
||||||
|
let app = CRAFT_I18N.iter().find(|(c, ..)| c.eq_ignore_ascii_case(code)).map_or("", |(_, _, app)| *app);
|
||||||
|
app.lines()
|
||||||
|
.filter(|l| !l.starts_with('#'))
|
||||||
|
.filter_map(|l| {
|
||||||
|
let mut columns = l.split('\t');
|
||||||
|
Some((columns.next()?, columns.next()?))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
let escape = |s: &str| s.replace('\\', "\\\\").replace('\n', "\\n").replace('\t', "\\t");
|
||||||
|
rows.contains(&(escape(context).as_str(), escape(source).as_str()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The embedded catalog texts this language reads, the shared one then PdfCraft's; none for
|
||||||
|
/// English. For checks that the interface fonts draw every translation.
|
||||||
|
pub fn catalog_texts(self) -> impl Iterator<Item = &'static str> {
|
||||||
|
let code = self.code();
|
||||||
|
CRAFT_I18N.iter().filter(move |(c, ..)| c.eq_ignore_ascii_case(code)).flat_map(|(_, shared, app)| [*shared, *app])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this build embeds the craft-i18n catalogs (`CRAFT_I18N_DIR` was set). Without them the
|
||||||
|
/// interface is English only; tests that need a translation skip with a note.
|
||||||
|
pub fn catalogs_embedded() -> bool {
|
||||||
|
!CRAFT_I18N.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The canonical `language` preference for user input: [`AUTO`] or a registered code (any case).
|
/// The canonical `language` preference for user input: [`AUTO`] or a registered code (any case).
|
||||||
@@ -500,10 +396,8 @@ pub fn system_lang() -> Lang {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn detect_system_lang() -> Lang {
|
fn detect_system_lang() -> Lang {
|
||||||
for var in ["LC_ALL", "LC_MESSAGES", "LANG"] {
|
if let Some(l) = env_lang(|var| std::env::var(var).ok()) {
|
||||||
if let Some(l) = std::env::var(var).ok().and_then(|v| language_override(&v)) {
|
return l;
|
||||||
return l;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Apps started from the Finder don't inherit LANG: use the macOS preferred-languages list.
|
// Apps started from the Finder don't inherit LANG: use the macOS preferred-languages list.
|
||||||
// The absolute path keeps a `defaults` earlier on PATH from running; any failure means English.
|
// The absolute path keeps a `defaults` earlier on PATH from running; any failure means English.
|
||||||
@@ -521,6 +415,13 @@ fn detect_system_lang() -> Lang {
|
|||||||
Lang::EN
|
Lang::EN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The language the locale variables name, `LC_ALL` first, then `LC_MESSAGES`, then `LANG`;
|
||||||
|
/// `var` reads one.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn env_lang(var: impl Fn(&str) -> Option<String>) -> Option<Lang> {
|
||||||
|
["LC_ALL", "LC_MESSAGES", "LANG"].into_iter().find_map(|name| var(name).and_then(|v| language_override(&v)))
|
||||||
|
}
|
||||||
|
|
||||||
/// A named locale can override the display language. On Windows, the neutral
|
/// A named locale can override the display language. On Windows, the neutral
|
||||||
/// C/POSIX locale inherited from a launcher says nothing about the user's UI language.
|
/// C/POSIX locale inherited from a launcher says nothing about the user's UI language.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -622,7 +523,7 @@ pub fn current() -> Lang {
|
|||||||
|
|
||||||
/// Does `lang` have a catalog entry for this plain string? (English never does: it is the source.)
|
/// Does `lang` have a catalog entry for this plain string? (English never does: it is the source.)
|
||||||
pub fn has(lang: Lang, s: &str) -> bool {
|
pub fn has(lang: Lang, s: &str) -> bool {
|
||||||
lang.catalog().plain(s).is_some()
|
lang.i18n().get(lang.lang, "", s).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Translate an English UI string into the current language ([`tr`] with [`current`]).
|
/// Translate an English UI string into the current language ([`tr`] with [`current`]).
|
||||||
@@ -633,12 +534,13 @@ pub fn t(s: &str) -> &str {
|
|||||||
/// The name of a keyboard key, distinct from a command such as Delete. Missing key-name
|
/// The name of a keyboard key, distinct from a command such as Delete. Missing key-name
|
||||||
/// translations use the English key name, never a plain command translation.
|
/// translations use the English key name, never a plain command translation.
|
||||||
pub fn key_name(s: &str) -> &str {
|
pub fn key_name(s: &str) -> &str {
|
||||||
current().catalog().contextual("key", s).unwrap_or(s)
|
let lang = current();
|
||||||
|
lang.i18n().get(lang.lang, "key", s).unwrap_or(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Translate an English UI string; unknown strings come back unchanged.
|
/// Translate an English UI string; unknown strings come back unchanged.
|
||||||
pub fn tr(lang: Lang, s: &str) -> &str {
|
pub fn tr(lang: Lang, s: &str) -> &str {
|
||||||
lang.catalog().plain(s).unwrap_or(s)
|
lang.i18n().t(lang.lang, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How the layers below say their own sentences in the current language.
|
/// How the layers below say their own sentences in the current language.
|
||||||
@@ -654,16 +556,41 @@ pub fn words(wording: &str) -> String {
|
|||||||
|
|
||||||
/// Like [`tr`], for an English string that needs a disambiguating `context`.
|
/// Like [`tr`], for an English string that needs a disambiguating `context`.
|
||||||
pub fn tr_ctx<'a>(lang: Lang, context: &str, s: &'a str) -> &'a str {
|
pub fn tr_ctx<'a>(lang: Lang, context: &str, s: &'a str) -> &'a str {
|
||||||
lang.catalog().contextual(context, s).unwrap_or_else(|| tr(lang, s))
|
lang.i18n().tc(lang.lang, context, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A string keyed by its command id, falling back to the translation of the English `label`.
|
/// A string keyed by its command id, falling back to the translation of the English `label`. Only
|
||||||
|
/// PdfCraft's own `@id` rows count ([`own_id`]).
|
||||||
pub fn tr_id<'a>(lang: Lang, id: &str, label: &'a str) -> &'a str {
|
pub fn tr_id<'a>(lang: Lang, id: &str, label: &'a str) -> &'a str {
|
||||||
lang.catalog().id(id).unwrap_or_else(|| tr(lang, label))
|
if own_id(lang, id, label) { lang.i18n().tid(lang.lang, id, label) } else { tr(lang, label) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `lang` has PdfCraft's own `@id` row for `id` ([`Lang::has_own`]: command ids are each
|
||||||
|
/// app's own) and it keeps the `{placeholders}` and trailing `…` of the English `label`. craft-i18n
|
||||||
|
/// can't check that, because an `@id` row's source is the id, so a row that doesn't is skipped
|
||||||
|
/// here and the label's translation shows instead.
|
||||||
|
fn own_id(lang: Lang, id: &str, label: &str) -> bool {
|
||||||
|
lang.has_own("@id", id) && lang.i18n().get(lang.lang, "@id", id).is_some_and(|translated| same_shape(translated, label))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `translated` has the `{placeholders}` (in any order) and the trailing `…` of `english`,
|
||||||
|
/// the check craft-i18n makes on every row whose source is English text, and no `{` left open or
|
||||||
|
/// `}` without its `{`.
|
||||||
|
fn same_shape(translated: &str, english: &str) -> bool {
|
||||||
|
let placeholders = |text: &str| -> Option<Vec<String>> {
|
||||||
|
let mut names = text.split('{').skip(1).map(|p| p.split_once('}').map(|(name, _)| name.to_owned())).collect::<Option<Vec<_>>>()?;
|
||||||
|
if text.matches('}').count() != names.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
names.sort_unstable();
|
||||||
|
Some(names)
|
||||||
|
};
|
||||||
|
translated.ends_with('…') == english.ends_with('…') && placeholders(translated).is_some_and(|names| Some(names) == placeholders(english))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill `{name}` placeholders in one pass. Unknown placeholders are left as written, and inserted
|
/// Fill `{name}` placeholders in one pass. Unknown placeholders are left as written, and inserted
|
||||||
/// values are never read as templates again, so a file name containing `{n}` stays intact.
|
/// values are never read as templates again, so a file name containing `{n}` stays intact. Unlike
|
||||||
|
/// the crate's `fmt` it adds no bidi isolates: the egui fork orders each run for display.
|
||||||
pub fn fmt(template: &str, args: &[(&str, &str)]) -> String {
|
pub fn fmt(template: &str, args: &[(&str, &str)]) -> String {
|
||||||
let mut out = String::with_capacity(template.len());
|
let mut out = String::with_capacity(template.len());
|
||||||
let mut rest = template;
|
let mut rest = template;
|
||||||
@@ -728,12 +655,14 @@ pub fn edit_label(label: &pdfcraft_engine::Label) -> String {
|
|||||||
|
|
||||||
/// A command label such as "Undo Insert pages from a.pdf" in the current language. A catalog
|
/// A command label such as "Undo Insert pages from a.pdf" in the current language. A catalog
|
||||||
/// whose word order doesn't put the verb first translates "Undo {action}" and "Redo {action}"
|
/// whose word order doesn't put the verb first translates "Undo {action}" and "Redo {action}"
|
||||||
/// as whole phrases; otherwise the translated verb goes before the action.
|
/// as whole phrases, both of them; otherwise the translated verb goes before the action. (The
|
||||||
|
/// shared catalog has "Undo {action}" without "Redo {action}" in some languages, where one phrase
|
||||||
|
/// alone would word Undo and Redo differently.)
|
||||||
pub fn command_label(text: &str) -> String {
|
pub fn command_label(text: &str) -> String {
|
||||||
for (prefix, template) in [("Undo", "Undo {action}"), ("Redo", "Redo {action}")] {
|
for (prefix, template) in [("Undo", "Undo {action}"), ("Redo", "Redo {action}")] {
|
||||||
if let Some(action) = text.strip_prefix(prefix).and_then(|tail| tail.strip_prefix(' ')) {
|
if let Some(action) = text.strip_prefix(prefix).and_then(|tail| tail.strip_prefix(' ')) {
|
||||||
let action = action_label(action);
|
let action = action_label(action);
|
||||||
if has(current(), template) {
|
if has(current(), "Undo {action}") && has(current(), "Redo {action}") {
|
||||||
return fmt(t(template), &[("action", &action)]);
|
return fmt(t(template), &[("action", &action)]);
|
||||||
}
|
}
|
||||||
return format!("{} {action}", t(prefix));
|
return format!("{} {action}", t(prefix));
|
||||||
@@ -742,20 +671,22 @@ pub fn command_label(text: &str) -> String {
|
|||||||
t(text).to_owned()
|
t(text).to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A registered command's menu label: its `@id` catalog entry if there is one, otherwise
|
/// A registered command's menu label: its `@id` entry in PdfCraft's own catalog if there is one
|
||||||
/// [`command_label`] of the current English label.
|
/// that keeps the label's shape ([`own_id`]), otherwise [`command_label`] of the current English
|
||||||
|
/// label.
|
||||||
pub fn menu_label(id: &str, label: &str) -> String {
|
pub fn menu_label(id: &str, label: &str) -> String {
|
||||||
match current().catalog().id(id) {
|
let lang = current();
|
||||||
|
match own_id(lang, id, label).then(|| lang.i18n().get(lang.lang, "@id", id)).flatten() {
|
||||||
Some(translated) => translated.to_owned(),
|
Some(translated) => translated.to_owned(),
|
||||||
None => command_label(label),
|
None => command_label(label),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A plural-aware message: `one`/`other` are the English forms (with `{n}` where the count goes).
|
/// A plural-aware message: `one`/`other` are the English forms (with `{n}` where the count goes).
|
||||||
|
/// The form is the language's CLDR category; `{n}` is written in `0`–`9` without grouping or
|
||||||
|
/// isolate marks, as PdfCraft always has.
|
||||||
pub fn trn(lang: Lang, n: u64, one: &str, other: &str) -> String {
|
pub fn trn(lang: Lang, n: u64, one: &str, other: &str) -> String {
|
||||||
let idx = (lang.0.plural)(n);
|
lang.i18n().trn_plain(lang.lang, n, one, other)
|
||||||
let text = lang.catalog().plural(one, other, idx).unwrap_or(if n == 1 { one } else { other });
|
|
||||||
fmt(text, &[("n", &n.to_string())])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `name` lowercased with the Latin accents that occur in language names folded away, so a
|
/// `name` lowercased with the Latin accents that occur in language names folded away, so a
|
||||||
@@ -779,6 +710,27 @@ fn name_sort_key(name: &str) -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A test language whose PdfCraft catalog is `source` (catalog format), so tests of lookup code
|
||||||
|
/// don't depend on the embedded catalogs' wording. `code` picks the CLDR plural rules.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn fixture_lang(code: &'static str, source: &'static str) -> Lang {
|
||||||
|
let embedded: &'static craft_i18n::Embedded = Box::leak(Box::new([(code, "", source)]));
|
||||||
|
let (i18n, _) = I18n::new(embedded, &[code]);
|
||||||
|
let i18n: &'static I18n = Box::leak(Box::new(i18n));
|
||||||
|
let lang = i18n.from_code(code).unwrap_or(craft_i18n::Lang::EN);
|
||||||
|
Lang { lang, catalogs: Some(i18n) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For a test that needs the embedded catalogs: `false`, with a note, when none were embedded
|
||||||
|
/// (`CRAFT_I18N_DIR` unset), so the test can skip; the interface is then English only.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn needs_catalogs() -> bool {
|
||||||
|
if !catalogs_embedded() {
|
||||||
|
eprintln!("skipped: built without craft-i18n (set CRAFT_I18N_DIR to a craft-i18n checkout); the interface is English only");
|
||||||
|
}
|
||||||
|
catalogs_embedded()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,175 +0,0 @@
|
|||||||
//! Parser and lookup tables for translation catalogs (`*.tsv`, format documented in `ja.tsv`).
|
|
||||||
//!
|
|
||||||
//! Validation is strict (unknown escapes, unknown `@` contexts, placeholder, ellipsis and plural
|
|
||||||
//! form mismatches, duplicates), but a bad line is only skipped and reported: the rest of the
|
|
||||||
//! catalog still loads and the skipped string shows in English. The tests insist the bundled
|
|
||||||
//! catalogs have no errors at all.
|
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
/// A catalog entry as read from the file.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct Entry {
|
|
||||||
pub context: String,
|
|
||||||
pub source: String,
|
|
||||||
pub translation: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A parsed catalog. Values are owned for the life of the process (catalogs are built once).
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Catalog {
|
|
||||||
/// context-free strings: English source → translation (the hot path, looked up every frame)
|
|
||||||
plain: HashMap<String, String>,
|
|
||||||
/// strings with a disambiguating context: context → source → translation
|
|
||||||
contextual: HashMap<String, HashMap<String, String>>,
|
|
||||||
/// command-id keyed strings
|
|
||||||
ids: HashMap<String, String>,
|
|
||||||
/// plural messages: `one|other` → forms
|
|
||||||
plurals: HashMap<String, Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unescape(text: &str) -> Result<String, String> {
|
|
||||||
let mut out = String::with_capacity(text.len());
|
|
||||||
let mut chars = text.chars();
|
|
||||||
while let Some(c) = chars.next() {
|
|
||||||
if c != '\\' {
|
|
||||||
out.push(c);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match chars.next() {
|
|
||||||
Some('n') => out.push('\n'),
|
|
||||||
Some('t') => out.push('\t'),
|
|
||||||
Some('\\') => out.push('\\'),
|
|
||||||
Some(other) => return Err(format!("unknown escape \\{other}; use \\n, \\t or \\\\")),
|
|
||||||
None => return Err("trailing backslash; use \\\\ for a literal backslash".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `{name}` placeholders of a template, sorted (translators may reorder them).
|
|
||||||
pub fn placeholders(text: &str) -> Vec<&str> {
|
|
||||||
let mut names = Vec::new();
|
|
||||||
let mut rest = text;
|
|
||||||
while let Some((_, after_open)) = rest.split_once('{') {
|
|
||||||
let Some((name, after_close)) = after_open.split_once('}') else { break };
|
|
||||||
names.push(name);
|
|
||||||
rest = after_close;
|
|
||||||
}
|
|
||||||
names.sort_unstable();
|
|
||||||
names
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check one entry against its English source. `forms` is the language's number of plural forms.
|
|
||||||
fn validate(entry: &Entry, forms: usize) -> Result<(), String> {
|
|
||||||
let Entry { context, source, translation } = entry;
|
|
||||||
match context.as_str() {
|
|
||||||
// The source is a command id, not a template; the bundled-catalog tests check the id
|
|
||||||
// exists and compare the translation with that command's English label.
|
|
||||||
"@id" => Ok(()),
|
|
||||||
"@plural" => {
|
|
||||||
let english: Vec<&str> = source.split('|').collect();
|
|
||||||
let [one, other] = english.as_slice() else { return Err("plural source must be one|other".into()) };
|
|
||||||
if one.is_empty() || other.is_empty() || placeholders(one) != placeholders(other) {
|
|
||||||
return Err("English plural forms must be nonempty and have matching placeholders".into());
|
|
||||||
}
|
|
||||||
let translated: Vec<&str> = translation.split('|').collect();
|
|
||||||
if translated.len() != forms || translated.iter().any(|form| form.is_empty()) {
|
|
||||||
return Err(format!("expected {forms} nonempty plural forms"));
|
|
||||||
}
|
|
||||||
if translated.iter().any(|form| placeholders(form) != placeholders(other)) {
|
|
||||||
return Err("plural placeholders differ from the English source".into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ if context.starts_with('@') => Err(format!("unknown reserved context {context:?}")),
|
|
||||||
_ => {
|
|
||||||
if placeholders(source) != placeholders(translation) {
|
|
||||||
return Err("placeholders differ from the English source".into());
|
|
||||||
}
|
|
||||||
if source.ends_with('…') != translation.ends_with('…') {
|
|
||||||
return Err("trailing ellipsis differs from the English source".into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_line(line: &str, forms: usize) -> Result<Entry, String> {
|
|
||||||
let mut columns = line.split('\t');
|
|
||||||
let (Some(context), Some(source), Some(translation), None) = (columns.next(), columns.next(), columns.next(), columns.next()) else {
|
|
||||||
return Err("expected `context<TAB>source<TAB>translation`".into());
|
|
||||||
};
|
|
||||||
let entry = Entry { context: unescape(context)?, source: unescape(source)?, translation: unescape(translation)? };
|
|
||||||
if entry.source.is_empty() || entry.translation.is_empty() {
|
|
||||||
return Err("source and translation must be nonempty".into());
|
|
||||||
}
|
|
||||||
validate(&entry, forms)?;
|
|
||||||
Ok(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the entries of a catalog file for a language with `forms` plural forms. Malformed lines
|
|
||||||
/// and duplicates (the first entry wins) are returned as errors and skipped, so a bad translation
|
|
||||||
/// never breaks the UI.
|
|
||||||
pub fn parse_entries(text: &str, forms: usize) -> (Vec<Entry>, Vec<String>) {
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let mut seen = HashSet::new();
|
|
||||||
for (index, line) in text.lines().enumerate() {
|
|
||||||
if line.trim().is_empty() || line.starts_with('#') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let line_no = index.saturating_add(1);
|
|
||||||
match parse_line(line, forms) {
|
|
||||||
Ok(entry) if !seen.insert((entry.context.clone(), entry.source.clone())) => {
|
|
||||||
errors.push(format!("line {line_no}: duplicate key {:?} / {:?}", entry.context, entry.source));
|
|
||||||
}
|
|
||||||
Ok(entry) => entries.push(entry),
|
|
||||||
Err(error) => errors.push(format!("line {line_no}: {error}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(entries, errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Catalog {
|
|
||||||
/// Build a catalog from its valid entries; the errors of the skipped lines come back too.
|
|
||||||
pub fn parse(text: &str, forms: usize) -> (Catalog, Vec<String>) {
|
|
||||||
let (entries, errors) = parse_entries(text, forms);
|
|
||||||
let mut c = Catalog::default();
|
|
||||||
for Entry { context, source, translation } in entries {
|
|
||||||
match context.as_str() {
|
|
||||||
"" => {
|
|
||||||
c.plain.insert(source, translation);
|
|
||||||
}
|
|
||||||
"@id" => {
|
|
||||||
c.ids.insert(source, translation);
|
|
||||||
}
|
|
||||||
"@plural" => {
|
|
||||||
c.plurals.insert(source, translation.split('|').map(str::to_string).collect());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
c.contextual.entry(context).or_default().insert(source, translation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(c, errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plain(&self, s: &str) -> Option<&str> {
|
|
||||||
self.plain.get(s).map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contextual(&self, ctx: &str, s: &str) -> Option<&str> {
|
|
||||||
self.contextual.get(ctx)?.get(s).map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self, id: &str) -> Option<&str> {
|
|
||||||
self.ids.get(id).map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The plural form `index` of the message whose English forms are `one|other`.
|
|
||||||
pub fn plural(&self, one: &str, other: &str, index: usize) -> Option<&str> {
|
|
||||||
let forms = self.plurals.get(&format!("{one}|{other}"))?;
|
|
||||||
forms.get(index.min(forms.len().saturating_sub(1))).map(String::as_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+519
-188
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -185,6 +185,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn right_to_left_labels_match_a_query_typed_in_logical_order() {
|
fn right_to_left_labels_match_a_query_typed_in_logical_order() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let arabic = crate::i18n::Lang::from_code("ar").expect("Arabic is registered");
|
let arabic = crate::i18n::Lang::from_code("ar").expect("Arabic is registered");
|
||||||
let translated = crate::i18n::tr(arabic, "Organize pages");
|
let translated = crate::i18n::tr(arabic, "Organize pages");
|
||||||
assert_eq!(translated, "تنظيم الصفحات", "the catalogue keeps logical text");
|
assert_eq!(translated, "تنظيم الصفحات", "the catalogue keeps logical text");
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ use super::*;
|
|||||||
/// back. This is the `Style::right_to_left` half of the explicit RTL policy.
|
/// back. This is the `Style::right_to_left` half of the explicit RTL policy.
|
||||||
#[test]
|
#[test]
|
||||||
fn arabic_chrome_is_right_to_left_after_the_language_changes() {
|
fn arabic_chrome_is_right_to_left_after_the_language_changes() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let previous = crate::i18n::current();
|
let previous = crate::i18n::current();
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
crate::i18n::set_current(crate::i18n::Lang::from_code("ar").expect("registered language"));
|
crate::i18n::set_current(crate::i18n::Lang::from_code("ar").expect("registered language"));
|
||||||
|
|||||||
@@ -815,6 +815,9 @@ mod tests {
|
|||||||
/// left): never a mix, and never a label signing would refuse.
|
/// left): never a mix, and never a label signing would refuse.
|
||||||
#[test]
|
#[test]
|
||||||
fn appearance_labels_are_translated_or_all_english() {
|
fn appearance_labels_are_translated_or_all_english() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
use crate::i18n::{Lang, has, tr};
|
use crate::i18n::{Lang, has, tr};
|
||||||
let english = AppearanceLabels::default();
|
let english = AppearanceLabels::default();
|
||||||
assert_eq!(appearance_labels(Lang::EN), (english.clone(), false));
|
assert_eq!(appearance_labels(Lang::EN), (english.clone(), false));
|
||||||
@@ -845,6 +848,9 @@ mod tests {
|
|||||||
/// its label, in the words given, and only the values when the labels are off.
|
/// its label, in the words given, and only the values when the labels are off.
|
||||||
#[test]
|
#[test]
|
||||||
fn preview_lines_use_the_labels_signing_stamps() {
|
fn preview_lines_use_the_labels_signing_stamps() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (de, _) = appearance_labels(crate::i18n::Lang::from_code("de").unwrap());
|
let (de, _) = appearance_labels(crate::i18n::Lang::from_code("de").unwrap());
|
||||||
let all = Appearance { reason: true, location: true, ..Appearance::default() };
|
let all = Appearance { reason: true, location: true, ..Appearance::default() };
|
||||||
let lines = detail_lines(&all, &de, "Ada", " Genehmigt ", "Berlin", "2026.10.14 09:30:00 Z");
|
let lines = detail_lines(&all, &de, "Ada", " Genehmigt ", "Berlin", "2026.10.14 09:30:00 Z");
|
||||||
|
|||||||
@@ -698,6 +698,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn search_placeholder_never_overlaps_the_shortcut() {
|
fn search_placeholder_never_overlaps_the_shortcut() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let previous = crate::i18n::current();
|
let previous = crate::i18n::current();
|
||||||
for (code, expected) in [("en", "Ctrl+K"), ("de", "Strg+K")] {
|
for (code, expected) in [("en", "Ctrl+K"), ("de", "Strg+K")] {
|
||||||
crate::i18n::set_current(crate::i18n::Lang::from_code(code).unwrap());
|
crate::i18n::set_current(crate::i18n::Lang::from_code(code).unwrap());
|
||||||
|
|||||||
@@ -327,6 +327,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn date_language_names_fall_back_when_the_native_name_cannot_be_drawn() {
|
fn date_language_names_fall_back_when_the_native_name_cannot_be_drawn() {
|
||||||
|
if !crate::i18n::needs_catalogs() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let date_language = |code: &str| pdfcraft_engine::dates::DATE_LANGUAGES.iter().find(|l| l.code == code).expect("date language");
|
let date_language = |code: &str| pdfcraft_engine::dates::DATE_LANGUAGES.iter().find(|l| l.code == code).expect("date language");
|
||||||
let hindi = date_language("hi");
|
let hindi = date_language("hi");
|
||||||
assert_eq!(date_language_picker_name(hindi, |_| true), hindi.name, "the native name is kept when it can be drawn");
|
assert_eq!(date_language_picker_name(hindi, |_| true), hindi.name, "the native name is kept when it can be drawn");
|
||||||
|
|||||||
@@ -415,6 +415,9 @@ fn the_shortcuts_dialog_lists_the_real_bindings() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings() {
|
fn localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings() {
|
||||||
|
if crate::support::skip_without_catalogs("localized_shortcuts_update_immediately_and_keep_command_ids_and_key_bindings") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut h = harness();
|
let mut h = harness();
|
||||||
h.ctx.set_os(OperatingSystem::Windows);
|
h.ctx.set_os(OperatingSystem::Windows);
|
||||||
h.state_mut().set_option("language", "de").unwrap();
|
h.state_mut().set_option("language", "de").unwrap();
|
||||||
|
|||||||
@@ -1090,6 +1090,9 @@ fn panel_width(h: &Harness<'static, PdfCraftApp>) -> f32 {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_panel_keeps_its_width_with_a_reply_box_open() {
|
fn the_panel_keeps_its_width_with_a_reply_box_open() {
|
||||||
|
if crate::support::skip_without_catalogs("the_panel_keeps_its_width_with_a_reply_box_open") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// The reply row reserved 48 px for "Post"; Russian's wider button overflowed it, and a
|
// The reply row reserved 48 px for "Post"; Russian's wider button overflowed it, and a
|
||||||
// resizable panel keeps the width its content used, so every repaint widened the panel.
|
// resizable panel keeps the width its content used, so every repaint widened the panel.
|
||||||
let mut h = harness(|app| app.set_option("panel", "comments").unwrap());
|
let mut h = harness(|app| app.set_option("panel", "comments").unwrap());
|
||||||
@@ -3144,6 +3147,9 @@ fn hotbar_picker_adds_and_runs_freehand_highlight() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hotbar_picker_searches_translated_tool_labels() {
|
fn hotbar_picker_searches_translated_tool_labels() {
|
||||||
|
if crate::support::skip_without_catalogs("hotbar_picker_searches_translated_tool_labels") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut h = harness(|app| {
|
let mut h = harness(|app| {
|
||||||
app.set_option("language", "fr").unwrap();
|
app.set_option("language", "fr").unwrap();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -478,6 +478,9 @@ fn theme_commands_and_options_report_preference_and_effective_colours() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lithuanian_password_dialog_translates_dynamic_labels_and_validation() {
|
fn lithuanian_password_dialog_translates_dynamic_labels_and_validation() {
|
||||||
|
if crate::support::skip_without_catalogs("lithuanian_password_dialog_translates_dynamic_labels_and_validation") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (mut h, c) = harness();
|
let (mut h, c) = harness();
|
||||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "lt" }));
|
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "lt" }));
|
||||||
ok(&mut h, &c, "ui.set", json!({ "key": "dialog", "value": "protect" }));
|
ok(&mut h, &c, "ui.set", json!({ "key": "dialog", "value": "protect" }));
|
||||||
@@ -496,6 +499,9 @@ fn lithuanian_password_dialog_translates_dynamic_labels_and_validation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lithuanian_inspector_title_updates_through_the_control_channel() {
|
fn lithuanian_inspector_title_updates_through_the_control_channel() {
|
||||||
|
if crate::support::skip_without_catalogs("lithuanian_inspector_title_updates_through_the_control_channel") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (mut h, c) = harness();
|
let (mut h, c) = harness();
|
||||||
ok(&mut h, &c, "ui.set", json!({ "key": "panel", "value": "comments" }));
|
ok(&mut h, &c, "ui.set", json!({ "key": "panel", "value": "comments" }));
|
||||||
h.run_steps(2);
|
h.run_steps(2);
|
||||||
@@ -515,6 +521,9 @@ fn lithuanian_inspector_title_updates_through_the_control_channel() {
|
|||||||
/// command ids agents drive stay exactly the same.
|
/// command ids agents drive stay exactly the same.
|
||||||
#[test]
|
#[test]
|
||||||
fn language_switch_preserves_document_and_command_ids() {
|
fn language_switch_preserves_document_and_command_ids() {
|
||||||
|
if crate::support::skip_without_catalogs("language_switch_preserves_document_and_command_ids") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
use pdfcraft_ui_egui::i18n;
|
use pdfcraft_ui_egui::i18n;
|
||||||
let (mut h, c) = harness();
|
let (mut h, c) = harness();
|
||||||
let doc = h.state().views[0].id;
|
let doc = h.state().views[0].id;
|
||||||
@@ -953,6 +962,9 @@ fn state_and_view_options() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn japanese_controls_and_search_keep_command_ids() {
|
fn japanese_controls_and_search_keep_command_ids() {
|
||||||
|
if crate::support::skip_without_catalogs("japanese_controls_and_search_keep_command_ids") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if crate::support::skip_without_glyphs("japanese_controls_and_search_keep_command_ids", "ja", "") {
|
if crate::support::skip_without_glyphs("japanese_controls_and_search_keep_command_ids", "ja", "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -985,6 +997,9 @@ fn japanese_controls_and_search_keep_command_ids() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn german_preferences_and_search_keep_command_ids() {
|
fn german_preferences_and_search_keep_command_ids() {
|
||||||
|
if crate::support::skip_without_catalogs("german_preferences_and_search_keep_command_ids") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (mut h, c) = harness();
|
let (mut h, c) = harness();
|
||||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "de" }));
|
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "de" }));
|
||||||
ok(&mut h, &c, "ui.click", json!({ "label": "Konvertieren" }));
|
ok(&mut h, &c, "ui.click", json!({ "label": "Konvertieren" }));
|
||||||
@@ -1017,6 +1032,9 @@ fn german_preferences_and_search_keep_command_ids() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ukrainian_preferences_and_search_keep_command_ids() {
|
fn ukrainian_preferences_and_search_keep_command_ids() {
|
||||||
|
if crate::support::skip_without_catalogs("ukrainian_preferences_and_search_keep_command_ids") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (mut h, c) = harness();
|
let (mut h, c) = harness();
|
||||||
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "uk" }));
|
ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "uk" }));
|
||||||
ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" }));
|
ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" }));
|
||||||
@@ -1050,6 +1068,9 @@ fn ukrainian_preferences_and_search_keep_command_ids() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preferences_menu_and_shortcut_allow_switching_interface_languages() {
|
fn preferences_menu_and_shortcut_allow_switching_interface_languages() {
|
||||||
|
if crate::support::skip_without_catalogs("preferences_menu_and_shortcut_allow_switching_interface_languages") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if crate::support::skip_without_glyphs("preferences_menu_and_shortcut_allow_switching_interface_languages", "ja", "") {
|
if crate::support::skip_without_glyphs("preferences_menu_and_shortcut_allow_switching_interface_languages", "ja", "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1094,6 +1115,9 @@ fn preferences_menu_and_shortcut_allow_switching_interface_languages() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn japanese_dialogs_errors_and_custom_action_names() {
|
fn japanese_dialogs_errors_and_custom_action_names() {
|
||||||
|
if crate::support::skip_without_catalogs("japanese_dialogs_errors_and_custom_action_names") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if crate::support::skip_without_glyphs("japanese_dialogs_errors_and_custom_action_names", "ja", "") {
|
if crate::support::skip_without_glyphs("japanese_dialogs_errors_and_custom_action_names", "ja", "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1137,6 +1161,9 @@ fn japanese_dialogs_errors_and_custom_action_names() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simplified_chinese_about_tabs_and_credit_controls() {
|
fn simplified_chinese_about_tabs_and_credit_controls() {
|
||||||
|
if crate::support::skip_without_catalogs("simplified_chinese_about_tabs_and_credit_controls") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if crate::support::skip_without_glyphs("simplified_chinese_about_tabs_and_credit_controls", "zh-hans", "") {
|
if crate::support::skip_without_glyphs("simplified_chinese_about_tabs_and_credit_controls", "zh-hans", "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1169,6 +1196,9 @@ fn simplified_chinese_about_tabs_and_credit_controls() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simplified_chinese_signature_prompts_and_errors_keep_document_state() {
|
fn simplified_chinese_signature_prompts_and_errors_keep_document_state() {
|
||||||
|
if crate::support::skip_without_catalogs("simplified_chinese_signature_prompts_and_errors_keep_document_state") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if crate::support::skip_without_glyphs("simplified_chinese_signature_prompts_and_errors_keep_document_state", "zh-hans", "") {
|
if crate::support::skip_without_glyphs("simplified_chinese_signature_prompts_and_errors_keep_document_state", "zh-hans", "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ fn fill_and_sign_dates_follow_the_date_format_preference_and_it_is_remembered()
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn month_names_follow_the_date_language_or_else_the_interface_language() {
|
fn month_names_follow_the_date_language_or_else_the_interface_language() {
|
||||||
|
if crate::support::skip_without_catalogs("month_names_follow_the_date_language_or_else_the_interface_language") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||||
let mut app = PdfCraftApp::new();
|
let mut app = PdfCraftApp::new();
|
||||||
app.set_option("language", "en").unwrap();
|
app.set_option("language", "en").unwrap();
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ const TELUGU: &str = "తెలుగు ఫైల్";
|
|||||||
const HEBREW: &str = "ייפוי כח.pdf";
|
const HEBREW: &str = "ייפוי כח.pdf";
|
||||||
const DEVANAGARI: &str = "हिंदी पाठ सम्पादन";
|
const DEVANAGARI: &str = "हिंदी पाठ सम्पादन";
|
||||||
|
|
||||||
|
/// The embedded craft-i18n catalogs `code` reads (the shared one and PdfCraft's), one text; empty
|
||||||
|
/// in a build without them.
|
||||||
|
fn catalog(code: &str) -> String {
|
||||||
|
pdfcraft_ui_egui::i18n::Lang::from_code(code).map(|l| l.catalog_texts().collect::<Vec<_>>().join("\n")).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
fn families() -> Vec<FontId> {
|
fn families() -> Vec<FontId> {
|
||||||
vec![FontId::proportional(13.0), FontId::monospace(13.0), theme::medium(13.0), theme::semibold(17.0)]
|
vec![FontId::proportional(13.0), FontId::monospace(13.0), theme::medium(13.0), theme::semibold(17.0)]
|
||||||
}
|
}
|
||||||
@@ -329,11 +335,14 @@ fn installed_chinese_face_covers(cjk: theme::CjkPreference, catalog: &str, name:
|
|||||||
/// catalog's characters.
|
/// catalog's characters.
|
||||||
#[test]
|
#[test]
|
||||||
fn simplified_chinese_ui_uses_an_installed_hans_face_without_an_embedded_one() {
|
fn simplified_chinese_ui_uses_an_installed_hans_face_without_an_embedded_one() {
|
||||||
|
if crate::support::skip_without_catalogs("simplified_chinese_ui_uses_an_installed_hans_face_without_an_embedded_one") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if !pdfcraft_fonts::ui_chinese_fonts().is_empty() {
|
if !pdfcraft_fonts::ui_chinese_fonts().is_empty() {
|
||||||
eprintln!("skipping simplified_chinese_ui_uses_an_installed_hans_face_without_an_embedded_one: a Hans face is bundled");
|
eprintln!("skipping simplified_chinese_ui_uses_an_installed_hans_face_without_an_embedded_one: a Hans face is bundled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
installed_chinese_face_covers(theme::CjkPreference::SimplifiedChinese, include_str!("../../src/i18n/zh-hans.tsv"), "Simplified");
|
installed_chinese_face_covers(theme::CjkPreference::SimplifiedChinese, &catalog("zh-hans"), "Simplified");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Traditional Chinese interface keeps its installed Traditional face before the Japanese
|
/// A Traditional Chinese interface keeps its installed Traditional face before the Japanese
|
||||||
@@ -341,7 +350,10 @@ fn simplified_chinese_ui_uses_an_installed_hans_face_without_an_embedded_one() {
|
|||||||
/// Traditional-only characters, and Japanese-first fallback would otherwise win.
|
/// Traditional-only characters, and Japanese-first fallback would otherwise win.
|
||||||
#[test]
|
#[test]
|
||||||
fn traditional_chinese_ui_keeps_an_installed_hant_face_before_japanese() {
|
fn traditional_chinese_ui_keeps_an_installed_hant_face_before_japanese() {
|
||||||
installed_chinese_face_covers(theme::CjkPreference::TraditionalChinese, include_str!("../../src/i18n/zh-hant.tsv"), "Traditional");
|
if crate::support::skip_without_catalogs("traditional_chinese_ui_keeps_an_installed_hant_face_before_japanese") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installed_chinese_face_covers(theme::CjkPreference::TraditionalChinese, &catalog("zh-hant"), "Traditional");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Other languages keep the installed face last.
|
/// Other languages keep the installed face last.
|
||||||
@@ -373,6 +385,9 @@ fn ui_fonts_work_without_craft_fonts() {
|
|||||||
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
|
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
|
||||||
#[test]
|
#[test]
|
||||||
fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
||||||
|
if crate::support::skip_without_catalogs("primary_ui_fonts_cover_latin_and_cyrillic_catalogs") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
use skrifa::MetadataProvider as _;
|
use skrifa::MetadataProvider as _;
|
||||||
let defs = theme::font_definitions();
|
let defs = theme::font_definitions();
|
||||||
assert_eq!(&defs.families[&FontFamily::Monospace][..2], ["JetBrainsMono", "Inter"]);
|
assert_eq!(&defs.families[&FontFamily::Monospace][..2], ["JetBrainsMono", "Inter"]);
|
||||||
@@ -380,28 +395,28 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
|||||||
let fallback_font = skrifa::FontRef::from_index(fallback.font.as_ref().as_ref(), fallback.index).unwrap();
|
let fallback_font = skrifa::FontRef::from_index(fallback.font.as_ref().as_ref(), fallback.index).unwrap();
|
||||||
let fallback_charmap = fallback_font.charmap();
|
let fallback_charmap = fallback_font.charmap();
|
||||||
for (code, catalog) in [
|
for (code, catalog) in [
|
||||||
("cs", include_str!("../../src/i18n/cs.tsv")),
|
("cs", &catalog("cs")),
|
||||||
("el", include_str!("../../src/i18n/el.tsv")),
|
("el", &catalog("el")),
|
||||||
("pt-br", include_str!("../../src/i18n/pt-br.tsv")),
|
("pt-br", &catalog("pt-br")),
|
||||||
("de", include_str!("../../src/i18n/de.tsv")),
|
("de", &catalog("de")),
|
||||||
("es", include_str!("../../src/i18n/es.tsv")),
|
("es", &catalog("es")),
|
||||||
("fr", include_str!("../../src/i18n/fr.tsv")),
|
("fr", &catalog("fr")),
|
||||||
("ru", include_str!("../../src/i18n/ru.tsv")),
|
("ru", &catalog("ru")),
|
||||||
("kk", include_str!("../../src/i18n/kk.tsv")),
|
("kk", &catalog("kk")),
|
||||||
("bg", include_str!("../../src/i18n/bg.tsv")),
|
("bg", &catalog("bg")),
|
||||||
("hu", include_str!("../../src/i18n/hu.tsv")),
|
("hu", &catalog("hu")),
|
||||||
("uk", include_str!("../../src/i18n/uk.tsv")),
|
("uk", &catalog("uk")),
|
||||||
("it", include_str!("../../src/i18n/it.tsv")),
|
("it", &catalog("it")),
|
||||||
("tr", include_str!("../../src/i18n/tr.tsv")),
|
("tr", &catalog("tr")),
|
||||||
("sl", include_str!("../../src/i18n/sl.tsv")),
|
("sl", &catalog("sl")),
|
||||||
("lt", include_str!("../../src/i18n/lt.tsv")),
|
("lt", &catalog("lt")),
|
||||||
("id", include_str!("../../src/i18n/id.tsv")),
|
("id", &catalog("id")),
|
||||||
("vi", include_str!("../../src/i18n/vi.tsv")),
|
("vi", &catalog("vi")),
|
||||||
("fi", include_str!("../../src/i18n/fi.tsv")),
|
("fi", &catalog("fi")),
|
||||||
("sq", include_str!("../../src/i18n/sq.tsv")),
|
("sq", &catalog("sq")),
|
||||||
("ca", include_str!("../../src/i18n/ca.tsv")),
|
("ca", &catalog("ca")),
|
||||||
("ro", include_str!("../../src/i18n/ro.tsv")),
|
("ro", &catalog("ro")),
|
||||||
("nb", include_str!("../../src/i18n/nb.tsv")),
|
("nb", &catalog("nb")),
|
||||||
] {
|
] {
|
||||||
// Vietnamese stacks its diacritics: the precomposed letters and the base letters they
|
// Vietnamese stacks its diacritics: the precomposed letters and the base letters they
|
||||||
// combine with, including `đ` and the tone marks `ạ ả ấ ầ ẩ ẫ ậ ắ ằ ẳ ẵ ặ ế ề ể ễ ệ`.
|
// combine with, including `đ` and the tone marks `ạ ả ấ ầ ẩ ẫ ậ ắ ằ ẳ ẵ ặ ế ề ể ễ ệ`.
|
||||||
|
|||||||
@@ -563,6 +563,9 @@ fn the_calendar_picks_today_in_the_fields_format_from_any_month() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lithuanian_calendar_localizes_months_and_weekdays_without_changing_field_values() {
|
fn lithuanian_calendar_localizes_months_and_weekdays_without_changing_field_values() {
|
||||||
|
if crate::support::skip_without_catalogs("lithuanian_calendar_localizes_months_and_weekdays_without_changing_field_values") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
use egui_kittest::kittest::Queryable;
|
use egui_kittest::kittest::Queryable;
|
||||||
let mut h = harness();
|
let mut h = harness();
|
||||||
h.state_mut().set_option("language", "lt").unwrap();
|
h.state_mut().set_option("language", "lt").unwrap();
|
||||||
|
|||||||
@@ -536,6 +536,9 @@ fn stamped(path: &std::path::Path) -> String {
|
|||||||
/// so under the preview: signing neither fails nor writes `?` for them.
|
/// so under the preview: signing neither fails nor writes `?` for them.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_signature_labels_follow_the_interface_language() {
|
fn the_signature_labels_follow_the_interface_language() {
|
||||||
|
if crate::support::skip_without_catalogs("the_signature_labels_follow_the_interface_language") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
use pdfcraft_ui_egui::i18n::{Lang, tr};
|
use pdfcraft_ui_egui::i18n::{Lang, tr};
|
||||||
let p12 = concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12");
|
let p12 = concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12");
|
||||||
let cert = pdfcraft_engine::sign::pkcs12::open(&std::fs::read(p12).unwrap(), "test").unwrap().certificate;
|
let cert = pdfcraft_engine::sign::pkcs12::open(&std::fs::read(p12).unwrap(), "test").unwrap().certificate;
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ pub fn skip_without_glyphs(name: &str, language: &str, text: &str) -> bool {
|
|||||||
if std::env::var("CRAFT_FONTS_REQUIRED").as_deref() == Ok("1") {
|
if std::env::var("CRAFT_FONTS_REQUIRED").as_deref() == Ok("1") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let catalog = i18n::LANGUAGES.iter().find(|l| l.code == language).map_or("", |l| l.source);
|
let catalogs: Vec<&str> = i18n::Lang::from_code(language).into_iter().flat_map(i18n::Lang::catalog_texts).collect();
|
||||||
let translations = catalog.lines().filter(|l| !l.starts_with('#')).filter_map(|l| l.split('\t').nth(2));
|
let translations = catalogs.iter().flat_map(|c| c.lines()).filter(|l| !l.starts_with('#')).filter_map(|l| l.split('\t').nth(2));
|
||||||
let chars: BTreeSet<char> = std::iter::once(text).chain(translations).flat_map(str::chars).filter(|c| !c.is_control()).collect();
|
let chars: BTreeSet<char> = std::iter::once(text).chain(translations).flat_map(str::chars).filter(|c| !c.is_control()).collect();
|
||||||
let cjk = theme::CjkPreference::for_language(language);
|
let cjk = theme::CjkPreference::for_language(language);
|
||||||
let missing = [theme::CjkPreference::Default, cjk].into_iter().find_map(|cjk| {
|
let missing = [theme::CjkPreference::Default, cjk].into_iter().find_map(|cjk| {
|
||||||
@@ -103,3 +103,12 @@ pub fn skip_without_glyphs(name: &str, language: &str, text: &str) -> bool {
|
|||||||
eprintln!("skipping {name}: no interface font draws {missing:?} (set CRAFT_FONTS_DIR to a craft-fonts checkout with a face for it)");
|
eprintln!("skipping {name}: no interface font draws {missing:?} (set CRAFT_FONTS_DIR to a craft-fonts checkout with a face for it)");
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `name` is skipped because the build embeds no craft-i18n catalogs (English only).
|
||||||
|
pub fn skip_without_catalogs(name: &str) -> bool {
|
||||||
|
let skip = !pdfcraft_ui_egui::i18n::catalogs_embedded();
|
||||||
|
if skip {
|
||||||
|
eprintln!("skipping {name}: built without craft-i18n (set CRAFT_I18N_DIR to a craft-i18n checkout)");
|
||||||
|
}
|
||||||
|
skip
|
||||||
|
}
|
||||||
@@ -1459,6 +1459,9 @@ fn cancelling_then_discarding_an_earlier_dirty_tab_preserves_the_selected_docume
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn long_german_tool_labels_stay_within_the_sidebar() {
|
fn long_german_tool_labels_stay_within_the_sidebar() {
|
||||||
|
if crate::support::skip_without_catalogs("long_german_tool_labels_stay_within_the_sidebar") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
use pdfcraft_ui_egui::{LeftPanel, Mode};
|
use pdfcraft_ui_egui::{LeftPanel, Mode};
|
||||||
let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"));
|
let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"));
|
||||||
h.state_mut().set_option("language", "de").unwrap();
|
h.state_mut().set_option("language", "de").unwrap();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ add a language: [localization.md](localization.md). Summary in [ROADMAP.md](../R
|
|||||||
|
|
||||||
## How this was measured
|
## How this was measured
|
||||||
|
|
||||||
- **Strings translated** is measured: the catalogs in `crates/ui-egui/src/i18n/<code>.tsv`, counted
|
- **Strings translated** is measured: the catalogs in `crates/ui-egui/src/i18n/<code>.tsv` (since moved to craft-i18n's `locales/<code>/`), counted
|
||||||
as distinct `(context, English)` keys with a non-empty translation. The denominator is the union of
|
as distinct `(context, English)` keys with a non-empty translation. The denominator is the union of
|
||||||
keys across all catalogs, counting language-specific context rows such as `theme`/`Appearance`.
|
keys across all catalogs, counting language-specific context rows such as `theme`/`Appearance`.
|
||||||
**That union is a moving target**: every change that adds a string widens it, so a count is only
|
**That union is a moving target**: every change that adds a string widens it, so a count is only
|
||||||
|
|||||||
+12
-11
@@ -38,7 +38,7 @@ Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com
|
|||||||
|
|
||||||
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](https://github.com/storytold/pdfcraft/issues/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 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](https://github.com/storytold/pdfcraft/issues/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 (`sq.tsv`, 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.
|
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
|
## Arabic and right-to-left text
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ Wrapped paragraphs break between words in logical order and epaint reorders each
|
|||||||
|
|
||||||
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](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#ckb) (`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 (`ێ ڵ ڕ ۆ ە ڤ ژ چ گ پ`).
|
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](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#ckb) (`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 (`fa.tsv`, 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`](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#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.
|
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`](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#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.
|
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.
|
||||||
|
|
||||||
@@ -62,14 +62,15 @@ German labels keep nouns capitalized, including tool names inserted into notices
|
|||||||
|
|
||||||
## How translations work
|
## How translations work
|
||||||
|
|
||||||
PdfCraft uses the same system as PhotoCraft (`crates/ui-egui/src/i18n/`):
|
PdfCraft reads its translations from [craft-i18n](https://github.com/storytold/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.
|
- 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 a catalog, `crates/ui-egui/src/i18n/<code>.tsv`, with one entry per line: `context<TAB>English<TAB>translation`. An empty context marks a plain string. `@id` keys an entry by command id (for example `file.saveAs`), so one menu item can read differently from another with the same English label. `@plural` holds plural forms. Any other context disambiguates an English word with several meanings (`tl_ctx!` in UI code, for example the signature pad's "Type" button against the "Type" column). The header of `ja.tsv` documents the format and escapes.
|
- Each language has two catalogs in craft-i18n: `locales/<code>/common.tsv` (rows every app shares) and `locales/<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. `@id` keys an entry by command id (for example `file.saveAs`); PdfCraft reads `@id` rows only from `pdfcraft.tsv`. `@plural` holds plural forms, chosen by CLDR rules. Any other context disambiguates an English word with several meanings (`tl_ctx!` in UI code). craft-i18n's `docs/integration.md` documents the format and escapes.
|
||||||
|
- The build embeds the catalogs from `CRAFT_I18N_DIR`, a craft-i18n checkout at the commit in `craft-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::fmt` fills placeholders in one pass, so a value such as a file name containing `{n}` is inserted as written.
|
- Placeholders (`{name}`) must appear in both columns. A trailing `…` must be kept. `i18n::fmt` fills 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 of `cargo 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 to `tl!` or `i18n::t` in the UI source, engine wording (below) and key of the bundled catalogs, so a new English string shows up there until each catalog has it.
|
- A missing entry never fails a build or a test: that string shows in English. `cargo xtask i18n-report`, a step of `cargo 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 to `tl!` or `i18n::t` in the UI source, engine wording (below) and key of the embedded PdfCraft catalogs.
|
||||||
- Catalogs are validated strictly: unknown escapes, unknown `@` contexts, placeholder or ellipsis mismatches, wrong plural form counts and duplicates are errors. The tests require the bundled catalogs to have none; at run time a bad line is skipped (and logged) and that string shows in English.
|
- 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_consistent` prints them.
|
||||||
- Each `.tsv` has an `ATTRIBUTION.toml` entry with `kind = "translation"` (title, author, source, licence), checked by `cargo xtask assets`. Catalogs are project files under the project licence, so the entry has no `sha256` and editing a catalog doesn't touch `ATTRIBUTION.toml`; git history records their provenance.
|
- `ATTRIBUTION.toml` records the translations as one `[[build_input]]`; craft-i18n's `ATTRIBUTION.md` credits the translators.
|
||||||
- Translations are clean-room: write them from the meaning of the English text, never from another product's string tables.
|
- Translations are clean-room: write them from the meaning of the English text, never from another product's string tables.
|
||||||
|
|
||||||
## Sentences the engine words
|
## Sentences the engine words
|
||||||
@@ -86,10 +87,10 @@ Error details that come from the operating system, and faults rather than mistak
|
|||||||
|
|
||||||
## Adding a language
|
## Adding a language
|
||||||
|
|
||||||
1. Copy the header of `ja.tsv` into `xx.tsv` and translate entries.
|
1. In craft-i18n, add `locales/xx/pdfcraft.tsv` with its `@id language.name` row, and translate entries.
|
||||||
2. Add one row to `LANGUAGES` in `crates/ui-egui/src/i18n.rs`: code, native name, catalog and plural rule.
|
2. Add one row to `LANGUAGES` in `crates/ui-egui/src/i18n.rs` (code and whether it is right-to-left), one to `english_name`, and one to `DATE_LANGUAGES` in `crates/engine/src/dates.rs`.
|
||||||
3. Add an `ATTRIBUTION.toml` entry for the catalog (`kind = "translation"`, no `sha256`), then run `cargo xtask assets --write` (it regenerates `ATTRIBUTION.md` only) and `cargo xtask assets`.
|
3. Bump `craft-i18n.rev` (and the `commit:` of the craft-i18n source in both flatpak manifests) to a craft-i18n commit with the catalog.
|
||||||
|
|
||||||
The Preferences dropdown, the system-language match, the catalog tests (format, duplicates, placeholders, plural forms, command ids) and the translation gaps report then pick it up.
|
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.
|
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.
|
||||||
@@ -97,6 +97,13 @@ modules:
|
|||||||
- 'exec "/app/libexec/cups/${0##*/}" "$@"'
|
- 'exec "/app/libexec/cups/${0##*/}" "$@"'
|
||||||
- name: pdfcraft
|
- name: pdfcraft
|
||||||
buildsystem: simple
|
buildsystem: simple
|
||||||
|
# The release binaries were built with the interface translations at the commit in
|
||||||
|
# craft-i18n.rev (the craft-i18n source below); nothing here compiles, so this records which
|
||||||
|
# translations the bundle carries and keeps it in step with the from-source manifest (a test
|
||||||
|
# keeps both equal to the pin).
|
||||||
|
build-options:
|
||||||
|
env:
|
||||||
|
CRAFT_I18N_DIR: /run/build/pdfcraft/craft-i18n
|
||||||
build-commands:
|
build-commands:
|
||||||
- install -Dm755 bin/pdfcraft /app/bin/pdfcraft
|
- install -Dm755 bin/pdfcraft /app/bin/pdfcraft
|
||||||
- install -Dm755 bin/pdfcraft-cli /app/bin/pdfcraft-cli
|
- install -Dm755 bin/pdfcraft-cli /app/bin/pdfcraft-cli
|
||||||
@@ -111,3 +118,7 @@ modules:
|
|||||||
sources:
|
sources:
|
||||||
- type: dir
|
- type: dir
|
||||||
path: stage
|
path: stage
|
||||||
|
- type: git
|
||||||
|
url: https://github.com/storytold/craft-i18n
|
||||||
|
commit: dfbe955b1ba4aa84bb34c80c45b9d917e8da4704
|
||||||
|
dest: craft-i18n
|
||||||
@@ -99,6 +99,9 @@ modules:
|
|||||||
env:
|
env:
|
||||||
CARGO_HOME: /run/build/pdfcraft/cargo
|
CARGO_HOME: /run/build/pdfcraft/cargo
|
||||||
CARGO_NET_OFFLINE: 'true'
|
CARGO_NET_OFFLINE: 'true'
|
||||||
|
# The interface translations (the craft-i18n source below), embedded by build.rs.
|
||||||
|
CRAFT_I18N_DIR: /run/build/pdfcraft/craft-i18n
|
||||||
|
CRAFT_I18N_REQUIRED: '1'
|
||||||
build-commands:
|
build-commands:
|
||||||
- cargo --offline build --release --locked -p pdfcraft
|
- cargo --offline build --release --locked -p pdfcraft
|
||||||
- install -Dm755 target/release/pdfcraft /app/bin/pdfcraft
|
- install -Dm755 target/release/pdfcraft /app/bin/pdfcraft
|
||||||
@@ -120,4 +123,9 @@ modules:
|
|||||||
- corpus
|
- corpus
|
||||||
- build-flatpak
|
- build-flatpak
|
||||||
- .flatpak-builder
|
- .flatpak-builder
|
||||||
|
# The interface translations at the commit in craft-i18n.rev (a test keeps the two equal).
|
||||||
|
- type: git
|
||||||
|
url: https://github.com/storytold/craft-i18n
|
||||||
|
commit: dfbe955b1ba4aa84bb34c80c45b9d917e8da4704
|
||||||
|
dest: craft-i18n
|
||||||
- cargo-sources.json
|
- cargo-sources.json
|
||||||
+4
-54
@@ -3,8 +3,7 @@
|
|||||||
//! `ATTRIBUTION.toml` lists every asset: committed or vendored files (`[[asset]]`), files that
|
//! `ATTRIBUTION.toml` lists every asset: committed or vendored files (`[[asset]]`), files that
|
||||||
//! Cargo dependencies compile into PdfCraft (`[[bundled]]`), and files xtask downloads at build
|
//! Cargo dependencies compile into PdfCraft (`[[bundled]]`), and files xtask downloads at build
|
||||||
//! time (`[[fetched]]`). This gate fails when:
|
//! time (`[[fetched]]`). This gate fails when:
|
||||||
//! - an asset-like file in the repository has no entry, or its SHA-256 is missing or differs
|
//! - an asset-like file in the repository has no entry, or its SHA-256 is missing or differs;
|
||||||
//! (translation catalogs are project files and carry none);
|
|
||||||
//! - a licence is not on the allowlist, or a declared licence file is missing;
|
//! - a licence is not on the allowlist, or a declared licence file is missing;
|
||||||
//! - an icon, image or font names Adobe as author or source, or an `adobe_data` entry is not on
|
//! - an icon, image or font names Adobe as author or source, or an `adobe_data` entry is not on
|
||||||
//! the closed list approved in AGENTS.md §1.1;
|
//! the closed list approved in AGENTS.md §1.1;
|
||||||
@@ -50,11 +49,6 @@ pub const ADOBE_DATA_ALLOWED: &[(&str, &str)] = &[("hayro-cmap", "assets/cmaps.b
|
|||||||
/// table has to be re-attested deliberately.
|
/// table has to be re-attested deliberately.
|
||||||
pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14_metrics.rs", "vendor/hayro-cmap/assets/cmaps.brotli"];
|
pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14_metrics.rs", "vendor/hayro-cmap/assets/cmaps.brotli"];
|
||||||
|
|
||||||
/// Where the UI translation catalogs live. `kind = "translation"` entries for `.tsv` files here
|
|
||||||
/// are project files under the project licence: they carry no `sha256`, because git history
|
|
||||||
/// already records their provenance (AGENTS.md §1.3).
|
|
||||||
pub const TRANSLATION_DIR: &str = "crates/ui-egui/src/i18n/";
|
|
||||||
|
|
||||||
/// File extensions that count as assets wherever they appear in the repository.
|
/// File extensions that count as assets wherever they appear in the repository.
|
||||||
const ASSET_EXTENSIONS: &[&str] = &[
|
const ASSET_EXTENSIONS: &[&str] = &[
|
||||||
"png", "jpg", "jpeg", "gif", "svg", "ico", "icns", "webp", "bmp", "tif", "tiff", "avif", "heic", "ttf", "otf", "ttc", "woff", "woff2", "pfb",
|
"png", "jpg", "jpeg", "gif", "svg", "ico", "icns", "webp", "bmp", "tif", "tiff", "avif", "heic", "ttf", "otf", "ttc", "woff", "woff2", "pfb",
|
||||||
@@ -103,20 +97,13 @@ pub struct Asset {
|
|||||||
pub licence_file: String,
|
pub licence_file: String,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub usage: String,
|
pub usage: String,
|
||||||
/// Required for every entry except a translation catalog ([`Asset::is_translation_catalog`]).
|
/// Required for every entry.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sha256: Option<String>,
|
pub sha256: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub adobe_data: bool,
|
pub adobe_data: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Asset {
|
|
||||||
/// A UI translation catalog: a project file whose entry carries no hash.
|
|
||||||
pub fn is_translation_catalog(&self) -> bool {
|
|
||||||
self.kind == "translation" && self.path.starts_with(TRANSLATION_DIR) && self.path.ends_with(".tsv")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Bundled {
|
pub struct Bundled {
|
||||||
@@ -177,9 +164,6 @@ pub fn run(args: &[String]) -> Result<()> {
|
|||||||
std::fs::write(root.join("ATTRIBUTION.md"), &markdown)?;
|
std::fs::write(root.join("ATTRIBUTION.md"), &markdown)?;
|
||||||
println!("assets: wrote ATTRIBUTION.md");
|
println!("assets: wrote ATTRIBUTION.md");
|
||||||
}
|
}
|
||||||
for a in manifest.asset.iter().filter(|a| a.is_translation_catalog() && a.sha256.is_some()) {
|
|
||||||
println!("assets: note: {}: translation catalogs aren't hashed; its sha256 line can be deleted", a.path);
|
|
||||||
}
|
|
||||||
let mut problems = check(&root, &manifest, &repo_files(&root)?, &cargo_lock_versions(&root)?);
|
let mut problems = check(&root, &manifest, &repo_files(&root)?, &cargo_lock_versions(&root)?);
|
||||||
match std::fs::read_to_string(root.join("ATTRIBUTION.md")) {
|
match std::fs::read_to_string(root.join("ATTRIBUTION.md")) {
|
||||||
Ok(existing) if existing == markdown => {}
|
Ok(existing) if existing == markdown => {}
|
||||||
@@ -275,12 +259,9 @@ pub fn check(root: &Path, m: &Manifest, repo_files: &[String], lock: &BTreeSet<(
|
|||||||
if a.adobe_data && !(ADOBE_DATA_FILES.contains(&a.path.as_str()) && a.kind == "data") {
|
if a.adobe_data && !(ADOBE_DATA_FILES.contains(&a.path.as_str()) && a.kind == "data") {
|
||||||
problems.push(format!("{}: adobe_data is only allowed for the closed list in AGENTS.md §1.1", a.path));
|
problems.push(format!("{}: adobe_data is only allowed for the closed list in AGENTS.md §1.1", a.path));
|
||||||
}
|
}
|
||||||
let catalog = a.is_translation_catalog();
|
|
||||||
match (std::fs::read(root.join(&a.path)), &a.sha256) {
|
match (std::fs::read(root.join(&a.path)), &a.sha256) {
|
||||||
(Err(_), _) => problems.push(format!("{}: listed but missing", a.path)),
|
(Err(_), _) => problems.push(format!("{}: listed but missing", a.path)),
|
||||||
// A leftover hash line (e.g. kept while resolving a rebase) is not checked; `run` notes it.
|
(Ok(_), None) => problems.push(format!("{}: sha256 is missing", a.path)),
|
||||||
(Ok(_), _) if catalog => {}
|
|
||||||
(Ok(_), None) => problems.push(format!("{}: sha256 is missing (only translation catalogs go without one)", a.path)),
|
|
||||||
(Ok(bytes), Some(hash)) if sha256_hex(&bytes) == *hash => {}
|
(Ok(bytes), Some(hash)) if sha256_hex(&bytes) == *hash => {}
|
||||||
(Ok(_), Some(_)) => {
|
(Ok(_), Some(_)) => {
|
||||||
problems.push(format!("{}: SHA-256 does not match ATTRIBUTION.toml (re-verify provenance, then update the entry)", a.path))
|
problems.push(format!("{}: SHA-256 does not match ATTRIBUTION.toml (re-verify provenance, then update the entry)", a.path))
|
||||||
@@ -402,7 +383,7 @@ pub fn render_markdown(m: &Manifest) -> String {
|
|||||||
s.push_str("<!-- Generated from ATTRIBUTION.toml by `cargo xtask assets --write`. Do not edit by hand. -->\n\n");
|
s.push_str("<!-- Generated from ATTRIBUTION.toml by `cargo xtask assets --write`. Do not edit by hand. -->\n\n");
|
||||||
s.push_str("Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. ");
|
s.push_str("Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. ");
|
||||||
s.push_str(
|
s.push_str(
|
||||||
"The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset but the translation catalogs, is [ATTRIBUTION.toml](ATTRIBUTION.toml). ",
|
"The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset, is [ATTRIBUTION.toml](ATTRIBUTION.toml). ",
|
||||||
);
|
);
|
||||||
s.push_str("Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).\n\n");
|
s.push_str("Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).\n\n");
|
||||||
let esc = |t: &str| t.replace('|', "\\|");
|
let esc = |t: &str| t.replace('|', "\\|");
|
||||||
@@ -545,37 +526,6 @@ mod tests {
|
|||||||
assert!(p.iter().any(|p| p.contains("no ATTRIBUTION.toml entry")), "{p:?}");
|
assert!(p.iter().any(|p| p.contains("no ATTRIBUTION.toml entry")), "{p:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn translation_catalogs_require_attribution() {
|
|
||||||
assert!(is_asset_path("crates/ui-egui/src/i18n/ja.tsv"));
|
|
||||||
let p = check(&root(), &Manifest::default(), &["crates/ui-egui/src/i18n/ja.tsv".into()], &lock(&[]));
|
|
||||||
assert!(p.iter().any(|p| p.contains("no ATTRIBUTION.toml entry")), "{p:?}");
|
|
||||||
let m = Manifest { asset: vec![asset("crates/ui-egui/src/i18n/ja.tsv", "Adobe", "translation", "MIT")], ..Default::default() };
|
|
||||||
assert!(check(&root(), &m, &[], &lock(&[])).iter().any(|p| p.contains("from Adobe is forbidden")));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One `[[asset]]` entry as ATTRIBUTION.toml spells it, with `extra` lines appended.
|
|
||||||
fn parsed(path: &str, kind: &str, extra: &str) -> Manifest {
|
|
||||||
let text = format!(
|
|
||||||
"[[asset]]\npath = \"{path}\"\ntitle = \"t\"\nauthor = \"a\"\nsource = \"s\"\nlicence = \"MIT\"\nlicence_file = \"Cargo.toml\"\nkind = \"{kind}\"\nusage = \"u\"\n{extra}"
|
|
||||||
);
|
|
||||||
toml::from_str(&text).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Catalogs change in most UI PRs; a pinned hash would make every pair of them conflict.
|
|
||||||
#[test]
|
|
||||||
fn translation_catalogs_carry_no_hash_and_everything_else_does() {
|
|
||||||
let check_one = |m: Manifest| check(&root(), &m, &[], &lock(&[]));
|
|
||||||
let ja = "crates/ui-egui/src/i18n/ja.tsv";
|
|
||||||
assert_eq!(check_one(parsed(ja, "translation", "")), Vec::<String>::new());
|
|
||||||
let stale = format!("sha256 = \"{}\"\n", "0".repeat(64));
|
|
||||||
assert_eq!(check_one(parsed(ja, "translation", &stale)), Vec::<String>::new(), "a leftover hash is not checked");
|
|
||||||
for (path, kind) in [("assets/fonts/Inter-Regular.ttf", "font"), (ja, "data"), ("Cargo.toml", "translation")] {
|
|
||||||
let p = check_one(parsed(path, kind, ""));
|
|
||||||
assert!(p.iter().any(|p| p.contains("sha256 is missing")), "{path} {kind}: {p:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adobe_ofl_fonts_are_allowed_and_other_adobe_fonts_rejected() {
|
fn adobe_ofl_fonts_are_allowed_and_other_adobe_fonts_rejected() {
|
||||||
let l = lock(&[("noto", "1.0.0")]);
|
let l = lock(&[("noto", "1.0.0")]);
|
||||||
|
|||||||
Reference in new issue
Block a user