From 7458f7d47d4e423af676123b17276339ed1ca7fe Mon Sep 17 00:00:00 2001 From: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com> Date: Fri, 9 Oct 2026 01:39:40 +0800 Subject: [PATCH] M14: complete Simplified Chinese About credits and multiline guidance (#311) Co-authored-by: Codex Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 2 +- ROADMAP.md | 2 ++ crates/ui-egui/src/credits.rs | 5 ++- crates/ui-egui/src/i18n/mod.rs | 51 ++++++++++++++++++++++++++--- crates/ui-egui/src/i18n/zh-hans.tsv | 41 +++++++++++++++++++++++ crates/ui-egui/tests/control.rs | 27 +++++++++++++++ docs/localization.md | 2 +- 7 files changed, 123 insertions(+), 7 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index b445d5137e..36087581b0 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -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 = "fbc6e61d3f602ada7f292a034174070c00c7c789290334714f3fff5c013af2ad" +sha256 = "5da2aa2944fcad4720972062f0f97800f73b8bfa5dd0fff6c28f4ec0f8ee19f9" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" diff --git a/ROADMAP.md b/ROADMAP.md index b1c29e5f91..2872582ae9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,6 +129,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-09 (M14, Simplified Chinese About):** The About tabs, contributor controls and statistics, and model table now have Simplified Chinese translations, along with multiline link-privacy and cut-and-stack guidance. Coverage checks include direct `i18n::t` calls, multiline literals and dynamic credit labels; UI control checks exercise the translated About tabs and tables. Chinese font delivery remains incomplete; overall ≈ 30–35% (unchanged). + - **2026-10-08 (M14, French):** Français (`fr`) is a complete interface catalog (about 1,880 entries), selected automatically for `fr`, `fr_FR`, `fr_CA` and other French locales, and from Preferences. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names. Latin UI faces cover the catalog. Localization remains partial for Czech and Brazilian Portuguese; overall ≈ 30–35% (unchanged). - **2026-10-08 (M14, right-to-left file names):** A tab with an Arabic or Hebrew file name showed replacement boxes. The interface now takes an `Arab` face from craft-fonts when the build input has one, and on desktop falls back to one font installed on the machine for characters no embedded face has (`PDFCRAFT_SYSTEM_FONTS=0` turns that off; README screenshots do). File names and paths in the tab strip, the recent-files list and Properties are put in display order, since egui joins the letters but does not reorder runs. Still missing: multi-line text (bookmarks, comments, search results), text input and a mirrored layout, so `misc.rtl-ui` stays planned; the craft-fonts revision pinned for releases predates its Arabic face, so release builds rely on the installed font until the pin moves. Overall estimate unchanged (about 30–35%). diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index 5f28e275c9..59535f35e6 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -12,6 +12,9 @@ use crate::theme::Tokens; /// The About dialog is a modal that sizes to its content; the lists scroll inside this height. const LIST_HEIGHT: f32 = 380.0; +/// English lookup keys for the model table, also checked by catalog coverage tests. +pub const MODEL_COLUMNS: [&str; 6] = ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"]; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Contributor { pub login: &'static str, @@ -325,7 +328,7 @@ pub fn models_ui(ui: &mut egui::Ui) { let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1); egui::ScrollArea::both().id_salt("credits_models").max_height(LIST_HEIGHT).auto_shrink([false, true]).show(ui, |ui| { egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| { - for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] { + for h in MODEL_COLUMNS { ui.label(RichText::new(crate::i18n::t(h)).strong()); } ui.end_row(); diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index f3e679d2dc..d88bc99edf 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -776,7 +776,24 @@ mod tests { } } - /// New tl!("literal") labels must not silently fall back to English in the complete catalog. + #[test] + fn simplified_chinese_covers_about_credit_labels() { + let zh = Lang::from_code("zh-hans").expect("zh-hans registered"); + for label in ["About", "Contributors", "Models"].into_iter().chain(crate::credits::MODEL_COLUMNS) { + assert!(has(zh, label), "missing About label: {label}"); + } + for mode in crate::credits::NameMode::ALL { + assert!(has(zh, mode.label()), "missing name mode: {}", mode.label()); + } + for key in crate::credits::SortKey::ALL { + let (label, header) = key.label(); + for text in [label, header] { + assert!(has(zh, text), "missing contributor sort/header: {text}"); + } + } + } + + /// Direct tl! and i18n::t labels must not silently fall back to English in the complete catalog. #[test] fn simplified_chinese_covers_ui_literals() { let zh = Lang::from_code("zh-hans").expect("zh-hans registered"); @@ -791,9 +808,14 @@ mod tests { stack.push(path); } } else if path.extension().is_some_and(|ext| ext == "rs") { - let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n"); + let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n").replace("crate::i18n::t(", "tl!("); let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default(); - while let Some((_, after)) = rest.split_once("tl!(\"") { + while let Some((_, after)) = rest.split_once("tl!(") { + let after = after.trim_start(); + let Some(after) = after.strip_prefix('"') else { + rest = after; + continue; + }; let mut escaped = false; let end = after .char_indices() @@ -806,7 +828,9 @@ mod tests { }) .expect("closed tl! literal"); let (raw, tail) = after.split_at(end); - if tail.starts_with("\")") { + let closing = tail.strip_prefix('"').expect("closing quote").trim_start(); + let closing = closing.strip_prefix(',').unwrap_or(closing).trim_start(); + if closing.starts_with(')') { // Current UI literals use the shared Rust/JSON string escapes. let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes"); literals.insert(label); @@ -836,6 +860,25 @@ mod tests { assert_eq!(trn(zh, 1, "{n} page", "{n} pages"), "1 页"); assert_eq!(trn(zh, 0, "{n} field", "{n} fields"), "0 个字段"); assert_eq!(trn(zh, 2, "{n} field", "{n} fields"), "2 个字段"); + assert_eq!(fmt(t("{n} contributors · {c} commits"), &[("n", "2"), ("c", "1,234")]), "2 位贡献者 · 1,234 次提交"); + let contributor = crate::credits::Contributor { + login: "reader{n}", + display_name: Some("Save"), + real_name: None, + prs: 2, + commits: 3, + lines_added: 10, + lines_deleted: 4, + binary_added: 1, + binary_deleted: 0, + first_commit: "2026-10-01T00:00:00Z", + last_commit: "2026-10-09T00:00:00Z", + }; + assert_eq!( + contributor.summary(), + "@reader{n}:2 个 PR,3 次提交,新增 10 行 / 删除 4 行(净增 +6 行),新增 1 个 / 删除 0 个二进制资源,2026-10-01 – 2026-10-09" + ); + assert_eq!(contributor.name(crate::credits::NameMode::DisplayName), "Save"); set_current(Lang::EN); } diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f073b480db..2eb43aaa0c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -8,6 +8,47 @@ # Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters; # the trailing "…" of dialog commands is kept. +# About, contributor/model credits, and multiline interface guidance. + About 关于 + Contributors 贡献者 + Models 模型 + Show 显示 + Username 用户名 + Display name 显示名称 + Real name 真实姓名 + Sort 排序 + Name (A–Z) 名称(A–Z) + Merged PRs 已合并的 PR + PRs PR 数 + Commits 提交数 + Lines added 新增行数 + Lines deleted 删除行数 + Line delta 净增行数 + Binary assets added 新增二进制资源 + Binary assets removed 删除二进制资源 + First commit 首次提交 + Last commit 最近提交 + +LOC 新增行 + −LOC 删除行 + ΔLOC 净增行 + +Bin 新增资源 + −Bin 删除资源 + First 首次提交 + Last 最近提交 + Reverse the order 反转排序 + Grab bag 名称列表 + Table 表格 + {n} contributors · {c} commits {n} 位贡献者 · {c} 次提交 + No contributor data was built into this copy. 此版本未包含贡献者数据。 + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}:{prs} 个 PR,{commits} 次提交,新增 {added} 行 / 删除 {deleted} 行(净增 {delta} 行),新增 {bin_added} 个 / 删除 {bin_deleted} 个二进制资源,{first} – {last} + No model credits were built into this copy. 此版本未包含模型贡献记录。 + Company 公司 + Model 模型 + % of all commits 占全部提交的比例 + Lines +/− 新增 / 删除行数 + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. 仅在您信任此文档时继续。链接地址可能携带文档中的信息,例如您在表单中输入的内容。 + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. 请使用单面打印。保持纸张顺序,沿裁切标记切开,然后按从左到右、从上到下的顺序叠放各摞纸张。 + Menu 菜单 File 文件 Edit 编辑 diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index 2afebce7a4..634fac3612 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -546,6 +546,33 @@ fn japanese_dialogs_errors_and_custom_action_names() { } } +#[test] +fn simplified_chinese_about_tabs_and_credit_controls() { + let (mut h, c) = harness(); + let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone(); + ok(&mut h, &c, "ui.set", json!({"key": "language", "value": "zh-hans"})); + ok(&mut h, &c, "ui.set", json!({"key": "dialog", "value": "about"})); + for label in ["关于", "贡献者", "模型"] { + let found = ok(&mut h, &c, "ui.inspect", json!({"query": label})); + assert!(found["widgets"].as_array().unwrap().iter().any(|w| w["label"] == label), "{found}"); + } + ok(&mut h, &c, "ui.click", json!({"label": "贡献者"})); + for label in ["用户名", "显示名称", "真实姓名", "排序", "名称列表", "表格", "首次提交"] { + let found = ok(&mut h, &c, "ui.inspect", json!({"query": label})); + assert!(found["count"].as_u64().unwrap() > 0, "{label}: {found}"); + } + ok(&mut h, &c, "ui.click", json!({"label": "表格"})); + for label in ["新增行", "删除行", "净增行", "新增资源", "删除资源"] { + let found = ok(&mut h, &c, "ui.inspect", json!({"query": label})); + assert!(found["count"].as_u64().unwrap() > 0, "{label}: {found}"); + } + ok(&mut h, &c, "ui.click", json!({"label": "模型"})); + let columns = ok(&mut h, &c, "ui.inspect", json!({"query": "占全部提交的比例"})); + let empty = ok(&mut h, &c, "ui.inspect", json!({"query": "此版本未包含模型贡献记录。"})); + assert!(columns["count"].as_u64().unwrap() > 0 || empty["count"].as_u64().unwrap() > 0, "{columns}, {empty}"); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"], documents); +} + #[test] fn simplified_chinese_signature_prompts_and_errors_keep_document_state() { let (mut h, c) = harness(); diff --git a/docs/localization.md b/docs/localization.md index 61b9cfe70e..582c830a21 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -18,7 +18,7 @@ Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com There is no right-to-left interface language yet. File names and document titles in Arabic or Hebrew are drawn with a craft-fonts `Arab` face when the build input has one; on desktop, a character no embedded face has is drawn with one font already installed on the machine (`PDFCRAFT_SYSTEM_FONTS=0` turns this off). The tab strip, the recent-files list and Properties show such names in display order; other places (bookmarks, comments, search results) have the glyphs but may order mixed-direction text wrongly. -Simplified Chinese and French coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Generated history labels and diagnostics are also tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140). +Simplified Chinese and French coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Simplified Chinese also checks direct `i18n::t` calls and multiline literals, plus the About tabs, contributor name modes, sort options, table headers and model columns. Generated history labels, contributor summaries and diagnostics are tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140). ## How translations work