mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 03:28:22 -06:00
i18n: the forms, the settings pages and the viewer menus draw their technical terms in Chinese
The second pass over the interface below the menu bar: the strings that are technical terms or proper nouns - `Mercury GPU Acceleration (…)`, `8/16/32 bits per channel`, `OCIO (built-in ACES config)`, `sRGB IEC61966-2.1 (default)`, `Import Photoshop File`, `Current Settings`, the viewer's channel and Fast Previews items. Each wording was checked against the installed After Effects 26.5.0 Simplified Chinese, the way the menu bar was in #353. Terms Adobe keeps in English (`Display P3`, `HDTV (Rec. 709)`, `RGB`, `Alpha`, `H.264`, `ProRes`) need no row: a missing row falls back to the English source, which is the decision. The text reaches the catalog through the funnels that draw it, so one change each covers every call site: - `forms.rs`: field labels, notes, choice items and info bodies, plus `dialogs::modal`'s title - the modal id stays keyed on the untranslated source, so changing the language cannot move it; - `settings.rs`: page titles, sections, notes, buttons, setting labels and choice items (the page table itself is engine data and cannot reach the catalog); - `comp_settings.rs` (3D renderer), `key_dialogs.rs::combo` (Current Settings and the roving items), `properties.rs` + `timeline.rs` (the text animator menu), `dialogs.rs` (About links); - `render_queue.rs`: the Opus bitrate goes through `tr_args`, so the value is substituted after the lookup - worth calling out, because building the string with `format!` and translating the result looks up the assembled text and silently stays English (the same trap as the project settings' GPU label, which this commit also fixes). 30 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. Tests: `CONVERTED` gains the three engine files whose strings the interface draws (`prefs.rs`, `viewer.rs`, `commands/viewer_cmds.rs`); each holds no drawn literals, so the literal scan stays green and the new rows have a source to be "used" from. `cargo test -p effectcraft-ui-egui --lib i18n`: 16 passed.
This commit is contained in:
@@ -514,6 +514,40 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Third", "三分之一"),
|
||||
("Toggle Mask and Shape Path Visibility", "切换蒙版与形状路径可见性"),
|
||||
("Toggle Transparency Grid", "切换透明网格"),
|
||||
// Technical terms and proper nouns: the project settings, the import and export forms, the
|
||||
// viewer's channel and Fast Previews popups, the keyframe dialogs and the About window. The
|
||||
// terms that are the same in Chinese (Display P3, HDTV (Rec. 709), RGB, Alpha, H.264, ProRes…)
|
||||
// need no row: a missing row falls back to the English source, which is the decision.
|
||||
("16 bits per channel", "每通道 16 位"),
|
||||
("32 bits per channel (float)", "每通道 32 位(浮点)"),
|
||||
("8 bits per channel", "每通道 8 位"),
|
||||
("Adaptive Resolution", "自适应分辨率"),
|
||||
("Adobe-style built-in", "Adobe 风格内置"),
|
||||
("Blue", "蓝色"),
|
||||
("Current Settings", "当前设置"),
|
||||
("Disable Per-character 3D", "停用逐字 3D 化"),
|
||||
("Draft", "草图"),
|
||||
("Enable Per-character 3D", "启用逐字 3D 化"),
|
||||
("Fast Draft", "快速草图"),
|
||||
("Fast Previews (Draft 3D, Faster Effects)", "快速预览(3D 草图、更快效果)"),
|
||||
("Green", "绿色"),
|
||||
("Import Image Sequence", "导入图像序列"),
|
||||
("Import PDF / Illustrator File", "导入 PDF / Illustrator 文件"),
|
||||
("Import Photoshop File", "导入 Photoshop 文件"),
|
||||
("Interpret Unlabeled Alpha As", "将未标记的 Alpha 解释为"),
|
||||
("Join the ArtCraft Discord", "加入 ArtCraft Discord"),
|
||||
("Mercury GPU Acceleration ({})", "Mercury GPU 加速({})"),
|
||||
("Mercury Software Only", "仅 Mercury 软件"),
|
||||
("OCIO (built-in ACES config)", "OCIO(内置 ACES 配置)"),
|
||||
("Off (Final Quality)", "关闭(最终质量)"),
|
||||
("OpenGL (compatibility)", "OpenGL(兼容)"),
|
||||
("Opus Bitrate: {} kbps", "Opus 比特率:{} kbps"),
|
||||
("RGB Straight", "RGB 直接"),
|
||||
("Red", "红色"),
|
||||
("Tone map", "色调映射"),
|
||||
("Use In-Window Menu Bar on macOS", "在 macOS 上使用窗口内菜单栏"),
|
||||
("Wireframe", "线框"),
|
||||
("sRGB IEC61966-2.1 (default)", "sRGB IEC61966-2.1(默认)"),
|
||||
];
|
||||
|
||||
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
@@ -954,6 +988,38 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Third", "Third"),
|
||||
("Toggle Mask and Shape Path Visibility", "Toggle Mask and Shape Path Visibility"),
|
||||
("Toggle Transparency Grid", "Toggle Transparency Grid"),
|
||||
// The same rows as the Simplified catalog (the tests require both lists to match). The
|
||||
// Traditional wording is the translator's call and stays the English source until then.
|
||||
("16 bits per channel", "16 bits per channel"),
|
||||
("32 bits per channel (float)", "32 bits per channel (float)"),
|
||||
("8 bits per channel", "8 bits per channel"),
|
||||
("Adaptive Resolution", "Adaptive Resolution"),
|
||||
("Adobe-style built-in", "Adobe-style built-in"),
|
||||
("Blue", "Blue"),
|
||||
("Current Settings", "Current Settings"),
|
||||
("Disable Per-character 3D", "Disable Per-character 3D"),
|
||||
("Draft", "Draft"),
|
||||
("Enable Per-character 3D", "Enable Per-character 3D"),
|
||||
("Fast Draft", "Fast Draft"),
|
||||
("Fast Previews (Draft 3D, Faster Effects)", "Fast Previews (Draft 3D, Faster Effects)"),
|
||||
("Green", "Green"),
|
||||
("Import Image Sequence", "Import Image Sequence"),
|
||||
("Import PDF / Illustrator File", "Import PDF / Illustrator File"),
|
||||
("Import Photoshop File", "Import Photoshop File"),
|
||||
("Interpret Unlabeled Alpha As", "Interpret Unlabeled Alpha As"),
|
||||
("Join the ArtCraft Discord", "Join the ArtCraft Discord"),
|
||||
("Mercury GPU Acceleration ({})", "Mercury GPU Acceleration ({})"),
|
||||
("Mercury Software Only", "Mercury Software Only"),
|
||||
("OCIO (built-in ACES config)", "OCIO (built-in ACES config)"),
|
||||
("Off (Final Quality)", "Off (Final Quality)"),
|
||||
("OpenGL (compatibility)", "OpenGL (compatibility)"),
|
||||
("Opus Bitrate: {} kbps", "Opus Bitrate: {} kbps"),
|
||||
("RGB Straight", "RGB Straight"),
|
||||
("Red", "Red"),
|
||||
("Tone map", "Tone map"),
|
||||
("Use In-Window Menu Bar on macOS", "Use In-Window Menu Bar on macOS"),
|
||||
("Wireframe", "Wireframe"),
|
||||
("sRGB IEC61966-2.1 (default)", "sRGB IEC61966-2.1 (default)"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1012,6 +1078,12 @@ mod tests {
|
||||
("panels/viewer.rs", include_str!("../panels/viewer.rs")),
|
||||
("panels/viewer_tools.rs", include_str!("../panels/viewer_tools.rs")),
|
||||
("panels/viewers.rs", include_str!("../panels/viewers.rs")),
|
||||
// Text the interface draws but does not own: the settings pages and the viewer's channel
|
||||
// and Fast Previews lists come from the engine as data, so their source strings live here.
|
||||
// Both files pass the literal scan below unchanged (they hold no drawn literals).
|
||||
("engine settings pages", include_str!("../../../engine/src/prefs.rs")),
|
||||
("engine viewer channels", include_str!("../../../engine/src/viewer.rs")),
|
||||
("engine viewer commands", include_str!("../../../engine/src/commands/viewer_cmds.rs")),
|
||||
];
|
||||
|
||||
/// Call shapes that hand a string literal straight to the user.
|
||||
|
||||
@@ -340,11 +340,13 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
|
||||
Tab::Renderer => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tr("Renderer:"));
|
||||
let r =
|
||||
egui::ComboBox::from_id_salt("comp-renderer").selected_text(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" }).show_ui(ui, |ui| {
|
||||
let r = egui::ComboBox::from_id_salt("comp-renderer").selected_text(tr(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" })).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
ui.selectable_value(&mut d.advanced_3d, false, tr("Classic 3D"));
|
||||
ui.selectable_value(&mut d.advanced_3d, true, tr("Advanced 3D"));
|
||||
});
|
||||
},
|
||||
);
|
||||
auto.add("dialog.comp.renderer", r.response.rect, "Renderer");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,7 +158,8 @@ pub(crate) fn modal(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Toke
|
||||
.show(ctx, |ui| {
|
||||
ui.set_width(size.x - 36.0);
|
||||
ui.set_min_height(size.y - 36.0);
|
||||
ui.label(egui::RichText::new(title).font(Tokens::semibold(15.0)).color(t.tab_text_active));
|
||||
// The id stays keyed on the untranslated source, so switching language cannot move it.
|
||||
ui.label(egui::RichText::new(tr(title)).font(Tokens::semibold(15.0)).color(t.tab_text_active));
|
||||
ui.add_space(10.0);
|
||||
egui::ScrollArea::both().max_height((available.y - 84.0).max(40.0)).auto_shrink([false, true]).show(ui, body);
|
||||
});
|
||||
@@ -291,7 +292,7 @@ fn about_main(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, cmd: &mut
|
||||
);
|
||||
let foreground = if discord { Color32::WHITE } else { t.text };
|
||||
icons::paint(p, Rect::from_center_size(pos2(r.min.x + 18.0, r.center().y), vec2(15.0, 15.0)), icon, foreground);
|
||||
p.text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, label, Tokens::medium(12.5), foreground);
|
||||
p.text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, tr(label), Tokens::medium(12.5), foreground);
|
||||
p.text(
|
||||
pos2(r.max.x - 12.0, r.center().y),
|
||||
Align2::RIGHT_CENTER,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! Keyboard Shortcuts: `shortcut_editor`). Every form runs an engine command with the collected parameters, so whatever a
|
||||
//! dialog does an agent can do with one `engine.execute`.
|
||||
|
||||
use crate::i18n::tr;
|
||||
use crate::i18n::{tr, tr_args};
|
||||
use egui::{Color32, vec2};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
@@ -487,9 +487,11 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
|
||||
}
|
||||
"file.projectSettings" if p.as_object().is_none_or(|m| m.is_empty()) => {
|
||||
let st = &s.project.settings;
|
||||
// The catalogue row is `Mercury GPU Acceleration ({})`, so the value goes through
|
||||
// tr_args: wrapping the assembled string in tr() would look up the backend's name.
|
||||
let gpu_label = match &s.accel {
|
||||
Some(a) => format!("Mercury GPU Acceleration ({})", a.name()),
|
||||
None => "Mercury GPU Acceleration (no GPU: software)".to_string(),
|
||||
Some(a) => tr_args("Mercury GPU Acceleration ({})", &[&a.name()]),
|
||||
None => tr("Mercury GPU Acceleration (no GPU: software)").to_string(),
|
||||
};
|
||||
let depth = match st.bit_depth.label() {
|
||||
l if l.starts_with("16") => 1,
|
||||
@@ -765,7 +767,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
super::dialogs::modal(ctx, &f.title.clone(), vec2(480.0, h), t, |ui| {
|
||||
egui::Grid::new("form-grid").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| {
|
||||
for (i, fl) in f.fields.iter_mut().enumerate() {
|
||||
ui.label(&fl.label);
|
||||
ui.label(tr(&fl.label));
|
||||
let r = match &mut fl.kind {
|
||||
FieldKind::Number { value, speed } => {
|
||||
let dec = if *speed < 0.01 { 5 } else { 3 };
|
||||
@@ -801,7 +803,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
FieldKind::Note(text) => {
|
||||
ui.scope(|ui| {
|
||||
ui.set_width(220.0);
|
||||
ui.add(egui::Label::new(egui::RichText::new(text.as_str()).small()).wrap())
|
||||
ui.add(egui::Label::new(egui::RichText::new(tr(text.as_str())).small()).wrap())
|
||||
})
|
||||
.inner
|
||||
.rect
|
||||
@@ -810,11 +812,11 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
FieldKind::Choice { options, sel } => {
|
||||
let cur = options.get(*sel).map(|o| o.0.clone()).unwrap_or_default();
|
||||
egui::ComboBox::from_id_salt(("form", fl.key.as_str()))
|
||||
.selected_text(cur)
|
||||
.selected_text(tr(&cur))
|
||||
.width(240.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, (l, _)) in options.iter().enumerate() {
|
||||
ui.selectable_value(sel, i, l);
|
||||
ui.selectable_value(sel, i, tr(l));
|
||||
}
|
||||
})
|
||||
.response
|
||||
@@ -883,7 +885,7 @@ pub fn show_info(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let (title, body) = app.dialog_state.info.clone();
|
||||
let mut close = false;
|
||||
super::dialogs::modal(ctx, &title, vec2(440.0, 160.0), t, |ui| {
|
||||
ui.label(body);
|
||||
ui.label(tr(&body));
|
||||
ui.add_space(16.0);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() {
|
||||
|
||||
@@ -190,9 +190,10 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
|
||||
fn combo(ui: &mut egui::Ui, app: &mut EffectcraftApp, id: &str, sel: &mut usize, opts: &[&str], enabled: bool) {
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
let r = egui::ComboBox::from_id_salt(id).width(200.0).selected_text(opts[*sel]).show_ui(ui, |ui| {
|
||||
// The options are the English source strings; the automation id and label stay English.
|
||||
let r = egui::ComboBox::from_id_salt(id).width(200.0).selected_text(tr(opts[*sel])).show_ui(ui, |ui| {
|
||||
for (i, o) in opts.iter().enumerate() {
|
||||
ui.selectable_value(sel, i, *o);
|
||||
ui.selectable_value(sel, i, tr(o));
|
||||
}
|
||||
});
|
||||
app.auto.add(id, r.response.rect, id);
|
||||
|
||||
@@ -15,6 +15,7 @@ use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::dock::PanelKind;
|
||||
use crate::i18n::tr;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{EffectcraftApp, widgets};
|
||||
@@ -120,7 +121,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let per_char = layer.props.prop("text/perChar3d").is_some_and(|q| q.value.as_bool());
|
||||
let entries = animator_entries(per_char);
|
||||
let labels: Vec<String> = entries.iter().map(|(l, _)| l.clone()).collect();
|
||||
// The entries are the English source strings; the menu draws them translated.
|
||||
let labels: Vec<String> = entries.iter().map(|(l, _)| tr(l).to_string()).collect();
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, br.left_bottom(), &labels, None) {
|
||||
match entries[i].1 {
|
||||
"" => {}
|
||||
|
||||
@@ -183,7 +183,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren
|
||||
}
|
||||
v.push(("-".into(), Value::Null));
|
||||
for kbps in [24u32, 32, 64, 96, 128, 192, 256] {
|
||||
v.push((mark(o.opus_bitrate_kbps == kbps, &format!("Opus Bitrate: {kbps} kbps")), json!({"audioBitrate": kbps})));
|
||||
v.push((mark(o.opus_bitrate_kbps == kbps, &tr_args("Opus Bitrate: {} kbps", &[&kbps])), json!({"audioBitrate": kbps})));
|
||||
}
|
||||
for (a, k) in [(OpusApplication::Audio, "audio"), (OpusApplication::Voip, "voice")] {
|
||||
v.push((mark(o.opus_application == a, &tr_args("Opus Tuning: {}", &[&(a.label())])), json!({"opusApplication": k})));
|
||||
|
||||
@@ -50,14 +50,14 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
match *item {
|
||||
Item::Section(title) => {
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(title).font(Tokens::semibold(12.5)).color(t.tab_text_active));
|
||||
ui.label(RichText::new(tr(title)).font(Tokens::semibold(12.5)).color(t.tab_text_active));
|
||||
ui.separator();
|
||||
}
|
||||
Item::Note(text) => {
|
||||
ui.label(RichText::new(text).color(t.text_dim));
|
||||
ui.label(RichText::new(tr(text)).color(t.text_dim));
|
||||
}
|
||||
Item::Button { label, command, params } => {
|
||||
let r = ui.button(label);
|
||||
let r = ui.button(tr(label));
|
||||
reg(app, &format!("settings.button.{}", label.trim_end_matches('.')), &r, label);
|
||||
if r.clicked() {
|
||||
acts.push(Act::Run(command.into(), serde_json::from_str(params).unwrap_or(json!({}))));
|
||||
@@ -96,7 +96,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
match kind {
|
||||
Kind::Bool => {
|
||||
let mut b = v.as_bool().unwrap_or(false);
|
||||
let r = ui.checkbox(&mut b, label);
|
||||
let r = ui.checkbox(&mut b, tr(label));
|
||||
if r.changed() {
|
||||
acts.push(Act::Set(key.into(), json!(b)));
|
||||
}
|
||||
@@ -104,7 +104,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
}
|
||||
_ => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(vec2(label_w, 18.0), egui::Label::new(format!("{label}:")).truncate());
|
||||
ui.add_sized(vec2(label_w, 18.0), egui::Label::new(format!("{}:", tr(label))).truncate());
|
||||
let r = match kind {
|
||||
Kind::Int(lo, hi, unit) => {
|
||||
let mut x = v.as_i64().unwrap_or(0);
|
||||
@@ -146,11 +146,11 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
||||
let curv = v.as_str().map_or_else(|| v.to_string(), str::to_string);
|
||||
let shown = opts.iter().find(|o| o.1 == curv).map(|o| o.0).unwrap_or(curv.as_str()).to_string();
|
||||
egui::ComboBox::from_id_salt(("settings", key))
|
||||
.selected_text(shown)
|
||||
.selected_text(tr(&shown))
|
||||
.width(220.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (l, val) in opts {
|
||||
if ui.selectable_label(*val == curv, *l).clicked() {
|
||||
if ui.selectable_label(*val == curv, tr(l)).clicked() {
|
||||
acts.push(Act::Set(key.into(), json!(val)));
|
||||
}
|
||||
}
|
||||
@@ -449,7 +449,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.painter().text(
|
||||
r.left_center() + vec2(10.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
p.title,
|
||||
tr(p.title),
|
||||
Tokens::ui(12.5),
|
||||
if sel { Color32::WHITE } else { t.text },
|
||||
);
|
||||
@@ -466,7 +466,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
// dialog scrolled sideways a little (#284).
|
||||
ui.set_width(ui.available_width());
|
||||
let p = &all[idx];
|
||||
ui.label(RichText::new(p.title).font(Tokens::semibold(14.0)).color(t.tab_text_active));
|
||||
ui.label(RichText::new(tr(p.title)).font(Tokens::semibold(14.0)).color(t.tab_text_active));
|
||||
ui.add_space(6.0);
|
||||
egui::ScrollArea::vertical().max_height(body_h - 30.0).auto_shrink([false, false]).show(ui, |ui| {
|
||||
page_ui(app, ui, p, &cur, t, &mut acts);
|
||||
|
||||
@@ -14,6 +14,7 @@ use effectcraft_engine::time::Tick;
|
||||
use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::i18n::tr;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{EffectcraftApp, widgets};
|
||||
@@ -3280,7 +3281,7 @@ fn text_anim_popups(
|
||||
let mut opts: Vec<String> = Vec::new();
|
||||
let mut acts: Vec<(String, serde_json::Value)> = Vec::new();
|
||||
if key == "animate" {
|
||||
opts.push(if per_char { "Disable Per-character 3D" } else { "Enable Per-character 3D" }.into());
|
||||
opts.push(tr(if per_char { "Disable Per-character 3D" } else { "Enable Per-character 3D" }).to_string());
|
||||
acts.push(("layer.enablePerChar3D".into(), json!({"layer": layer.id.0, "enabled": !per_char})));
|
||||
opts.push("-".into());
|
||||
acts.push((String::new(), json!(null)));
|
||||
|
||||
Reference in New Issue
Block a user