M14: complete Simplified Chinese About credits and multiline guidance (#311)

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Flowershangfromthebranches
2026-10-08 13:39:40 -04:00
committed by GitHub
co-authored by Codex Brandon Thomas
parent 81a6ca456c
commit 7458f7d47d
7 changed files with 123 additions and 7 deletions
+1 -1
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 = "fbc6e61d3f602ada7f292a034174070c00c7c789290334714f3fff5c013af2ad"
sha256 = "5da2aa2944fcad4720972062f0f97800f73b8bfa5dd0fff6c28f4ec0f8ee19f9"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
+2
View File
@@ -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%).
+4 -1
View File
@@ -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();
+47 -4
View File
@@ -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);
}
+41
View File
@@ -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 编辑
Can't render this file because it contains an unexpected character in line 9 and column 16.
+27
View File
@@ -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();
+1 -1
View File
@@ -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