M14: complete the Czech (cs) interface catalog (#1066)

* M14: complete the Czech (cs) interface catalog

Czech covered the menus and a few later additions (167 keys). The catalog
now has every key the code uses (2,194): Home, All tools, panels, dialogs,
notices, diagnostics and history labels. Translated clean-room from the
meaning of the English labels; the 167 existing entries are unchanged.

- The @plural messages use the three Czech forms (one | few | other);
  other counted messages use count-neutral wording ("Vybráno stránek: {n}").
- "calendar date" months are genitive ("9. října 2026"); "Undo {action}"
  and "Redo {action}" are whole phrases ("Zpět: {action}").
- Labels the code joins with a leading or trailing space keep it.
- The key-name entries (context "key") are kept as they were.

Tests that relied on the incomplete catalog:
- czech_covers_every_menu_label keeps a short list of strings spelled the
  same in Czech (product and format names, Text, Index, Metadata, Model,
  templates such as "{0}: {1}"), compares leading and trailing spaces with
  the source instead of forbidding them, and requires a catalog without
  parse errors.
- date_format.rs expected the English fallback "Same as interface
  (Čeština)" under the Czech interface; that label is now translated.

ATTRIBUTION.toml: new SHA-256 of cs.tsv.

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

* M14: check the Czech catalog like the other complete catalogs

Czech now has the same coverage gates as the complete catalogs:
- czech_covers_commands_and_catalogue: every registered command, menu
  title and All tools group, section and item;
- czech_covers_ui_literals: every tl!("…") literal in the UI source;
- czech_plurals_dates_and_history_labels: 1/3/5 pages and 0 fields, the
  genitive date ("9. října 2026"), and Undo/Redo/Change history labels
  that keep captured names.
cs also joins the lists in missing_ocr_models_messages_are_translated and
bulk_field_properties_labels_are_in_every_full_catalog.

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

* i18n (cs): terminology review fixes

An independent review of the Czech catalog found 5 entries with a wrong
or misleading meaning and 44 that read unnaturally or inconsistently.

Meaning:
- US formats say so: "Čísla sociálního zabezpečení (USA)" (not the Czech
  birth number) and "Telefonní číslo (USA)" for the field format.
- Mouse Exit/Enter triggers: "Odjetí kurzoru" / "Najetí kurzoru".
- Rich text in form fields is not RTF; Form XObjects are not forms.

Wording and consistency:
- Home: pinning folders refers to "úvodní obrazovka" (also in the existing
  "Pin folder to Home…"); "úvodní stránka" reads as the first page.
- Callout is "Popisek se šipkou"; Self-signed is "Samopodepsaný" (fixes
  the agreement in "Samopodepsaná ID").
- Reset (forms), Apply redactions, guided actions, the radio button and
  checkbox styles, line endings (also the existing arrow entries), border
  styles, "Omezit na … znaků", "Rozlišovat velikost písmen", grammar in
  several messages, and the About window wording shared with the other
  Crafting Apps.

History labels for the Creator and Producer document information keys
use the Document Properties wording: "Aplikace" and "Producent PDF".

ATTRIBUTION.toml: new SHA-256 of cs.tsv.

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

* contributors: add my own people.toml entry

Own entry only, as AGENTS.md describes: credited by the name on my public
GitHub profile (sync_github_name), otherwise by @username.

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

* i18n (cs): translate "Offset from centre" (watermark offset, #1076)

New upstream string added after the catalog was last synced.

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

* i18n (cs): follow the reworded Combine files strings (#1032)

Upstream generalised the Combine files dialog (images and text files, a
prompt for files that can't be read) and replaced six strings. The old
Czech entries are dropped and the ten new ones translated.

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

* i18n (cs): translate the new Enhance page / camera image strings (#1027)

Four strings added upstream after the catalog was last synced.

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

* i18n (cs): margin labels Top/Bottom read Nahoře/Dole

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

* i18n: translate the Digital ID errors, the Signatures panel and key usages

The digital-ID dialog printed its validation errors, the Signatures panel
headings ("Rev. n: Signed by ...", "Unsigned signature field"), the
certificate issuer line of the signed-document bar and the nine key-usage
names in English. Translate them at the point where they are drawn, the way
title() and notify_fmt() already do, so engine and OS error texts that
share those paths pass through unchanged. Czech catalog entries added; other
languages fall back to English.

The signature appearance preview is left alone on purpose: it mirrors the
Helvetica text the engine bakes into the PDF ("Digitally signed by", "Reason:"),
which is document content, not interface.

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

* i18n: translate form action, print, panel layout, rendering and OCR language texts

Route the remaining plain-English messages through the translation lookup
where they are drawn: the Add-action validation hints, the Cut and stack
print error (preview and notice), the two panel-layout notices, the
"Rendering..." placeholders on unrendered pages, the page number under the
Page Boxes preview (reuses "Page {label}") and the OCR language name.

The messages of the docking validator and the engine and OS errors stay
English: they report internal invariants or text this code does not own.
"My stamps" stays too: it is stored as the category of the user's stamps, so
translating it would split a library when the language changes. The dynamic
stamp chip text mirrors the English text baked into the stamp and is left as is.

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

* i18n (cs): translate the Appearance preferences and the wheel scroll speed

The strings added upstream with the Appearance section and the mouse wheel
scroll speed setting were missing from the Czech catalog, so
czech_covers_ui_literals failed after the merge.

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

* i18n: use plural forms for counts built in code

The saved notice after Optimize ("3 images optimized", "2 items discarded"),
the pixel sizes in Export all images, the exact size in bytes (human_size and
the document properties) and the history label of a multi-field delete were
assembled from English words and a trailing "s". Use trn() for them, with
@plural entries in the Czech catalog (1 | 2-4 | 5+), and translate the
"Delete N fields" history label in action_label(). English output is unchanged;
other languages without a plural entry keep the English wording.

The Czech plural test now also checks bytes, optimized images and the
Delete N fields label.

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

* i18n: translate the text-box and organize toolbar tooltips

"Done adding text" and "Discard this text (Esc)" (the buttons under a new
text box) and the Rotate tooltips of the organize toolbar were passed to
icons::button as plain English. Translate them at the call site, as the
other icon buttons do.

The two text-box tooltips are tl!() literals, so every complete catalog
(ar, bg, cs, de, fr, hu, it, ja, pt-br, ru, uk, zh-hans) has to carry them;
the entries for the languages other than Czech are AI drafts that want a
native speaker's look. The Rotate tooltips already had translations.

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

---------

Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Daniel Lošák
2026-10-11 15:55:21 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Losminek Brandon Thomas
parent 0f7ff1ce30
commit de8e0176ed
28 changed files with 2310 additions and 61 deletions
+12 -12
View File
@@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b12eb7c9c8e540b2c1294966f4cb8c885d32763204f8fdb4d0963d275c282a10"
sha256 = "1863437c886142b2bccce3f44a538ae80d0ceea81074b84dfdfe8d9a4a4227ab"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "18f725656c3b560b3b84b908d017a8ccc1b01670cfeea4655d21fd3b41bc59a4"
sha256 = "5ae2e315a9b99b4c0dfaf7be019c4d430ace17b4fbbfea5a2779b3e972846a21"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -52,7 +52,7 @@ 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/mod.rs"
sha256 = "c3bc62f928891ee565f2b53a29e4efe6d7f4f83f44cd62b74227a17171b8d82c"
sha256 = "8b9e22fa25339e4aedb0542b8bf6f68dfd97bc3634105199e1951475e30eed53"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8956e77078a2dd55ee7fb9fe74d178753154bd4ea2ef98fecba8ed7553411c73"
sha256 = "fb9c91a98c6975bc24249e8ae750fd46b9245ac5d2d7381f1e21319b9adbcaac"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "4aa8784d99113ff58876e34469d6d2edadb6f226c748b153a11c97fac0eb5742"
sha256 = "f36aefae1a1ba6d576a4647fbfdd65b9b5086bfb266ea260e513244adc7af05a"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "5c6da9eb74460a7ba67fa53866760644f258054a05cef7df4aedd60cd51928c5"
sha256 = "c4d477cae250ff2cd16f5d1203b4fa4c7c6962efa158a4e501c4553d3709439a"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "25fd7dc03149033b02c4e655da84380dd0d90c41df556183c98285f056db4c82"
sha256 = "bada9b6f0b3775e4dab1213d9759070f7551d8a6142de10159268c88a04d3f0c"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "4045450381525f0acb0a3895879cd369f0c4de7a502790e8a4609c3333408931"
sha256 = "efb75d8575df9e1dd4610602f20545b3a14a3a0814060efc27eb7cab591ccccb"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1c747fb3a4cd1e66dd2868349d8df02385c1b825928bfca01b5059c7de9bdaba"
sha256 = "ca305b188abc8d9c261901dde10f7cd8518520983fdf88d2358374b352ad3d5a"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "40919a7582812d7231cd2c0cc712b57f066b8e1af6fc43281ec8ce6df4f60cca"
sha256 = "f004b06f4e7eb8f4a99e22e1abfd97c95795b2215080e4008574522e54eda74c"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f61852f58a28ae3d8a3a59219b91ef79842c7bc7b01ea451aca6110f54939755"
sha256 = "cd9ecaf661533cbd3b055cfd3994602f6739c83061d1e225ba43ee6f229068cf"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0c0a492b757d3a5c488b2ef4e48386bf7528083c85805bcfd0e7e06fe4e9092f"
sha256 = "a8a66390b2d80b4708c4f854318b923eef7f0f5932439fb5ef25f2651980f24e"
[[asset]]
path = "assets/icons/accessibility.svg"
+3
View File
@@ -100,3 +100,6 @@ display_name = "Mustafa Hadidi"
real_name = "Lucas"
display_name = "Lucas"
sync_github_name = true # use the name on your public GitHub profile as display name
[people.Losminek]
sync_github_name = true
+1 -1
View File
@@ -2360,7 +2360,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
let now = ui.input(|inp| inp.time);
let since = *view.waiting_since.entry(i).or_insert(now);
let msg = if now - since > 6.0 { "Still rendering — this page is unusually complex…" } else { "Rendering…" };
painter.text(r.center(), Align2::CENTER_CENTER, msg, theme::regular(12.0), t.text_faint);
painter.text(r.center(), Align2::CENTER_CENTER, tl!(msg), theme::regular(12.0), t.text_faint);
}
}
if tiled && r.intersects(visible) {
+2 -2
View File
@@ -364,8 +364,8 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad
.show(ui, |ui| {
ui.spacing_mut().item_spacing = egui::vec2(2.0, 2.0);
ui.horizontal(|ui| {
done = crate::icons::button(ui, "check", 24.0, false, "Done adding text").clicked();
cancel = crate::icons::button(ui, "x", 24.0, false, "Discard this text (Esc)").clicked();
done = crate::icons::button(ui, "check", 24.0, false, tl!("Done adding text")).clicked();
cancel = crate::icons::button(ui, "x", 24.0, false, tl!("Discard this text (Esc)")).clicked();
});
})
.response
+1 -1
View File
@@ -321,7 +321,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
PropsTab::Advanced => {
row(ui, "PDF version", i.pdf_version.clone());
row(ui, "Location", crate::bidi::visual(doc.path.as_deref().unwrap_or_default()).into_owned());
row(ui, "File size", format!("{} ({} bytes)", human_size(i.file_size), i.file_size));
row(ui, "File size", format!("{} ({})", human_size(i.file_size), crate::panels::byte_count(i.file_size)));
let p = &i.pages[0];
row(ui, "Page size", units.size(f64::from(p.width), f64::from(p.height)));
row(ui, "Number of pages", i.pages.len().to_string());
+2 -2
View File
@@ -348,7 +348,7 @@ pub fn show(app: &mut crate::PdfCraftApp, ui: &mut egui::Ui) {
let t = crate::theme::Tokens::get(ui.ctx());
let mut workspace = std::mem::take(&mut app.docking);
if let Err(error) = workspace.validate().and_then(|()| sync(&mut workspace, app)) {
app.notify(format!("Panel layout could not be restored: {error}"));
app.notify_fmt("Panel layout could not be restored: {error}", &[("error", &error)]);
workspace = Workspace::default();
let _ = sync(&mut workspace, app);
}
@@ -411,7 +411,7 @@ pub fn show(app: &mut crate::PdfCraftApp, ui: &mut egui::Ui) {
if let Err(error) =
serde_json::to_value(action).map_err(|error| error.to_string()).and_then(|action| command(app, false, &json!({"action": action})))
{
app.notify(format!("Panel move was not applied: {error}"));
app.notify_fmt("Panel move was not applied: {error}", &[("error", &error)]);
}
}
}
+3 -1
View File
@@ -90,7 +90,9 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
} else if kind == ExportKind::AllImages {
ui.horizontal(|ui| {
ui.label(tl!("Exclude images smaller than"));
let label = move |n: u32| if n == 0 { tl!("No limit").to_string() } else { format!("{n} pixels") };
let label = move |n: u32| {
if n == 0 { tl!("No limit").to_string() } else { crate::i18n::trn(crate::i18n::current(), u64::from(n), "{n} pixel", "{n} pixels") }
};
egui::ComboBox::from_id_salt("export-min").selected_text(label(d.min_side)).show_ui(ui, |ui| {
for n in [0, 16, 32, 64, 128, 256] {
ui.selectable_value(&mut d.min_side, n, label(n));
+2
View File
@@ -2261,6 +2261,8 @@ scroll speed Fast سريعة
Enhance camera image تحسين صورة الكاميرا
Drag the corners onto the document's edges, then choose Enhance page. اسحب الزوايا إلى حواف المستند، ثم اختر تحسين الصفحة.
Enhance page تحسين الصفحة
Done adding text إنهاء إضافة النص
Discard this text (Esc) تجاهل هذا النص (Esc)
Clean-up تنظيف
Original الأصل
Greyscale تدرج رمادي
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2203,6 +2203,8 @@ scroll speed Fast Висока
Enhance camera image Подобри снимка от камера
Drag the corners onto the document's edges, then choose Enhance page. Плъзнете ъглите до краищата на документа, след това изберете „Подобри страницата“.
Enhance page Подобри страницата
Done adding text Готово с добавянето на текст
Discard this text (Esc) Отхвърли този текст (Esc)
Clean-up Почистване
Original Оригинал
Greyscale Нива на сивото
Can't render this file because it contains an unexpected character in line 8 and column 16.
File diff suppressed because it is too large Load Diff
+2
View File
@@ -2241,6 +2241,8 @@ scroll speed Fast Schnell
Enhance camera image Kamerafoto verbessern
Drag the corners onto the document's edges, then choose Enhance page. Ziehen Sie die Ecken auf die Kanten des Dokuments und wählen Sie dann Seite verbessern.
Enhance page Seite verbessern
Done adding text Texteingabe abschließen
Discard this text (Esc) Diesen Text verwerfen (Esc)
Clean-up Bereinigung
Original Original
Greyscale Graustufen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+2
View File
@@ -2240,6 +2240,8 @@ scroll speed Fast Rapide
Enhance camera image Améliorer la photo d'un document
Drag the corners onto the document's edges, then choose Enhance page. Faites glisser les coins sur les bords du document, puis choisissez Améliorer la page.
Enhance page Améliorer la page
Done adding text Terminer l'ajout de texte
Discard this text (Esc) Abandonner ce texte (Échap)
Clean-up Nettoyage
Original Original
Greyscale Niveaux de gris
Can't render this file because it contains an unexpected character in line 6 and column 39.
+2
View File
@@ -2239,6 +2239,8 @@ scroll speed Fast Gyors
Enhance camera image Kamerakép javítása
Drag the corners onto the document's edges, then choose Enhance page. Húzza a sarkokat a dokumentum széleire, majd válassza az Oldal javítása lehetőséget.
Enhance page Oldal javítása
Done adding text Szöveg hozzáadásának befejezése
Discard this text (Esc) Szöveg elvetése (Esc)
Clean-up Tisztítás
Original Eredeti
Greyscale Szürkeárnyalatos
Can't render this file because it contains an unexpected character in line 7 and column 44.
+2
View File
@@ -2248,6 +2248,8 @@ scroll speed Fast Veloce
Enhance camera image Migliora foto della fotocamera
Drag the corners onto the document's edges, then choose Enhance page. Trascina gli angoli sui bordi del documento, poi scegli Migliora la pagina.
Enhance page Migliora la pagina
Done adding text Termina l'aggiunta del testo
Discard this text (Esc) Scarta questo testo (Esc)
Clean-up Pulizia
Original Originale
Greyscale Scala di grigi
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2240,6 +2240,8 @@ scroll speed Fast 速い
Enhance camera image カメラ画像を補正
Drag the corners onto the document's edges, then choose Enhance page. 四隅を文書の端までドラッグしてから、「ページを補正」を選択します。
Enhance page ページを補正
Done adding text テキストの追加を完了
Discard this text (Esc) このテキストを破棄(Esc)
Clean-up クリーンアップ
Original オリジナル
Greyscale グレースケール
Can't render this file because it contains an unexpected character in line 6 and column 47.
+89 -5
View File
@@ -472,6 +472,9 @@ pub fn action_label(text: &str) -> String {
{
return fmt(t("Change {key}"), &[("key", t(key))]);
}
if let Some(n) = text.strip_prefix("Delete ").and_then(|rest| rest.strip_suffix(" fields")).and_then(|n| n.parse::<u64>().ok()) {
return trn(current(), n, "Delete {n} field", "Delete {n} fields");
}
t(text).to_owned()
}
@@ -760,7 +763,7 @@ mod tests {
let error = pdfcraft_engine::ocr::OcrError::NoModels.to_string();
let notice = "Text recognition isn't installed: its model files are missing. Reinstall PdfCraft, or set PDFCRAFT_MODELS to the folder that holds them.";
assert!(include_str!("../ocr_ui.rs").contains(notice), "keep in step with ocr_ui.rs");
for code in ["bg", "de", "es", "fr", "hu", "ja", "ru", "te", "uk", "zh-hans", "zh-hant"] {
for code in ["bg", "cs", "de", "es", "fr", "hu", "ja", "ru", "te", "uk", "zh-hans", "zh-hant"] {
let lang = Lang::from_code(code).expect("registered");
assert!(has(lang, notice) && has(lang, &error), "{code}");
}
@@ -823,7 +826,7 @@ mod tests {
labels.insert(label.to_string());
}
assert!(labels.len() > 20 && labels.contains("Comb of characters"), "the scan found the dialog's labels: {labels:?}");
for code in ["bg", "de", "es", "fr", "hu", "it", "ja", "pt-br", "ru", "te", "uk", "zh-hans", "zh-hant"] {
for code in ["bg", "cs", "de", "es", "fr", "hu", "it", "ja", "pt-br", "ru", "te", "uk", "zh-hans", "zh-hant"] {
let lang = Lang::from_code(code).expect("registered");
let missing: Vec<_> = labels.iter().filter(|l| !has(lang, l)).collect();
assert!(missing.is_empty(), "{code} lacks Shared Field Properties labels: {missing:#?}");
@@ -1275,13 +1278,38 @@ mod tests {
assert!(has(cs, menu), "Czech lacks the menu title {menu:?}");
assert!(has(cs, spec.label), "Czech lacks the {menu} menu label {:?} ({})", spec.label, spec.id);
}
const KEEP_AS_IS: &[&str] = &["OK"];
let (entries, _) = parse_entries(cs.0.source, cs.0.plural_forms());
// Spelled the same in Czech: product and format names, abbreviations, key caps, words
// Czech shares with English, and templates with no English words.
const KEEP_AS_IS: &[&str] = &[
"OK",
"PDF/A…",
"Microsoft Word (.docx)",
"PostScript / EPS",
"Home / End",
"JavaScript",
"ZIP",
"Text",
"Index",
"Metadata",
"Model",
"{field}: {e}",
"{0}: {1}",
"{month} {y}",
"+LOC",
"−LOC",
"ΔLOC",
"+Bin",
"−Bin",
];
let (entries, errors) = parse_entries(cs.0.source, cs.0.plural_forms());
assert!(errors.is_empty(), "{errors:?}");
// The code joins some labels with a space at either end: keep exactly the source's.
let edges = |s: &str| (s.len() - s.trim_start().len(), s.len() - s.trim_end().len());
for e in &entries {
let key_cap = e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str());
assert!(e.source != e.translation || KEEP_AS_IS.contains(&e.source.as_str()) || key_cap, "{:?} is untranslated", e.source);
assert!(!e.translation.contains("..."), "use … rather than three dots: {:?}", e.translation);
assert_eq!(e.translation.trim(), e.translation, "stray whitespace: {:?}", e.translation);
assert_eq!(edges(&e.translation), edges(&e.source), "stray whitespace: {:?}", e.translation);
}
let mut app = crate::PdfCraftApp::default();
app.set_option("language", "cs").unwrap();
@@ -1292,6 +1320,62 @@ mod tests {
assert_eq!(restored.language, "cs");
}
/// Czech translates every registered command, menu and All tools label.
#[test]
fn czech_covers_commands_and_catalogue() {
let cs = Lang::from_code("cs").expect("cs registered");
for command in pdfcraft_engine::commands::COMMANDS {
assert!(has(cs, command.label), "missing command: {}", command.label);
if let Some(menu) = command.menu {
assert!(has(cs, menu), "missing menu: {menu}");
}
}
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
assert!(has(cs, group.label), "missing group: {}", group.label);
for section in group.sections {
assert!(has(cs, section.title), "missing section: {}", section.title);
for item in section.items {
assert!(has(cs, item.label), "missing item: {}", item.label);
}
}
}
}
/// Czech translates every `tl!("…")` literal in the UI source.
#[test]
fn czech_covers_ui_literals() {
let cs = Lang::from_code("cs").expect("cs registered");
let literals = ui_literals();
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
let missing: Vec<_> = literals.iter().filter(|label| !has(cs, label)).collect();
assert!(missing.is_empty(), "untranslated Czech UI literals: {missing:#?}");
}
/// Czech counts take three forms (1; 2–4; 0 and 5+), dates use the genitive month after the
/// day, and history labels keep captured names.
#[test]
fn czech_plurals_dates_and_history_labels() {
let cs = Lang::from_code("cs").expect("cs registered");
assert_eq!(trn(cs, 1, "{n} page", "{n} pages"), "1 stránka");
assert_eq!(trn(cs, 3, "{n} page", "{n} pages"), "3 stránky");
assert_eq!(trn(cs, 5, "{n} page", "{n} pages"), "5 stránek");
assert_eq!(trn(cs, 0, "{n} field", "{n} fields"), "0 polí");
assert_eq!(trn(cs, 1, "{n} byte", "{n} bytes"), "1 bajt");
assert_eq!(trn(cs, 512, "{n} byte", "{n} bytes"), "512 bajtů");
assert_eq!(trn(cs, 3, "{n} image optimized", "{n} images optimized"), "3 obrázky optimalizovány");
assert_eq!(tr(cs, "October"), "Říjen");
let month = tr_ctx(cs, "calendar date", "October");
assert_eq!(fmt(tr(cs, "{month} {day}, {y}"), &[("month", month), ("day", "9"), ("y", "2026")]), "9. října 2026");
set_current(cs);
assert_eq!(command_label("Undo Insert pages from Zpráva {n}.pdf"), "Zpět: Vložit stránky ze souboru Zpráva {n}.pdf");
assert_eq!(command_label("Redo Fill in Kontakt {key}"), "Znovu: Vyplnit Kontakt {key}");
assert_eq!(action_label("Change Title"), "Změnit: Název");
assert_eq!(action_label("Delete 3 fields"), "Odstranit 3 pole");
assert_eq!(action_label("Delete 12 fields"), "Odstranit 12 polí");
assert_eq!(fmt(t("{n} pages selected"), &[("n", "3")]), "Vybráno stránek: 3");
set_current(Lang::EN);
}
#[test]
fn simplified_chinese_is_registered() {
let zh = Lang::from_code("zh-hans").expect("zh-hans registered");
+2
View File
@@ -2254,6 +2254,8 @@ scroll speed Fast Rápida
Enhance camera image Aprimorar foto da câmera
Drag the corners onto the document's edges, then choose Enhance page. Arraste os cantos até as bordas do documento e escolha Aprimorar página.
Enhance page Aprimorar página
Done adding text Concluir a adição de texto
Discard this text (Esc) Descartar este texto (Esc)
Clean-up Limpeza
Original Original
Greyscale Escala de cinza
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -2153,6 +2153,8 @@ scroll speed Fast Высокая
Enhance camera image Улучшить снимок с камеры
Drag the corners onto the document's edges, then choose Enhance page. Перетащите углы к краям документа, затем выберите «Улучшить страницу».
Enhance page Улучшить страницу
Done adding text Завершить добавление текста
Discard this text (Esc) Отклонить этот текст (Esc)
Clean-up Очистка
Original Оригинал
Greyscale Оттенки серого
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -2262,6 +2262,8 @@ scroll speed Fast Швидка
Enhance camera image Поліпшити знімок з камери
Drag the corners onto the document's edges, then choose Enhance page. Перетягніть кути до країв документа, потім виберіть «Поліпшити сторінку».
Enhance page Поліпшити сторінку
Done adding text Завершити додавання тексту
Discard this text (Esc) Відкинути цей текст (Esc)
Clean-up Очищення
Original Оригінал
Greyscale Відтінки сірого
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2204,6 +2204,8 @@ scroll speed Fast 快
Enhance camera image 增强相机图像
Drag the corners onto the document's edges, then choose Enhance page. 将四个角拖到文档边缘,然后选择“增强页面”。
Enhance page 增强页面
Done adding text 完成添加文本
Discard this text (Esc) 放弃此文本 (Esc)
Clean-up 清理
Original 原始
Greyscale 灰度
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2 -2
View File
@@ -89,9 +89,9 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
egui::Grid::new("ocr-settings").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
ui.label(tl!("Document language"));
let name = LANGUAGES.iter().find(|l| l.0 == d.language).map_or("English", |l| l.1);
egui::ComboBox::from_id_salt("ocr-language").selected_text(name).width(220.0).show_ui(ui, |ui| {
egui::ComboBox::from_id_salt("ocr-language").selected_text(tl!(name)).width(220.0).show_ui(ui, |ui| {
for (code, name) in LANGUAGES {
ui.selectable_value(&mut d.language, (*code).to_string(), *name);
ui.selectable_value(&mut d.language, (*code).to_string(), tl!(*name));
}
});
ui.end_row();
+5 -7
View File
@@ -91,17 +91,15 @@ fn stage_label(stage: OptimizeStage) -> String {
/// What the saved notice adds after the size: images optimized and items discarded.
fn summary(r: &OptimizeReport) -> String {
let o = &r.optimize;
let lang = crate::i18n::current();
let mut parts = Vec::new();
if o.images_resampled + o.images_recompressed > 0 {
parts.push(format!(
"{} image{} optimized",
o.images_resampled + o.images_recompressed,
if o.images_resampled + o.images_recompressed == 1 { "" } else { "s" }
));
let images = o.images_resampled + o.images_recompressed;
if images > 0 {
parts.push(crate::i18n::trn(lang, images as u64, "{n} image optimized", "{n} images optimized"));
}
let discarded: usize = r.discarded.iter().map(|(_, n)| n).sum();
if discarded > 0 {
parts.push(format!("{discarded} item{} discarded", if discarded == 1 { "" } else { "s" }));
parts.push(crate::i18n::trn(lang, discarded as u64, "{n} item discarded", "{n} items discarded"));
}
if parts.is_empty() { String::new() } else { format!("; {}", parts.join(", ")) }
}
+1 -1
View File
@@ -175,7 +175,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
ui.painter().text(
page.center_bottom() + vec2(0.0, 6.0),
egui::Align2::CENTER_TOP,
format!("Page {}", current + 1),
crate::i18n::fmt(tl!("Page {label}"), &[("label", &(current + 1).to_string())]),
theme::regular(11.0),
t.text_faint,
);
+6 -1
View File
@@ -1282,10 +1282,15 @@ pub fn human_size(n: usize) -> String {
match n {
n if n >= 1 << 20 => format!("{:.1} MB", n as f64 / (1u64 << 20) as f64),
n if n >= 1 << 10 => format!("{:.1} KB", n as f64 / (1u64 << 10) as f64),
n => format!("{n} bytes"),
n => byte_count(n),
}
}
/// An exact size in bytes, with the count's own plural form.
pub fn byte_count(n: usize) -> String {
crate::i18n::trn(crate::i18n::current(), n as u64, "{n} byte", "{n} bytes")
}
#[cfg(test)]
mod page_moves {
use super::{moved_index, panel_gap};
+1 -1
View File
@@ -1481,7 +1481,7 @@ fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens)
}
}
if let Some(e) = error {
ui.label(egui::RichText::new(e).small().color(t.text_muted));
ui.label(egui::RichText::new(tl!(&e)).small().color(t.text_muted));
}
ui.add_space(8.0);
ui.label(egui::RichText::new(tl!("Actions")).font(theme::semibold(13.0)));
+2 -2
View File
@@ -318,7 +318,7 @@ impl PdfCraftApp {
let settings = match self.print_draft.settings(doc.info.pages.len(), &labels) {
Ok(s) => s,
Err(e) => {
self.notify_error(e);
self.notify_tr(&e);
return false;
}
};
@@ -786,7 +786,7 @@ pub(crate) fn body(
ui.painter().rect_filled(area, 6.0, t.hover);
match (&settings, sheets.get(d.sheet)) {
(Err(e), _) => {
ui.put(area.shrink(16.0), egui::Label::new(egui::RichText::new(e).color(t.text_muted)).wrap());
ui.put(area.shrink(16.0), egui::Label::new(egui::RichText::new(tl!(e)).color(t.text_muted)).wrap());
}
(Ok(_), Some(sheet)) => {
let k = (PREVIEW_BOX.0 / sheet.size.0 as f32).min(PREVIEW_BOX.1 / sheet.size.1 as f32);
+16 -13
View File
@@ -551,7 +551,7 @@ fn title(ui: &mut egui::Ui, text: &str) {
fn error(ui: &mut egui::Ui, err: &Option<String>) {
if let Some(e) = err.as_ref().filter(|e| !e.is_empty()) {
ui.colored_label(Color32::from_rgb(0xD7, 0x37, 0x3F), e);
ui.colored_label(Color32::from_rgb(0xD7, 0x37, 0x3F), tl!(e));
ui.add_space(4.0);
}
}
@@ -955,7 +955,12 @@ pub(crate) fn banner(sigs: &[SignatureInfo]) -> Option<(&'static str, Color32, &
{
let by = c.certificate.as_ref().map(|x| {
let org = x.subject.organization().map(|o| format!(", {o}")).unwrap_or_default();
format!("{}{org}, certificate issued by {}.", x.display_name(), x.issuer.common_name().unwrap_or("an unknown issuer"))
let who = format!("{}{org}", x.display_name());
let (template, issuer) = match x.issuer.common_name() {
Some(issuer) => ("{who}, certificate issued by {issuer}.", issuer),
None => ("{who}, certificate issued by an unknown issuer.", ""),
};
crate::i18n::fmt(tl!(template), &[("who", &who), ("issuer", issuer)])
});
return Some(("badge-check", Color32::from_rgb(0x14, 0x73, 0xE6), "Certified by {by}", by.unwrap_or_default()));
}
@@ -1016,16 +1021,14 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, sigs: &[SignatureInfo], expan
for s in ordered {
let open = expanded.contains(&s.field);
let (icon, color) = status_icon(s);
let head = if s.signed {
format!(
"Rev. {}: {} by {}",
s.revision,
if s.certify.is_some() { "Certified" } else { "Signed" },
s.signer.as_deref().unwrap_or("an unknown signer")
)
} else {
format!("Unsigned signature field: {}", s.field)
let (template, name) = match (s.signed, s.certify.is_some(), s.signer.as_deref()) {
(false, ..) => ("Unsigned signature field: {name}", s.field.as_str()),
(true, true, Some(signer)) => ("Rev. {rev}: Certified by {name}", signer),
(true, true, None) => ("Rev. {rev}: Certified by an unknown signer", ""),
(true, false, Some(signer)) => ("Rev. {rev}: Signed by {name}", signer),
(true, false, None) => ("Rev. {rev}: Signed by an unknown signer", ""),
};
let head = crate::i18n::fmt(tl!(template), &[("rev", &s.revision.to_string()), ("name", name)]);
let resp = ui
.horizontal(|ui| {
let chevron = if open { "chevron-down" } else { "chevron-right" };
@@ -1172,8 +1175,8 @@ fn key_usage(bits: u16) -> String {
"Encipher Only",
"Decipher Only",
];
let used: Vec<&str> = NAMES.iter().enumerate().filter(|(i, _)| bits & (1 << i) != 0).map(|(_, n)| *n).collect();
if used.is_empty() { "None".into() } else { used.join(", ") }
let used: Vec<&str> = NAMES.iter().enumerate().filter(|(i, _)| bits & (1 << i) != 0).map(|(_, n)| tl!(*n)).collect();
if used.is_empty() { tl!("None").into() } else { used.join(", ") }
}
/// What the viewer asks for.
+1 -1
View File
@@ -110,7 +110,7 @@ fn month_names_follow_the_date_language_or_else_the_interface_language() {
let text = |h: &Harness<'static, PdfCraftApp>| h.state().date_text(None).unwrap();
assert_eq!(text(&h), h.state().session.today_text(None, Some("cs")).unwrap(), "follows the interface (Czech)");
// The picker offers following the interface first, then the interface languages.
assert!(shows(&h, "Same as interface (Čeština)"));
assert!(shows(&h, "Stejný jako rozhraní (Čeština)"));
h.state_mut().set_option("date-language", "es").unwrap();
h.run_steps(2);
assert_eq!(text(&h), h.state().session.today_text(None, Some("es")).unwrap());