i18n: the viewer toolbar and the solid dialog ask the catalog for the text they draw

Text a helper draws for a panel was handed to that helper as a bare literal, so the catalog lookup
never happened and parts of the interface stayed English with a Chinese language chosen:

- the Composition panel's toolbar tooltips - Choose grid and guide options, Toggle Transparency
  Grid, Toggle Mask and Shape Path Visibility, Region of Interest, Show Channel and Color
  Management Settings, Reset Exposure, Take Snapshot, Show Snapshot (hold);
- the items of its four popups (grid and guides, resolution, magnification, channel) - Grid,
  Guides, Rulers, Proportional Grid, 3D Reference Axes, Full, Half, Third, Quarter, Custom...,
  Fit, Fit up to 100%, Colorize;
- the solid dialog's "Affect all layers that use this solid" checkbox and its disabled hint
  "Only this layer uses this solid.".

`tog` and `popup` now look the label up themselves, so every current and future call site is
covered by one change each; the automation ids and labels stay English, as the rest of the panels
keep them for scripts and tests. `resolution_label` composes its words from the catalog, so the
closed resolution dropdown reads Chinese too, and the viewer's item now reuses the panel's existing
"Title/action safe" row instead of a second spelling of the same concept.

21 rows join the Simplified catalog. The same keys join the Traditional one with the English source
as their value: the tests require the two lists to match, and the Traditional wording is the
translator's call rather than this change's.

Tests: the row-completeness check now also accepts a row that a helper draws (its English source
appears as a literal there), and a new check refuses a bare `Checkbox::new` / `RadioButton::new`
label - the shape that hid the solid dialog, and the one the literal scan cannot see because the
label is the call's second argument.

This also corrects two Simplified rows #398 left wrong (`Channel` read 色版 in a Simplified
catalog, `Motion Blur: {}` read 动态模糊 while the menu catalog's own Motion Blur is 运动模糊).
This commit is contained in:
WaWu
2026-10-10 13:33:35 +08:00
parent 813c7c4650
commit 1ab6eb963f
3 changed files with 89 additions and 12 deletions
+77 -4
View File
@@ -129,7 +129,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Casts Shadows", "投射阴影"),
("Change Renderer…", "更改渲染器…"),
("Changing a shortcut creates a custom preset; the default stays as it is.", "修改快捷键会创建自定义预设;默认预设保持不变。"),
("Channel", "色版"),
("Channel", "通道"),
("Channel:", "通道:"),
("Chapter:", "章节:"),
("Choose...", "选择..."),
@@ -285,7 +285,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("More ArtCraft apps", "更多 ArtCraft 应用"),
("More tutorials", "更多教程"),
("Motion Blur", "运动模糊"),
("Motion Blur: {}", "动态模糊:{}"),
("Motion Blur: {}", "运动模糊:{}"),
("Motion Sketch: drag to record", "动态草图:拖动以记录"),
("Move Down", "下移"),
("Move Up", "上移"),
@@ -490,6 +490,30 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl 拖动定位点可记录其运动"),
("▣ {}", "▣ {}"),
("⚠ {} is used by:", "⚠ {} 已被以下项目使用:"),
// Strings a panel used to hand to a helper as a bare literal: the helper draws them (the
// viewer's toolbar tooltips and popup items, the solid dialog's checkbox), so the lookup has
// to reach the catalog from inside the helper. Kept at the end so the catalogs stay parallel.
("3D Reference Axes", "3D 参考轴"),
("Affect all layers that use this solid", "影响使用该纯色的所有图层"),
("Choose grid and guide options", "选择网格和参考线选项"),
("Colorize", "着色"),
("Custom", "自定义"),
("Custom...", "自定义…"),
("Fit", "适合"),
("Fit up to 100%", "适合(最大 100%)"),
("Full", "完整"),
("Half", "一半"),
("Only this layer uses this solid.", "仅此图层使用该纯色。"),
("Proportional Grid", "比例网格"),
("Quarter", "四分之一"),
("Region of Interest", "目标区域"),
("Reset Exposure", "重置曝光"),
("Show Channel and Color Management Settings", "显示通道和色彩管理设置"),
("Show Snapshot (hold)", "显示快照(按住)"),
("Take Snapshot", "拍摄快照"),
("Third", "三分之一"),
("Toggle Mask and Shape Path Visibility", "切换蒙版与形状路径可见性"),
("Toggle Transparency Grid", "切换透明网格"),
];
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
@@ -907,6 +931,29 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl 拖曳釘選點可記錄其運動"),
("▣ {}", "▣ {}"),
("⚠ {} is used by:", "⚠ {} 已被以下專案使用:"),
// The same rows as the Simplified catalog (the tests require both lists to match). The
// Traditional wording is the translator's call and is left as the English source for now.
("3D Reference Axes", "3D Reference Axes"),
("Affect all layers that use this solid", "Affect all layers that use this solid"),
("Choose grid and guide options", "Choose grid and guide options"),
("Colorize", "Colorize"),
("Custom", "Custom"),
("Custom...", "Custom..."),
("Fit", "Fit"),
("Fit up to 100%", "Fit up to 100%"),
("Full", "Full"),
("Half", "Half"),
("Only this layer uses this solid.", "Only this layer uses this solid."),
("Proportional Grid", "Proportional Grid"),
("Quarter", "Quarter"),
("Region of Interest", "Region of Interest"),
("Reset Exposure", "Reset Exposure"),
("Show Channel and Color Management Settings", "Show Channel and Color Management Settings"),
("Show Snapshot (hold)", "Show Snapshot (hold)"),
("Take Snapshot", "Take Snapshot"),
("Third", "Third"),
("Toggle Mask and Shape Path Visibility", "Toggle Mask and Shape Path Visibility"),
("Toggle Transparency Grid", "Toggle Transparency Grid"),
];
#[cfg(test)]
@@ -976,6 +1023,7 @@ mod tests {
".heading(\"",
".monospace(\"",
".on_hover_text(\"",
".on_disabled_hover_text(\"",
".hint_text(\"",
".text(\"",
".prefix(\"",
@@ -1039,19 +1087,44 @@ mod tests {
}
/// Every row is asked for by a converted panel: a leftover row means the panel went back to its
/// English literal.
/// English literal. A string a helper draws for the panel (a toolbar tooltip, a popup item, a
/// checkbox label) is looked up inside that helper, so its English source only appears as a
/// literal there - `helper_labels_are_translated` keeps those helpers honest.
#[test]
fn every_row_is_used_by_a_converted_panel() {
for (en, _) in catalogs() {
let plain = format!("tr({en:?})");
let formatted = format!("tr_args({en:?}");
let bare = format!("{en:?}");
assert!(
CONVERTED.iter().any(|(_, src)| src.contains(&plain) || src.contains(&formatted)),
CONVERTED.iter().any(|(_, src)| src.contains(&plain) || src.contains(&formatted) || src.contains(&bare)),
"{en:?} is in the catalog but no converted panel asks for it"
);
}
}
/// A label that a helper takes as an argument is drawn by the helper, so the helper has to ask
/// the catalog: `Checkbox::new` and `RadioButton::new` are not among the scanned call shapes
/// (the label is their second argument), which let the solid dialog's checkbox ship in English.
#[test]
fn helper_labels_are_translated() {
for (path, source) in CONVERTED {
for marker in ["Checkbox::new(", "RadioButton::new("] {
let mut from = 0;
while let Some(at) = source[from..].find(marker) {
let at = from + at;
let rest = &source[at + marker.len()..];
let line = &rest[..rest.find('\n').unwrap_or(rest.len())];
if let Some(q) = line.find('"') {
let before = line[..q].trim_end();
assert!(before.ends_with("tr("), "{path}: the `{marker}` label `{}` is not in the catalog", &line[q..]);
}
from = at + marker.len();
}
}
}
}
/// A newly hard-coded interface string fails here instead of shipping in English to every
/// language. It is a heuristic - it looks at the call shapes the panels use - and it ignores
/// one-character literals, which are punctuation or a key name rather than a sentence.
+2 -2
View File
@@ -418,8 +418,8 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if editing.is_some() {
ui.add_space(8.0);
let r = ui
.add_enabled(shared, egui::Checkbox::new(&mut affect_all, "Affect all layers that use this solid"))
.on_disabled_hover_text("Only this layer uses this solid.");
.add_enabled(shared, egui::Checkbox::new(&mut affect_all, tr("Affect all layers that use this solid")))
.on_disabled_hover_text(tr("Only this layer uses this solid."));
auto.add("dialog.solid.affectAll", r.rect, "Affect all layers that use this solid");
if shared && !affect_all {
ui.label(egui::RichText::new(tr("This change will create a new solid for this layer.")).color(t.text_dim).size(11.0));
+10 -6
View File
@@ -486,7 +486,9 @@ fn popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: &str, anchor: Rect, it
ui.separator();
continue;
}
let r = ui.selectable_label(*on, label.as_str());
// The item is the English source: the drawn text is looked up in the catalog, while
// the automation label stays English for scripts and tests.
let r = ui.selectable_label(*on, crate::i18n::tr(label));
app.auto.add(&format!("viewer.{auto}.{i}"), r.rect, label);
if r.clicked() {
chosen = Some(i);
@@ -520,10 +522,10 @@ pub(crate) fn resolution_label(res: Resolution, scale: f64) -> String {
} else {
"Quarter"
};
format!("({r})")
format!("({})", crate::i18n::tr(r))
}
Resolution::Custom(n) => format!("Custom ({n})"),
r => r.label().to_string(),
Resolution::Custom(n) => format!("{} ({n})", crate::i18n::tr("Custom")),
r => crate::i18n::tr(r.label()).to_string(),
}
}
@@ -602,7 +604,9 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
let tog = |ui: &mut egui::Ui, auto: &mut crate::automation::Registry, x: &mut f32, icon: Icon, on: bool, id: &str, tip: &str| -> (bool, Rect) {
let r = Rect::from_min_size(pos2(*x, cy - 11.0), vec2(22.0, 22.0));
let resp = widgets::icon_button(ui, r, icon, on, &t, egui::Id::new(("vw-btn", id))).on_hover_text(tip);
// The tip is the English source: the tooltip is looked up in the catalog, while the
// automation label stays English for scripts and tests.
let resp = widgets::icon_button(ui, r, icon, on, &t, egui::Id::new(("vw-btn", id))).on_hover_text(crate::i18n::tr(tip));
auto.add(&format!("viewer.{id}"), r, tip);
*x += 24.0;
(resp.clicked(), r)
@@ -632,7 +636,7 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
}
let v = &app.ui.viewer;
let items = vec![
("Title/Action Safe".to_string(), v.safe_margins),
("Title/action safe".to_string(), v.safe_margins),
("Proportional Grid".to_string(), v.proportional_grid),
("Grid".to_string(), v.grid),
("Guides".to_string(), v.guides),