From b45dfde4982e34f06b5a10b3a10e95e3d879e879 Mon Sep 17 00:00:00 2001 From: flyyangX <60176035+flyyangX@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:46:52 +0800 Subject: [PATCH] i18n: complete Simplified Chinese catalog and localize brush sections (#359) * i18n: complete Simplified Chinese UI catalog and brush labels * i18n(zh-hans): translate labels added on main since this PR (opening/cancel, layer navigation) Co-Authored-By: Claude Opus 5.5 * i18n(zh-hans): keep main's Mixer Brush entries, translate the rendering-mode strings added since (#434) Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/brush_panel.rs | 4 +- crates/ui-egui/src/brush_panel_tests.rs | 39 ++++++- crates/ui-egui/src/i18n/mod.rs | 28 ++++- crates/ui-egui/src/i18n/zh-hans.tsv | 139 +++++++++++++++++++++++- docs/localization-zh-hans.md | 81 +++++++------- docs/ui-design.md | 4 +- 6 files changed, 242 insertions(+), 53 deletions(-) diff --git a/crates/ui-egui/src/brush_panel.rs b/crates/ui-egui/src/brush_panel.rs index 3e7459f10..f6a1a468c 100644 --- a/crates/ui-egui/src/brush_panel.rs +++ b/crates/ui-egui/src/brush_panel.rs @@ -263,7 +263,7 @@ fn section_list(app: &mut PhotocraftApp, ui: &mut egui::Ui, b: &mut BrushSetting } let color = if sel { t.text } else { t.text_dim }; let font = if i == 0 { theme::semibold(12.0) } else { theme::medium(12.0) }; - ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, *name, font, color); + ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, tl!(name), font, color); let mut lock_clicked = false; if let Some(lock) = section_lock(b, i) { let lr = egui::Rect::from_center_size(egui::pos2(r.right() - 11.0, r.center().y), vec2(18.0, 18.0)); @@ -300,7 +300,7 @@ fn settings_tab(app: &mut PhotocraftApp, ui: &mut egui::Ui) { widgets::vline(ui, 482.0); ui.vertical(|ui| { ui.set_width(WIDTH - 190.0); - ui.label(RichText::new(SECTIONS[section].0).font(theme::semibold(12.5)).color(t.text)); + ui.label(RichText::new(tl!(SECTIONS[section].0)).font(theme::semibold(12.5)).color(t.text)); ui.add_space(6.0); let on = section_flag(&mut b, section).is_none_or(|f| *f); egui::ScrollArea::vertical().id_salt(("brush-section", section)).max_height(450.0).auto_shrink([false, false]).show(ui, |ui| { diff --git a/crates/ui-egui/src/brush_panel_tests.rs b/crates/ui-egui/src/brush_panel_tests.rs index ecfcdeb32..4c3244514 100644 --- a/crates/ui-egui/src/brush_panel_tests.rs +++ b/crates/ui-egui/src/brush_panel_tests.rs @@ -446,17 +446,24 @@ fn brushes_panel_groups_collapse_and_filter() { /// A fresh session with the Brush tool: shortcuts, the options bar and the Brush Settings window, /// in the default theme (#258). fn app_harness() -> Harness<'static, PhotocraftApp> { + app_harness_with_language(crate::i18n::Lang::EN) +} + +fn app_harness_with_language(lang: crate::i18n::Lang) -> Harness<'static, PhotocraftApp> { let mut app = app(); app.ui.tool = crate::state::Tool::Brush; let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).build_ui_state( - |ui, app: &mut PhotocraftApp| { + move |ui, app: &mut PhotocraftApp| { let ctx = ui.ctx().clone(); if !ctx.fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) { return; } + let previous = crate::i18n::current(); + crate::i18n::set_current(lang); crate::shortcuts::handle(app, &ctx); crate::panels::options_bar(app, ui); window(app, &ctx); + crate::i18n::set_current(previous); }, app, ); @@ -465,6 +472,36 @@ fn app_harness() -> Harness<'static, PhotocraftApp> { h } +#[test] +fn brush_sections_paint_simplified_chinese_labels_and_heading() { + fn collect_text(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Text(text) => out.push(text.galley.job.text.clone()), + egui::Shape::Vec(shapes) => { + for shape in shapes { + collect_text(shape, out); + } + } + _ => {} + } + } + let zh = crate::i18n::Lang::from_code("zh-hans").unwrap(); + let mut h = app_harness_with_language(zh); + h.key_press(egui::Key::F5); + h.run_steps(3); + let mut painted = Vec::new(); + for shape in &h.output().shapes { + collect_text(&shape.shape, &mut painted); + } + for (name, _) in SECTIONS { + let translated = crate::i18n::tr(zh, name); + assert!(painted.iter().any(|s| s == translated), "{name} must paint as {translated}"); + assert!(!painted.iter().any(|s| s == name), "{name} must not paint in English"); + } + let heading = crate::i18n::tr(zh, "Brush Tip Shape"); + assert_eq!(painted.iter().filter(|s| s.as_str() == heading).count(), 2, "both the section row and heading are translated"); +} + #[test] fn f5_edits_the_default_brush_with_every_section() { use egui_kittest::kittest::Queryable; diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index d23b21efe..10ddc2b07 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -74,7 +74,7 @@ pub static LANGUAGES: [LangInfo; 7] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, LangInfo { - code: "zh-hans", name: "简体中文", source: include_str!("zh-hans.tsv"), plural: plural_none, complete_menus: false, catalog: OnceLock::new() + code: "zh-hans", name: "简体中文", source: include_str!("zh-hans.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, // Traditional Chinese in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*` // locales all resolve here (see `candidates`). @@ -341,6 +341,22 @@ mod tests { assert_eq!(Lang::from_pref("xx-unknown"), Lang::EN); } + #[test] + fn simplified_chinese_covers_dynamic_shortcuts_and_layer_counts() { + let zh = Lang::from_code("zh-hans").expect("zh-hans registered"); + assert!(zh.complete_menus(), "Simplified Chinese must participate in the coverage gates"); + assert_eq!(Lang::from_pref("ZH-Hans"), zh); + assert_eq!(tr(zh, "Pixel Layer"), "像素图层"); + assert_eq!(tr(zh, "System Info"), "系统信息"); + for key in ["⌥", "Alt"] { + assert_eq!(fmt(tr(zh, "Add a mask (from the selection; {key} inverts)"), &[("key", key)]), format!("添加蒙版 (基于选区;{key} 反相)")); + } + for n in [0, 1, 3] { + assert_eq!(trn(zh, n, "{n} layer", "{n} layers"), format!("{n} 个图层")); + } + assert_eq!(tr(zh, "no such label"), "no such label"); + } + #[test] fn spanish_resolves_and_pluralises() { let es = Lang::from_code("es").expect("es registered"); @@ -529,6 +545,16 @@ mod tests { } } + /// Section names are dynamic labels, so the literal scanner cannot cover them. + #[test] + fn brush_section_names_are_translated() { + for lang in Lang::all().filter(|l| l.complete_menus()) { + for (name, _) in crate::brush_panel::SECTIONS { + assert!(lang.0.catalog().plain(name).is_some(), "{} missing brush section: {name}", lang.code()); + } + } + } + /// Blend mode names come from the colour crate; each must be translated. #[test] fn blend_mode_names_are_translated() { diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index dec282b24..ed5f6698e 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5,9 +5,10 @@ # with a single Chinese form. Other contexts disambiguate the English meaning. # Escapes inside columns: \n, \t, \\. Keep {name} placeholders and trailing …. # -# Original translations from the meaning of the English source keys in PhotoCraft -# PR #169 (c1fb8909c4b3b176eead12d66b04338a103cf63d). No proprietary translation -# resources were consulted or extracted. Licensed MIT OR Apache-2.0, as the code. +# Original translations from the meaning of PhotoCraft's English source keys, +# including the current menus, panels and platform-specific shortcut templates. +# No proprietary translation resources were consulted or extracted. +# Licensed MIT OR Apache-2.0, as the code. # Product and technology names retain their spelling. See docs/localization-zh-hans.md. File 文件 @@ -1143,8 +1144,6 @@ Midtones 中间调 Midtones from: 中间调下限: Midtones to: 中间调上限: - Mix 混合 - Mixer Brush 混合笔刷 Mixer Brush Tool 混合画笔工具 Minimum Depth 最小深度 Minimum Diameter 最小直径 @@ -1676,7 +1675,6 @@ Web Minimum Web 最小尺寸 Web Most Common Web 常用尺寸 WebP (lossless) WebP(无损) - Wet 湿度 Wet Edges 湿边 Yellows 黄色 Zoom Tool 缩放工具 @@ -1938,3 +1936,132 @@ cameraRaw Blending 混合 Patch Tool 修补工具 Patch: 修补: Lasso around an area, then drag the selection 用套索圈出区域,然后拖动选区 +# Current shell labels and shortcut templates. Keep {key} platform-dependent. + Add a mask (from the selection; {key} inverts) 添加蒙版 (基于选区;{key} 反相) + Add to selection ({key}) 添加到选区 ({key}) + Click the first point or press {key} to close · Esc cancels 单击起点或按 {key} 闭合 · Esc 取消 + Click to add a point · drag off or {key}-click or Delete to remove 单击添加点 · 拖离、按 {key} 单击或按 Delete 移除 + Click to apply, {key}-click to clear overrides, double-click for Style Options 单击应用,按 {key} 单击清除覆盖设置,双击打开样式选项 + Click to zoom in · {key}-click to zoom out · drag right/left to zoom in/out 单击放大 · 按 {key} 单击缩小 · 向右或向左拖动以放大或缩小 + Click: corner · Drag: smooth · Click first point: close · {key} finish · Esc cancel 单击:角点 · 拖动:平滑点 · 单击起点:闭合 · {key} 完成 · Esc 取消 + Commit Perspective Warp ({key}) 应用透视变形({key}) + Commit Puppet Warp ({key}) 应用操控变形({key}) + Commit any current edits ({key}) 应用当前所有编辑({key}) + Commit current crop operation ({key}) 应用当前裁剪操作 ({key}) + Commit transform ({key}) 应用变换({key}) + Commit warp ({key}) 应用变形({key}) + Create a new layer 创建图层 + Intersect with selection ({key}) 与选区相交 ({key}) + New document… 新建文档… + Search commands 搜索命令 + Show Guides 显示参考线 + Subtract from selection ({key}) 从选区中减去 ({key}) + ↑↓ navigate {key} run esc close ↑↓ 导航 {key} 运行 esc 关闭 +@plural {n} layer|{n} layers {n} 个图层 + Collapse group ({key}-click: all groups) 折叠组 (按 {key} 单击:所有组) + Expand group ({key}-click: all groups) 展开组 (按 {key} 单击:所有组) + Hide the layer's effects ({key}-click: all layers) 隐藏图层效果 (按 {key} 单击:所有图层) + Show the layer's effects ({key}-click: all layers) 显示图层效果 (按 {key} 单击:所有图层) + System Info… 系统信息… + System Info 系统信息 + More panels 更多面板 + These settings aren't available in PhotoCraft yet. PhotoCraft 暂不支持这些设置。 + Graphics 图形 + Reset GPU Backend 重置 GPU 后端 + Applies at next launch. 下次启动时生效。 + Click to sample the foreground colour · {key}-click for background 单击拾取前景色 · 按 {key} 单击拾取背景色 + {key}-click to set the source 按 {key} 单击设置源点 + {key} to subtract 按 {key} 从选区中减去 + Clone source {n} ({key}-click with Clone Stamp to set) 仿制源 {n}(使用仿制图章时按 {key} 单击设置) + Source: not set ({key}-click with Clone Stamp) 源点:未设置(使用仿制图章时按 {key} 单击) + Pixel Layer 像素图层 + Type Layer 文字图层 + Shape Layer 形状图层 + Group Layer 图层组 + Fill Layer 填充图层 + Smart Object Layer 智能对象图层 + Create Warped Text 创建变形文字 + Rasterize Smart Object 栅格化智能对象 + Rasterize Fill Content 栅格化填充内容 + Unlock: picking a preset replaces these settings 解锁:选择预设时替换这些设置 + Lock: keep these settings when picking another preset 锁定:选择其他预设时保留这些设置 + Diameter: 直径: + Hardness: 硬度: + No brush presets 没有画笔预设 + Toggle the Brush Settings panel (F5) 切换画笔设置面板 (F5) + Create new brush preset from the current settings 根据当前设置创建画笔预设 + Brush Settings… (F5): every option of this brush 画笔设置… (F5):此画笔的全部选项 + Rename Brush… 重命名画笔… + Delete Brush 删除画笔 + Group name 组名称 + Brush name 画笔名称 + Grid view 网格视图 + List view 列表视图 + Off: the speed of the pointer sets the spacing 关闭:由指针移动速度决定间距 + Tilt Scale 倾斜缩放 + Brush Projection 画笔投影 + Apply the pen's tilt and rotation to the tip shape 将笔的倾斜和旋转应用于笔尖形状 + Mixer Brush 混合器画笔 + Wet 湿润 + Mix 混合 + Wetness Jitter 湿润度抖动 + Mix Jitter 混合抖动 + Show the layer mask as an overlay 以叠加方式显示图层蒙版 + Toggle visibility 切换可见性 + Unlink the mask from the layer 取消蒙版与图层的链接 + Link the mask to the layer 将蒙版与图层链接 + Gradient 渐变 + Gradient Options 渐变选项 + Align with layer 与图层对齐 + Reset Alignment 重置对齐 + Centre the gradient (offset 0, 0) 将渐变居中(偏移量 0, 0) + The current gradient (pick one in Window › Gradients) 当前渐变(在“窗口 › 渐变”中选择) + Select a type layer to edit its character and paragraph settings. 选择文字图层以编辑字符和段落设置。 + Text color 文字颜色 + Type Options 文字选项 + Anti-aliasing method 消除锯齿方式 + Horizontal type 横排文字 + Vertical type 直排文字 + Fill path with foreground color 以前景色填充路径 + Stroke path with brush 用画笔描边路径 + Load path as a selection 将路径载入选区 + Make work path from selection 从选区生成工作路径 + Add vector mask 添加矢量蒙版 + Create new path 创建路径 + Delete path 删除路径 + Delete vector mask 删除矢量蒙版 + CPU (no GPU acceleration) CPU(无 GPU 加速) + DirectX 12 DirectX 12 + GPU backend GPU 后端 + Metal Metal + OpenGL OpenGL + Use tablet pressure 使用数位板压感 + Vulkan Vulkan + No path to make a selection from: draw one with the Pen tool or select one in the Paths panel. 没有可用于生成选区的路径:请用钢笔工具绘制路径,或在路径面板中选择路径。 + +# Layer navigation, copy transforms and cancellable opening (labels added on main since this catalog). + Cancel opening 取消打开 + Cancelled opening {name} 已取消打开 {name} + Cancelled {label} 已取消 {label} + Esc to cancel 按 Esc 取消 + Opening {name} 正在打开 {name} + Opening {name}… 正在打开 {name}… + Opening… 正在打开… + Reading… 正在读取… + Working… 正在处理… + +# Rendering mode and recovery + Rendering Mode 渲染模式 + Automatic (recommended) 自动(推荐) + CPU / Compatibility CPU / 兼容模式 + Automatic uses GPU acceleration when available and falls back to CPU rendering on errors. 自动模式会在可用时使用 GPU 加速,出错时改用 CPU 渲染。 + Switched to CPU rendering 已切换到 CPU 渲染 + GPU acceleration could not continue. Your documents are unchanged. GPU 加速无法继续。您的文档未更改。 + PhotoCraft is using the CPU image compositor. The window may still use your graphics adapter. PhotoCraft 正在使用 CPU 合成图像。窗口仍可能使用您的显卡。 + Retry GPU requires a restart. Save your work first. 重试 GPU 需要重新启动。请先保存您的工作。 + Keep Using CPU 继续使用 CPU + Retry GPU 重试 GPU + Advanced 高级 + GPU Backend GPU 后端 + Details 详细信息 + Rendering mode 渲染模式 diff --git a/docs/localization-zh-hans.md b/docs/localization-zh-hans.md index 32d2eb82e..d92829f36 100644 --- a/docs/localization-zh-hans.md +++ b/docs/localization-zh-hans.md @@ -1,40 +1,45 @@ # Simplified Chinese catalog -The `zh-hans` catalog targets the i18n interface proposed in -[PhotoCraft PR #169](https://github.com/storytold/photocraft/pull/169), commit -`c1fb8909c4b3b176eead12d66b04338a103cf63d`. It translates the English source keys -from that revision's catalog. The Chinese wording is original and uses ordinary -image-editing terminology; no proprietary translation resources were extracted -or copied. Contributions use the repository's MIT OR Apache-2.0 license. +The `zh-hans` catalog is registered as **简体中文** in the shared i18n foundation +introduced by [PhotoCraft PR #169](https://github.com/storytold/photocraft/pull/169). +Simplified Chinese support landed in +[PR #282](https://github.com/storytold/photocraft/pull/282), after the v0.2.0 release. +That release does not contain the language selector or this catalog; use a build +that includes the localisation changes. -## Draft status +The Chinese wording is original and uses ordinary image-editing terminology; +no proprietary translation resources were extracted or copied. Contributions +use the repository's MIT OR Apache-2.0 license. -This contribution contains catalog data and documentation for translation review. -The catalog is not loaded by current main, so it does not yet enable a Chinese UI. -The integration below has been validated locally against the referenced #169 -revision. Language registration and its tests will follow the shared i18n -foundation adopted upstream; the translations can be retained if that design -changes. +## Selecting Simplified Chinese + +Select **Preferences > Interface > Language > 简体中文**, or set +`interface.language` to `zh-hans` through the existing `prefs.set` command. +The native build's **Auto** setting reads `LANG`/`LC_*`, the macOS preferred +languages and the Windows user locale. `zh`, `zh-CN`, `zh-SG` and `zh-Hans` +variants resolve to this catalog. Traditional Chinese locales (`zh-TW`, `zh-HK`, +`zh-MO`, `zh-Hant`) resolve to the separate `zh-hant` catalog. + +The web build does not detect the browser language automatically; choose the +language manually there. ## Files and integration - `crates/ui-egui/src/i18n/zh-hans.tsv` contains the translations, independent of the lookup implementation. Its UTF-8 columns are `contextsourcetranslation`. -- With #169's foundation, register `zh-hans` in `crates/ui-egui/src/i18n/mod.rs`, - displayed as `简体中文`, with one plural form and the complete-catalog checks. -- Once registered, select **Preferences > Interface > Language > 简体中文**, or set - `interface.language` to `zh-hans` through the existing `prefs.set` command. +- `crates/ui-egui/src/i18n/mod.rs` registers `zh-hans` with one plural form and + `complete_menus: true`. Shared tests enforce coverage of every menu string, + `tl!` literal, blend mode, brush section and generated preference label. +- Shortcut templates retain `{key}` so the shell supplies the correct key for + the current platform. Layer-count messages use one Chinese plural form. Keep English source keys, contexts, command IDs, placeholders and escapes intact. Retain the trailing `…` on commands that open a dialog. Missing translations use the framework's English fallback. User-supplied names and document data are not translated. Product names and technology names such as PhotoCraft, ArtCraft, -OpenType, RGB, CMYK and Lab retain their spelling. - -The #169 locale resolver recognizes `zh`, `zh-CN`, `zh-SG` and `zh-Hans` -variants. Traditional Chinese locales (`zh-TW`, `zh-HK`, `zh-MO`, `zh-Hant`) do -not select this catalog. Automatic OS/browser detection remains the responsibility -of the i18n foundation; manual language selection works independently of it. +OpenType, RGB, CMYK and Lab retain their spelling. Status messages, errors and +automation output remain English as described in [UI design](ui-design.md#localisation); +complete catalog coverage does not claim those strings are translated. ## Terminology @@ -57,24 +62,18 @@ of the i18n foundation; manual language selection works independently of it. Run `cargo test -p photocraft-ui-egui`, the touched-crate all-target clippy check, `cargo xtask layers` and `cargo xtask wasm`. The shared catalog tests validate -duplicate keys, placeholders, ellipses, menu coverage, `tl!` literals and blend -modes. Chinese-specific tests cover locale selection, fallback, plural messages -and formatted labels. Render and inspect the menus, Preferences and representative +duplicate keys, placeholders, ellipses, menu coverage, `tl!` literals, brush +sections and blend modes. Chinese-specific tests cover locale selection, fallback, +plural messages, formatted labels and the painted brush section rows and heading. Render and inspect the menus, Preferences and representative dialogs with the existing offscreen `snapshot` example. -Font delivery is separate from the translation data. This contribution adds no -font assets. The PR #169 revision predates the newer system CJK fallback changes; -test against the current upstream font implementation before publishing a -release, and verify Web glyph coverage separately. +Font delivery is separate from the translation data. The catalog adds no font +assets; the shell's CJK fallback loads fonts separately. Verify missing glyphs, +label layout and platform-specific shortcuts visually on each target platform, +and verify web glyph coverage separately. -Offscreen review on Windows confirmed that the PR #169 baseline's Japanese-first -font fallback leaves some Simplified Chinese glyphs missing. New Document and -Interface Preferences were also reviewed using the locally installed Microsoft -YaHei font in a temporary harness; their Chinese labels fit at 1280 × 800. That -font override is only a review aid and is not part of the production change. - -If #169's implementation changes, retain the catalog's English source and -context keys, adapt the registration/loader, and rerun the catalog checks. If a -different resource format is adopted, convert these three columns mechanically -instead of retranslating the text. Review additions and changed source meanings -against the new English UI. +When English labels change, update their source keys and translations together, +preserving contexts and placeholders. The complete-catalog tests catch missing +keys, and the Simplified Chinese regression test checks dynamic shortcut labels, +layer counts and English fallback. Review changed source meanings against the +current English UI. diff --git a/docs/ui-design.md b/docs/ui-design.md index f2f36556f..5398e212e 100644 --- a/docs/ui-design.md +++ b/docs/ui-design.md @@ -84,7 +84,7 @@ the tests enforce it): - English (`en`), the source language. - Japanese (`ja`), complete. -- Simplified Chinese (`zh-hans`; see [`localization-zh-hans.md`](localization-zh-hans.md)), partial. +- Simplified Chinese (`zh-hans`; see [`localization-zh-hans.md`](localization-zh-hans.md)), complete. - Traditional Chinese (`zh-hant`), complete, in the vocabulary used in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*` locales all resolve to it. The resolver distinguishes the two Chinese scripts, so neither catalog is shown to the other script's locales. @@ -107,7 +107,7 @@ the tests enforce it): Localised so far: menus, the command palette, dialogs and panels (literals wrapped in `tl!("…")`; widgets such as `checkbox`, `slider_row`, `dropdown` and the buttons translate their labels -themselves). A test fails when a `tl!` literal, a menu string, a blend mode name or a generated +themselves). A test fails when a `tl!` literal, a menu string, a blend mode name, brush section name or a generated preference label has no entry in a language marked `complete_menus`. Not translated: status-bar messages and errors (they stay English, also for agents), names that are user data (layers, styles, documents), strings assembled with `format!` that were not converted to `fmt`/`trn`. Not done yet: