diff --git a/ROADMAP.md b/ROADMAP.md index 054e8bd5..70935025 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,6 @@ # Roadmap +- 2026-10-09: Settings ▸ General ▸ Language now translates the interface below the menu bar as well: panels, dialogs, buttons and tooltips read their text through `crates/ui-egui/src/i18n/ui.rs`, keyed by the English source string. Simplified and Traditional Chinese are complete (384 strings); Japanese rows can follow in the same file. A test fails when a converted panel gains a hard-coded string, and another keeps the catalog free of unused rows. - 2026-10-08: `effectcraft-cli render --out` resolves a relative path against the working directory, like `--project` (it used the project's folder, which doubled a path that already named it). Render Queue outputs set in a project are unchanged. - 2026-10-08: Long-press a grouped toolbar button to choose its tools, including Horizontal/Vertical Type; releasing the hold keeps the menu open. Existing point/paragraph text creation is unchanged. diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index 442edb8c..77357e87 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -3,6 +3,7 @@ //! build.rs from contributors/contributors.json (craftrules standards/contributors.md); nothing is //! read at run time. +use crate::i18n::{tr, tr_args}; use std::cmp::Ordering; use egui::RichText; @@ -243,7 +244,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) { let id = egui::Id::new("credits_view"); let mut v = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_default(); ui.horizontal_wrapped(|ui| { - ui.label("Show"); + ui.label(tr("Show")); for m in NameMode::ALL { let r = ui.selectable_label(v.names == m, m.label()); auto.add(&format!("about.credits.show.{}", m.id()), r.rect, m.label()); @@ -252,7 +253,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) { } } ui.separator(); - ui.label("Sort"); + ui.label(tr("Sort")); let combo = egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { for k in SortKey::ALL { let r = ui.selectable_label(v.key == k, k.label().0); @@ -264,29 +265,29 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) { } }); auto.add("about.credits.sort", combo.response.rect, "Sort"); - let r = ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text("Reverse the order"); + let r = ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(tr("Reverse the order")); auto.add("about.credits.reverse", r.rect, "Reverse the order"); if r.clicked() { v.ascending = !v.ascending; } ui.separator(); - let r = ui.selectable_label(!v.table, "Grab bag"); + let r = ui.selectable_label(!v.table, tr("Grab bag")); auto.add("about.credits.view.grabBag", r.rect, "Grab bag"); if r.clicked() { v.table = false; } - let r = ui.selectable_label(v.table, "Table"); + let r = ui.selectable_label(v.table, tr("Table")); auto.add("about.credits.view.table", r.rect, "Table"); if r.clicked() { v.table = true; } }); let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending); - ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak()); + ui.label(RichText::new(tr_args("{} contributors · {} commits", &[&(list.len()), &(group(TOTAL_COMMITS))])).small().weak()); ui.separator(); egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { if list.is_empty() { - ui.label("No contributor data was built into this copy."); + ui.label(tr("No contributor data was built into this copy.")); } else if v.table { table(ui, &list, &mut v, auto); } else { @@ -340,7 +341,7 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, auto: &mut Regi /// About ▸ Models: AI models credited in Co-Authored-By trailers. pub fn models_ui(ui: &mut egui::Ui) { if MODELS.is_empty() { - ui.label("No model credits were built into this copy."); + ui.label(tr("No model credits were built into this copy.")); return; } let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1); diff --git a/crates/ui-egui/src/header.rs b/crates/ui-egui/src/header.rs index ca9c3a17..0ea37835 100644 --- a/crates/ui-egui/src/header.rs +++ b/crates/ui-egui/src/header.rs @@ -1,6 +1,7 @@ //! The Tools bar: home, the tool slots, tool options, snapping, workspaces and the community //! buttons (Discord is always one click away). +use crate::i18n::tr; use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; use serde_json::json; @@ -27,14 +28,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { paint_logo(&p, brand); let bresp = ui.interact(brand, egui::Id::new("brand"), Sense::click()); app.auto.add("header.about", brand, "About EffectCraft"); - if bresp.on_hover_text("About EffectCraft").clicked() { + if bresp.on_hover_text(tr("About EffectCraft")).clicked() { app.dialog = Some(Dialog::About); } x += 32.0; // Home. let home = Rect::from_min_size(pos2(x, cy - 13.0), vec2(26.0, 26.0)); - if widgets::icon_button(ui, home, Icon::Home, app.ui.start_screen, &t, egui::Id::new("tool-home")).on_hover_text("Home").clicked() { + if widgets::icon_button(ui, home, Icon::Home, app.ui.start_screen, &t, egui::Id::new("tool-home")).on_hover_text(tr("Home")).clicked() { app.ui.start_screen = !app.ui.start_screen; } app.auto.add("header.home", home, "Home"); @@ -171,7 +172,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { app.ui.snapping = app.session.state.snapping; } app.auto.add("header.snapping", snap, "Snapping"); - let label = p.text(pos2(snap.max.x + 4.0, cy), Align2::LEFT_CENTER, "Snapping", Tokens::ui(12.0), t.text_dim); + let label = p.text(pos2(snap.max.x + 4.0, cy), Align2::LEFT_CENTER, tr("Snapping"), Tokens::ui(12.0), t.text_dim); snapping_options(app, ui, Rect::from_min_size(pos2(label.max.x + 2.0, cy - 9.0), vec2(18.0, 18.0))); // Right side: community buttons, workspaces. @@ -181,9 +182,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let dc = Color32::from_rgb(0x58, 0x65, 0xf2); p.rect_filled(discord, 13.0, if dresp.hovered() { dc.gamma_multiply(1.2) } else { dc }); icons::paint(&p, Rect::from_center_size(pos2(discord.min.x + 16.0, cy), vec2(14.0, 14.0)), Icon::Chat, Color32::WHITE); - p.text(pos2(discord.min.x + 28.0, cy), Align2::LEFT_CENTER, "Discord", Tokens::semibold(12.0), Color32::WHITE); + p.text(pos2(discord.min.x + 28.0, cy), Align2::LEFT_CENTER, tr("Discord"), Tokens::semibold(12.0), Color32::WHITE); app.auto.add("header.discord", discord, "Join the ArtCraft Discord"); - if dresp.on_hover_text("Join the ArtCraft community on Discord").clicked() { + if dresp.on_hover_text(tr("Join the ArtCraft community on Discord")).clicked() { let _ = app.session.execute("help.discord", json!({})); } rx = discord.min.x - 6.0; @@ -256,7 +257,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { fn snapping_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect) { let t = app.tokens; let pop = egui::Id::new("snap-options"); - if widgets::icon_button(ui, r, Icon::ChevronDown, false, &t, egui::Id::new("snap-options-button")).on_hover_text("Snapping options").clicked() { + if widgets::icon_button(ui, r, Icon::ChevronDown, false, &t, egui::Id::new("snap-options-button")).on_hover_text(tr("Snapping options")).clicked() { widgets::open_popup(ui, pop); } app.auto.add("header.snappingOptions", r, "Snapping options"); @@ -399,13 +400,13 @@ fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, key: &str, t } } p.text(row(33.0) + vec2(134.0, 0.0), Align2::LEFT_CENTER, tp.kind.label(), Tokens::ui(11.5), t.text_dim); - p.text(row(66.0), Align2::LEFT_CENTER, "Blend Mode:", Tokens::ui(12.0), t.text_dim); + p.text(row(66.0), Align2::LEFT_CENTER, tr("Blend Mode:"), Tokens::ui(12.0), t.text_dim); blend_rect = Rect::from_min_size(row(56.0) + vec2(84.0, 0.0), vec2(150.0, 20.0)); if widgets::dropdown(ui, blend_rect, tp.blend.label(), &t, id.with("blend-dd")).clicked() { widgets::open_popup(ui, blend_pop); } app.auto.add(&format!("header.{key}Options.blend"), blend_rect, "Blend Mode"); - p.text(row(98.0), Align2::LEFT_CENTER, "Opacity:", Tokens::ui(12.0), t.text_dim); + p.text(row(98.0), Align2::LEFT_CENTER, tr("Opacity:"), Tokens::ui(12.0), t.text_dim); let (or, v, _) = widgets::hot_number_at(ui, row(89.0) + vec2(84.0, 0.0), id.with("opacity"), tp.opacity, 0.5, (0.0, 100.0), 0, "%", &t); app.auto.add(&format!("header.{key}Options.opacity"), or, "Opacity"); if let Some(v) = v { @@ -475,14 +476,14 @@ fn paint_swatch(p: &egui::Painter, r: Rect, paint: &ToolPaint, hovered: bool, t: fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, mut x: f32, cy: f32) -> f32 { let t = app.tokens; let o = app.session.state.puppet.clone(); - p.text(pos2(x, cy), Align2::LEFT_CENTER, "Mesh:", Tokens::ui(12.0), t.text_dim); + p.text(pos2(x, cy), Align2::LEFT_CENTER, tr("Mesh:"), Tokens::ui(12.0), t.text_dim); x += 40.0; let show = Rect::from_min_size(pos2(x, cy - 10.0), vec2(20.0, 20.0)); if widgets::checkbox(ui, show, o.show_mesh, &t, egui::Id::new("puppet-show-mesh")).clicked() { let _ = app.session.execute("puppet.mesh", json!({"showMesh": !o.show_mesh})); } app.auto.add("header.puppet.showMesh", show, "Show mesh"); - p.text(pos2(show.max.x + 2.0, cy), Align2::LEFT_CENTER, "Show", Tokens::ui(12.0), t.text_dim); + p.text(pos2(show.max.x + 2.0, cy), Align2::LEFT_CENTER, tr("Show"), Tokens::ui(12.0), t.text_dim); x = show.max.x + 44.0; for (label, key, v, range) in [("Expansion:", "expansion", o.expansion, (-100.0, 200.0)), ("Density:", "density", o.density, (0.0, 100.0))] { p.text(pos2(x, cy), Align2::LEFT_CENTER, label, Tokens::ui(12.0), t.text_dim); @@ -498,7 +499,7 @@ fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter let label = "Record Options..."; let w = 104.0; let r = Rect::from_min_size(pos2(x, cy - 10.0), vec2(w, 20.0)); - let resp = ui.interact(r, egui::Id::new("puppet-record-options"), egui::Sense::click()).on_hover_text("⌘/Ctrl-drag a pin to record its motion"); + let resp = ui.interact(r, egui::Id::new("puppet-record-options"), egui::Sense::click()).on_hover_text(tr("⌘/Ctrl-drag a pin to record its motion")); p.rect_stroke(r, 3.0, egui::Stroke::new(1.0, if resp.hovered() { t.accent } else { t.field_border }), egui::StrokeKind::Inside); p.text(r.center(), Align2::CENTER_CENTER, label, Tokens::ui(11.5), t.text); app.auto.add("header.puppet.recordOptions", r, label); @@ -513,7 +514,7 @@ fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter let r = Rect::from_min_size(pos2(r.max.x + 8.0, cy - 10.0), vec2(w, 20.0)); let resp = ui .interact(r, egui::Id::new("puppet-follow"), egui::Sense::click()) - .on_hover_text("Select the leader pin, then Shift-click the pins that should trail it (hair, cloth, tails)"); + .on_hover_text(tr("Select the leader pin, then Shift-click the pins that should trail it (hair, cloth, tails)")); p.rect_stroke(r, 3.0, egui::Stroke::new(1.0, if resp.hovered() { t.accent } else { t.field_border }), egui::StrokeKind::Inside); p.text(r.center(), Align2::CENTER_CENTER, label, Tokens::ui(11.5), t.text); app.auto.add("header.puppet.follow", r, label); @@ -534,7 +535,7 @@ fn ws_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui) { } } ui.separator(); - if ui.button("Reset to Saved Layout").clicked() { + if ui.button(tr("Reset to Saved Layout")).clicked() { let n = app.ui.workspace.clone(); app.set_workspace(&n); ui.close(); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 47627374..101c2614 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4,7 +4,11 @@ //! plug-in names remain verbatim; these catalogs only translate the fixed menu tree. use crate::EffectcraftApp; + +/// Translations for the panels, tooltips and dialogs below the menu bar (see the module docs). +mod ui; use effectcraft_engine::menus::MenuEntry; +pub(crate) use ui::{set_current, tr, tr_args}; /// Every language with a catalog, in the order Settings ▸ General ▸ Language lists them. const CATALOG_CODES: [&str; 4] = ["ja", "zh-hans", "zh-hant", "uk"]; diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs new file mode 100644 index 00000000..8e173385 --- /dev/null +++ b/crates/ui-egui/src/i18n/ui.rs @@ -0,0 +1,1067 @@ +//! Translations for the interface *below* the menu bar: panel labels, buttons, field names, +//! tooltips and the sentences the panels draw themselves. +//! +//! The menu bar has its own catalog in [`super`], keyed by `(command, English label)`. Text with no +//! menu command is keyed here by its English source string, so a panel can wrap a literal without +//! inventing an id, and a missing row falls back to English instead of to an empty label. +//! +//! The language in force is set once per frame, from the same resolved language the fonts are +//! installed for ([`super::language`], [`set_current`]), so a panel does not have to thread it +//! through every function. It is thread-local: the render threads never draw interface text. +//! +//! A row that carries values keeps one `{}` per value and is read with [`tr_args`]; the tests below +//! refuse a row whose placeholder count differs from its English source, so a translation can never +//! drop a value, and `tr_args` itself cannot panic. + +use std::cell::Cell; +use std::fmt::Display; + +thread_local! { + /// The resolved interface language ("en", "ja", "zh-hans", "zh-hant"); "en" until the first + /// frame sets it. + static CURRENT: Cell<&'static str> = const { Cell::new("en") }; +} + +/// Remember the language the panels should draw in: called from the frame loop with the language +/// the fonts were installed for. +pub(crate) fn set_current(language: &'static str) { + CURRENT.with(|c| c.set(language)); +} + +/// The language [`tr`] and [`tr_args`] are using. +pub(crate) fn current() -> &'static str { + CURRENT.with(Cell::get) +} + +/// The catalog for `language`: empty for English and for any language that has no rows yet. +fn catalog(language: &str) -> &'static [(&'static str, &'static str)] { + match language { + "zh-hans" => SIMPLIFIED_CHINESE, + "zh-hant" => TRADITIONAL_CHINESE, + _ => &[], + } +} + +/// Translate one interface string: the current language's row for `text`, or `text` itself. +pub(crate) fn tr(text: &str) -> &str { + catalog(current()).iter().find(|(en, _)| *en == text).map_or(text, |(_, translated)| *translated) +} + +/// Translate an interface string that carries values: each `{}` takes the next argument. A missing +/// argument leaves its placeholder out and extra ones are ignored, so this cannot panic; the tests +/// keep the placeholder count equal to the English source's. +pub(crate) fn tr_args(text: &str, args: &[&dyn Display]) -> String { + let template = tr(text); + let mut out = String::with_capacity(template.len() + 16); + let mut rest = template; + let mut n = 0; + while let Some(at) = rest.find("{}") { + out.push_str(&rest[..at]); + if let Some(arg) = args.get(n) { + out.push_str(&arg.to_string()); + } + n += 1; + rest = &rest[at + 2..]; + } + out.push_str(rest); + out +} + +const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ + (" OK ", " 确定 "), + (" Delete ", " 删除 "), + (" Open Auto-Save ", " 打开自动保存 "), + ("(missing)", "(缺失)"), + ("360 View Orientation", "360 视图方向"), + ("3:2 Pulldown: {}", "3:2 折叠:{}"), + ( + "A clean-room, open-source compositor for motion graphics and visual effects: native on macOS, Windows and Linux, and in the browser. Part of the ArtCraft family of creative apps.", + "一款从零编写、开源的动态图形与视觉效果合成软件:原生支持 macOS、Windows 和 Linux,也可在浏览器中运行。属于 ArtCraft 创意应用家族。", + ), + ("About EffectCraft", "关于 EffectCraft"), + ("About...", "关于..."), + ("Active Camera", "活动摄像机"), + ("Adapt Feature On Every Frame", "每帧都适应特征"), + ("Adaptive Sample Limit:", "自适应采样限制:"), + ("Add Animator", "添加动画制作器"), + ("Add Comment", "添加注释"), + ("Add Formatting", "添加格式"), + ("Add Group", "添加组"), + ("Add Mirror", "添加镜像"), + ("Add Selected As", "将所选项目添加为"), + ("Add a comment", "添加注释"), + ("Add a shape, fill, stroke or path operation", "添加形状、填充、描边或路径操作"), + ("Add to Composition", "添加到合成"), + ("Add to Favorites", "添加到收藏夹"), + ("Add:", "添加:"), + ("Adjust Exposure (stops)", "调整曝光(光圈级数)"), + ("Adjust composition duration to the time span of the selected layers", "将合成时长调整为所选图层的时间范围"), + ("Advanced 3D", "高级 3D"), + ("Align Layers to:", "将图层对齐到:"), + ("All six face cameras turn together; the scene is not changed.", "六个面摄像机一起旋转;场景不会改变。"), + ("Anchor:", "定位点:"), + ("Aperture", "光圈"), + ("Apply Dimensions:", "应用尺寸:"), + ("Apply Motion To:", "将运动应用于:"), + ("Arrange", "排列"), + ("Assign", "指定"), + ("Auto", "自动"), + ("Auto-saves", "自动保存"), + ("Back", "背面"), + ("Background Color:", "背景颜色:"), + ("Blend Mode:", "混合模式:"), + ("Blending Mode", "混合模式"), + ("Blur", "模糊"), + ("Blur Level", "模糊级别"), + ("Browser Storage", "浏览器存储"), + ("Browse…", "浏览…"), + ("Brush Dynamics", "画笔动态"), + ("Caching frames…", "正在缓存帧…"), + ("Cancel", "取消"), + ("Casts Shadows", "投射阴影"), + ("Change Renderer…", "更改渲染器…"), + ("Changing a shortcut creates a custom preset; the default stays as it is.", "修改快捷键会创建自定义预设;默认预设保持不变。"), + ("Channel", "色版"), + ("Channel:", "通道:"), + ("Chapter:", "章节:"), + ("Choose...", "选择..."), + ("Classic 3D", "经典 3D"), + ("Clear", "清除"), + ("Clear list", "清空列表"), + ("Click a key to see its commands.", "单击某个按键以查看其命令。"), + ("Click adds a point (½ height = no change)", "单击可添加点(上下滑动高度的一半 = 无变化)"), + ("Click adds a point; drag one off to remove", "单击可添加点;拖出一个即可删除"), + ("Click to make this viewer active", "单击可激活此视图"), + ("Click to reveal the expression in the timeline", "单击可在时间轴中显示表达式"), + ("Click to show the active composition here", "单击可在此显示当前合成"), + ("Click, then click in the Composition panel", "单击后,在合成面板中单击"), + ("Clone Options", "克隆选项"), + ("Clone Source Overlay:", "克隆源叠加:"), + ("Close", "关闭"), + ("Close Panel", "关闭面板"), + ("Close tutorial", "关闭教程"), + ("Color", "颜色"), + ("Color Depth: {}", "颜色深度:{}"), + ("Color:", "颜色:"), + ("Colour map", "颜色图"), + ("Columns", "列"), + ("Comment:", "注释:"), + ("Community", "社区"), + ("Composition", "合成"), + ("Composition Mini-Flowchart", "合成微型流程图"), + ("Composition Name:", "合成名称:"), + ("Composition Settings ▸ 3D Renderer", "合成设置 ▸ 3D 渲染器"), + ("Composition Settings…", "合成设置…"), + ("Composition marker bin: drag a marker to the time ruler", "合成标记箱:将标记拖到时间标尺上"), + ("Cone Angle", "圆锥角"), + ("Cone Feather", "圆锥羽化"), + ("Constrain Proportions", "约束比例"), + ("Continuous (Lock Outgoing to Incoming)", "连续(将出点锁定到入点)"), + ("Cue Point", "提示点"), + ("Current Render", "当前渲染"), + ("Darker", "更深"), + ("Default Output:", "默认输出:"), + ("Default: {}", "默认:{}"), + ("Defaults", "默认值"), + ("Delete", "删除"), + ("Delete Selected Points", "删除所选的顶点"), + ("Delete selected project items", "删除所选的项目项"), + ("Delete this template", "删除此模板"), + ("Depth of Field", "景深"), + ("Discord", "Discord"), + ("Distribute Layers:", "分布图层:"), + ("Don't Recover", "不恢复"), + ("Don't Save", "不保存"), + ("Double-click a layer to open it in the Layer panel", "双击图层可在图层面板中打开"), + ("Double-click footage in the Project panel to open it here", "双击项目面板中的素材可在此处打开"), + ("Double-click to edit the comment", "双击可编辑注释"), + ("Download", "下载"), + ( + "Drag properties here from the timeline,\nor use Animation > Add Property to Essential Graphics.", + "请从时间轴将属性拖到此处,\n或使用 动画 > 将属性添加到基本图形。", + ), + ("Drag to change", "拖动可更改"), + ("Drag to create a guide", "拖动可创建参考线"), + ("Drag to slip the source", "拖动可滑移源"), + ("Drop Frame", "丢帧"), + ("Duplicate", "重复"), + ("Duration:", "持续时间:"), + ("Edit Gradient…", "编辑渐变…"), + ("Edit Target: {}", "编辑目标:{}"), + ("Edit in the Timeline", "在时间轴中编辑"), + ("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常退出。"), + ("Effects & Presets options", "效果和预设选项"), + ("Effects: {}", "效果:{}"), + ("Empty folder", "空文件夹"), + ("Enable Depth of Field", "启用景深"), + ("Enable Expression", "启用表达式"), + ("Enhance", "增强"), + ("Essential Properties — {}", "基本属性 — {}"), + ("Event", "事件"), + ("Export Template…", "导出模板…"), + ("Export...", "导出..."), + ("Expression Language Menu", "表达式语言菜单"), + ("Expression pick whip: drag onto a property", "表达式拾取器:拖到属性上"), + ("Expression:", "表达式:"), + ("Eyedropper: click, then click in the Composition panel", "吸管:单击后,在合成面板中单击"), + ("Falloff", "衰减"), + ("Falloff Distance", "衰减距离"), + ("Favorites", "收藏"), + ("Field Render: {}", "场渲染:{}"), + ("Figures", "数字"), + ("Fill Target", "填充目标"), + ("Finished", "已完成"), + ("Flowchart: {}", "流程图:{}"), + ("Focus Distance", "焦距"), + ("Follow Feature Rotation and Scale", "跟随特征的旋转和缩放"), + ("Footage: {}", "素材:{}"), + ("Format: {}", "格式:{}"), + ("Frame Aspect Ratio: {}", "帧长宽比:{}"), + ("Frame Blending: {}", "帧混合:{}"), + ("Frame Rate:", "帧速率:"), + ("Frame Render Time {}ms", "帧渲染时间 {} 毫秒"), + ("Frame Target:", "帧目标:"), + ("Grab bag", "列表"), + ("Grid", "网格"), + ("Guide Layers: {}", "参考图层:{}"), + ("Guides", "参考线"), + ("Height:", "高度:"), + ("Hide This", "隐藏此项"), + ("Histogram", "直方图"), + ("Hold In Place", "保持在原位"), + ("Home", "主页"), + ("If Confidence is Below", "如果置信度低于"), + ("Import", "导入"), + ("Import and Add to Composition", "导入并添加到合成"), + ("Import...", "导入..."), + ("Imported media", "已导入的媒体"), + ("Influence:", "影响:"), + ("Install from File…", "从文件安装…"), + ("Intensity", "强度"), + ("Inverted", "反转"), + ("Join the ArtCraft community on Discord", "加入 ArtCraft 的 Discord 社区"), + ("Key: {}", "按键:{}"), + ("LEARN", "学习"), + ("Label", "标签"), + ("Label Colors and Names:", "标签颜色和名称:"), + ("Label:", "标签:"), + ("Latest auto-save: {}", "最新的自动保存:{}"), + ("Layer", "图层"), + ("Layer controls", "图层控制"), + ("Layer {}", "图层 {}"), + ("Layer:", "图层:"), + ("Layer: {}", "图层:{}"), + ("Learn", "学习"), + ("Leave all attributes in \"{}\"", "将所有属性保留在“{}”中"), + ("Licence: {}", "许可:{}"), + ("Ligatures", "连字"), + ("Light Type", "灯光类型"), + ("Lighter", "更浅"), + ("Link Selected Property", "链接所选属性"), + ("Lock Aspect Ratio to {}", "锁定长宽比为 {}"), + ("Lock to Zoom", "锁定缩放"), + ("Locked: {} — {}", "已锁定:{} — {}"), + ("Log:", "日志:"), + ("Loop (Trace Path)", "循环(跟踪路径)"), + ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT 或 Apache-2.0 • 字体:Inter、JetBrains Mono(OFL)"), + ("Magnification ratio popup", "放大倍率弹出菜单"), + ("Make Comp Size", "设为合成长宽"), + ("Masks", "蒙版"), + ("Maximize Panel", "最大化面板"), + ("Media Replacement", "媒体替换"), + ("Mesh:", "网格:"), + ("Method Used: {} Average Error: {} pixels", "使用方法:{} 平均误差:{} 像素"), + ("Models folder: {}", "模型文件夹:{}"), + ("Modifiers:", "修饰键:"), + ("More", "更多"), + ("More ArtCraft apps", "更多 ArtCraft 应用"), + ("More tutorials", "更多教程"), + ("Motion Blur", "运动模糊"), + ("Motion Blur: {}", "动态模糊:{}"), + ("Motion Sketch: drag to record", "动态草图:拖动以记录"), + ("Move Down", "下移"), + ("Move Up", "上移"), + ("Move to Project Root", "移到项目根目录"), + ("Name", "名称"), + ("Name:", "名称:"), + ("Navigation", "导航"), + ("New", "新建"), + ("New Comp from Selection", "由所选项目新建合成"), + ("New Duration:", "新持续时间:"), + ("New composition name:", "新合成名称:"), + ("New from Render Queue item:", "由渲染队列项新建:"), + ("New from Template", "从模板新建"), + ("Next", "下一个"), + ("Nice work. Pick another tutorial on the Home screen's Learn tab.", "干得漂亮。请在主页面的“学习”选项卡中选择另一个教程。"), + ("No VR environments yet. Create one with Composition ▸ VR ▸ Create VR Environment…", "还没有 VR 环境。请用 合成 ▸ VR ▸ 创建 VR 环境… 新建一个。"), + ("No audio layer selected", "未选择音频图层"), + ("No background jobs running", "没有正在运行的后台任务"), + ("No composition markers", "没有合成标记"), + ("No contributor data was built into this copy.", "此副本未内置贡献者数据。"), + ("No device list from this host; the system default output is used.", "此主机无法获取设备列表;将使用系统默认输出。"), + ("No layer selected", "未选择图层"), + ("No matching effects or presets", "没有匹配的效果或预设"), + ("No model credits were built into this copy.", "此副本未内置模型致谢。"), + ("No nested compositions", "没有嵌套合成"), + ("No shortcut", "无快捷键"), + ("No unlocked layer selected", "未选择已解锁的图层"), + ("No video", "无视频"), + ("None", "无"), + ("Not applied: tick it in the list to add it.", "未应用:在列表中勾选即可添加。"), + ("Not assigned", "未分配"), + ("Note: Shadows are only cast from layers with Cast Shadows enabled.", "注意:只有启用了“投射阴影”的图层才会投射阴影。"), + ("Notify", "通知"), + ("OK", "确定"), + ("Offset:", "偏移:"), + ("On ({}) stop:", "在({})时停止:"), + ("One-Node Camera", "单节点摄像机"), + ("Opacity:", "不透明度:"), + ("Open Composition", "打开合成"), + ("Open Last Saved Project", "打开上次保存的项目"), + ("Open New Composition", "打开新建合成"), + ("Open Output", "打开输出"), + ("Open a composition to build Essential Graphics", "打开一个合成以构建基本图形"), + ("Open a composition to see its scopes", "打开一个合成以查看其示波器"), + ("Open a composition to track motion.", "打开一个合成以跟踪运动。"), + ("Opus Tuning: {}", "Opus 调优:{}"), + ("Original Duration:", "原始持续时间:"), + ("Output Cycle: {}", "输出循环:{}"), + ("Output To:", "输出到:"), + ("Parameter Name:", "参数名称:"), + ("Parent pick whip: drag onto a layer", "父级拾取器:拖到图层上"), + ("Pixel Aspect Ratio:", "像素长宽比:"), + ("Playing on Video Preview", "在视频预览中播放"), + ("Post-Render Action: {}", "渲染后动作:{}"), + ("Preserve frame rate when nested or in render queue", "嵌套或在渲染队列中时保留帧速率"), + ("Preserve resolution when nested", "嵌套时保留分辨率"), + ("Preset", "预设"), + ("Preset:", "预设:"), + ("Presets", "预设"), + ("Previous", "上一个"), + ("Primary:", "主要:"), + ("Process Before Match", "匹配前处理"), + ("Profile: {}", "配置文件:{}"), + ( + "Project color depth: click to cycle 8/16/32 bpc, Alt-click for Project Settings", + "项目颜色深度:单击可在 8/16/32 bpc 之间循环,按住 Alt 单击可打开项目设置", + ), + ("Project: {}", "项目:{}"), + ("Projects", "项目"), + ("Protected Region", "受保护区域"), + ("Proxy Use: {}", "代理使用:{}"), + ("Push All to Comp", "全部推送到合成"), + ("Radius", "半径"), + ("Rate Control: {}", "比特率控制:{}"), + ("Recent", "最近使用"), + ("Recording", "录制中"), + ("Remove", "移除"), + ("Rename", "重命名"), + ("Render: {} ms • UI {} fps{}", "渲染:{} 毫秒 • 界面 {} fps{}"), + ("Renderer:", "渲染器:"), + ("Request Persistent Storage", "请求持久化存储"), + ("Reset", "重置"), + ("Reset Labels", "重置标签"), + ("Reset Preset", "重置预设"), + ("Reset to Default", "重置为默认"), + ("Reset to Saved Layout", "重置为已保存的布局"), + ("Resolution/Down Sample Factor Popup", "分辨率/向下采样系数弹出菜单"), + ("Restore Panel Size", "恢复面板大小"), + ("Reverse the order", "反转顺序"), + ("Revert All", "全部还原"), + ("Roman Hanging Punctuation", "西文悬挂标点"), + ("Roving:", "游动:"), + ("Ruler zero point (drag to move, double-click to reset)", "标尺零点(拖动可移动,双击可重置)"), + ("Rulers", "标尺"), + ("Run", "运行"), + ("Sample Format: {}", "采样格式:{}"), + ("Samples Per Frame:", "每帧采样数:"), + ("Save from #{}", "从 #{} 保存"), + ("Saved {}", "已保存 {}"), + ("Script… (⌘/Ctrl+Enter runs)", "脚本…(⌘/Ctrl+Enter 运行)"), + ("Search commands or shortcuts", "搜索命令或快捷键"), + ("Select", "选择"), + ("Select a command to change its shortcut.", "选择一条命令以更改其快捷键。"), + ("Select a layer to see its effects", "选择图层以查看其效果"), + ("Select a layer to see its properties", "选择图层以查看其属性"), + ( + "Select a layer with a mask or shape path (or the path property), or Puppet pins.", + "请选择带蒙版或形状路径(或路径属性)的图层,或带有人偶定位点的图层。", + ), + ("Select a text layer", "选择文字图层"), + ("Select an item in the Project panel", "在项目面板中选择一个项"), + ("Select animated properties to show them in the Graph Editor", "选择已制作动画的属性以在图表编辑器中显示"), + ( + "Select the leader pin, then Shift-click the pins that should trail it (hair, cloth, tails)", + "先选择引导定位点,再按住 Shift 单击应跟随它的定位点(头发、布料、尾巴)", + ), + ("Set Ground Plane and Origin", "设置地平面和原点"), + ("Set Paint Colors to Black and White (D)", "将绘画颜色设为黑白(D)"), + ("Settings", "设置"), + ("Settings Name:", "设置名称:"), + ("Shadow Darkness", "阴影暗度"), + ("Shadow Diffusion", "阴影扩散"), + ("Shadows", "阴影"), + ("Shape…", "形状…"), + ("Show", "显示"), + ("Show Layer Name", "显示图层名称"), + ("Show Source Name", "显示源名称"), + ("Show me", "演示一下"), + ("Show:", "显示:"), + ("Shutter Angle:", "快门角度:"), + ("Shutter Phase:", "快门相位:"), + ("Size", "大小"), + ("Skip", "跳过"), + ("Snapping", "对齐"), + ("Snapping options", "对齐选项"), + ("Snapshot", "快照"), + ("Solo Supported Properties", "支持独奏的属性"), + ("Solo Switches: {}", "独奏开关:{}"), + ("Sort", "排序"), + ("Spacing:", "间距:"), + ("Spatial Interpolation:", "空间插值:"), + ("Speed:", "速度:"), + ("Start Timecode:", "开始时间码:"), + ("Start: {} Duration: {}", "开始:{} 持续时间:{}"), + ("Step {} of {}: {}", "第 {} 步,共 {} 步:{}"), + ("Stop {} at {}°", "停止点 {} 位于 {}°"), + ("Stored in: {}", "存储位置:{}"), + ("Stretch", "伸展"), + ("Stretch Factor:", "伸展系数:"), + ("Stylistic Sets", "样式集"), + ("Subpixel Positioning", "次像素定位"), + ("Swap Paint Colors (X)", "交换绘画颜色(X)"), + ("System Default", "系统默认"), + ("Table", "表格"), + ("Template name", "模板名称"), + ("Template: {}\n{}", "模板:{}\n{}"), + ("Temporal Interpolation:", "时间插值:"), + ("The Media Browser is not available here (use File ▸ Import)", "此处不支持媒体浏览器(请使用 文件 ▸ 导入)"), + ("The OpenEXR layer shown in red, green, blue and alpha", "以红、绿、蓝和 Alpha 显示的 OpenEXR 图层"), + ("The render queue is empty.", "渲染队列为空。"), + ("This ScriptUI panel has closed", "此 ScriptUI 面板已关闭"), + ("This change will create a new solid for this layer.", "此更改将为该图层创建新的纯色。"), + ("Time", "时间"), + ("Time Stretch…", "时间伸缩…"), + ("Time:", "时间:"), + ("Time: {}", "时间:{}"), + ("Title/action safe", "字幕/动作安全框"), + ("Toggle Switches / Modes", "切换开关/模式"), + ("Track Name:", "跟踪名称:"), + ("Trained models are available in the desktop app; the browser uses the classic engine.", "训练好的模型仅在桌面应用中可用;浏览器版使用经典引擎。"), + ("Transform", "变换"), + ("Transparency grid", "透明网格"), + ("Tutorial complete", "教程完成"), + ("Two-Node Camera", "双节点摄像机"), + ("Type", "文字"), + ("Type a command or effect…", "输入命令或效果…"), + ("URL:", "URL:"), + ("Undock Panel", "取消停靠面板"), + ("Unlink {}", "取消链接 {}"), + ( + "Use this option to place the currently selected layers together into a new intermediate composition.", + "使用此选项可将当前所选图层一起放入一个新的中间合成。", + ), + ("VR Environments", "VR 环境"), + ("Value:", "值:"), + ("Variable Font Axes", "可变字体轴"), + ("Video Codec: {}", "视频编解码器:{}"), + ("View:", "视图:"), + ("Welcome to EffectCraft", "欢迎使用 EffectCraft"), + ("Width:", "宽度:"), + ("You can undo this.", "您可以撤销此操作。"), + ("Your changes will be lost if you don't save them.", "如果不存储,你的更改将会丢失。"), + ("Zoom", "缩放"), + ("built-in", "内置"), + ("frames per second", "帧/秒"), + ("template name", "模板名称"), + ("{} contributors · {} commits", "{} 位贡献者 · {} 次提交"), + ("{} x {}, {}MB per 8bpc frame", "{} x {},每 8bpc 帧 {}MB"), + ("{} — coming soon", "{} — 即将推出"), + ("{} — double-click to open, {}-click to reveal", "{} — 双击打开,{}-单击可显示位置"), + ("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl 拖动定位点可记录其运动"), + ("▣ {}", "▣ {}"), + ("⚠ {} is used by:", "⚠ {} 已被以下项目使用:"), +]; + +const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ + (" OK ", " 確定 "), + (" Delete ", " 刪除 "), + (" Open Auto-Save ", " 開啟自動儲存 "), + ("(missing)", "(缺少)"), + ("360 View Orientation", "360 檢視方向"), + ("3:2 Pulldown: {}", "3:2 摺疊:{}"), + ( + "A clean-room, open-source compositor for motion graphics and visual effects: native on macOS, Windows and Linux, and in the browser. Part of the ArtCraft family of creative apps.", + "一款從零編寫、開源的動態圖形與視覺效果合成軟件:原生支援 macOS、Windows 和 Linux,也可在瀏覽器中執行。屬於 ArtCraft 創意應用家族。", + ), + ("About EffectCraft", "關於 EffectCraft"), + ("About...", "關於..."), + ("Active Camera", "活動攝影機"), + ("Adapt Feature On Every Frame", "每個影格都適應特徵"), + ("Adaptive Sample Limit:", "自適應取樣限制:"), + ("Add Animator", "添加動畫制作器"), + ("Add Comment", "添加註解"), + ("Add Formatting", "添加格式"), + ("Add Group", "添加組"), + ("Add Mirror", "添加鏡像"), + ("Add Selected As", "將所選專案添加為"), + ("Add a comment", "添加註解"), + ("Add a shape, fill, stroke or path operation", "添加形狀、填滿、筆畫或路徑操作"), + ("Add to Composition", "添加到合成"), + ("Add to Favorites", "添加到收藏夾"), + ("Add:", "添加:"), + ("Adjust Exposure (stops)", "調整曝光(光圈級數)"), + ("Adjust composition duration to the time span of the selected layers", "將合成時長調整為所選圖層的時間範圍"), + ("Advanced 3D", "高級 3D"), + ("Align Layers to:", "將圖層對齊到:"), + ("All six face cameras turn together; the scene is not changed.", "六個面攝影機一起旋轉;場景不會改變。"), + ("Anchor:", "釘選點:"), + ("Aperture", "光圈"), + ("Apply Dimensions:", "應用尺寸:"), + ("Apply Motion To:", "將運動應用於:"), + ("Arrange", "排列"), + ("Assign", "指定"), + ("Auto", "自動"), + ("Auto-saves", "自動儲存"), + ("Back", "背面"), + ("Background Color:", "背景顏色:"), + ("Blend Mode:", "混合模式:"), + ("Blending Mode", "混合模式"), + ("Blur", "模糊"), + ("Blur Level", "模糊級別"), + ("Browser Storage", "瀏覽器儲存"), + ("Browse…", "瀏覽…"), + ("Brush Dynamics", "筆刷動態"), + ("Caching frames…", "正在快取幀…"), + ("Cancel", "取消"), + ("Casts Shadows", "投射陰影"), + ("Change Renderer…", "更改算圖引擎…"), + ("Changing a shortcut creates a custom preset; the default stays as it is.", "修改快速鍵會創建自定義預設集;預設預設集保持不變。"), + ("Channel", "色版"), + ("Channel:", "通道:"), + ("Chapter:", "章節:"), + ("Choose...", "選取..."), + ("Classic 3D", "經典 3D"), + ("Clear", "清除"), + ("Clear list", "清空清單"), + ("Click a key to see its commands.", "按一下某個按鍵以查看其命令。"), + ("Click adds a point (½ height = no change)", "按一下可添加點(上下滑動高度的一半 = 無變化)"), + ("Click adds a point; drag one off to remove", "按一下可添加點;拖出一個即可刪除"), + ("Click to make this viewer active", "按一下可激活此檢視"), + ("Click to reveal the expression in the timeline", "按一下可在時間軸中顯示表達式"), + ("Click to show the active composition here", "按一下可在此顯示目前合成"), + ("Click, then click in the Composition panel", "按一下後,在合成面板中按一下"), + ("Clone Options", "仿製選項"), + ("Clone Source Overlay:", "仿製源疊加:"), + ("Close", "關閉"), + ("Close Panel", "關閉面板"), + ("Close tutorial", "關閉教學"), + ("Color", "顏色"), + ("Color Depth: {}", "顏色深度:{}"), + ("Color:", "顏色:"), + ("Colour map", "顏色圖"), + ("Columns", "列"), + ("Comment:", "註解:"), + ("Community", "社區"), + ("Composition", "合成"), + ("Composition Mini-Flowchart", "合成微型流程圖"), + ("Composition Name:", "合成名稱:"), + ("Composition Settings ▸ 3D Renderer", "合成設置 ▸ 3D 算圖引擎"), + ("Composition Settings…", "合成設置…"), + ("Composition marker bin: drag a marker to the time ruler", "合成標記箱:將標記拖到時間標尺上"), + ("Cone Angle", "圓錐角"), + ("Cone Feather", "圓錐羽化"), + ("Constrain Proportions", "約束比例"), + ("Continuous (Lock Outgoing to Incoming)", "連續(將出點鎖定到入點)"), + ("Cue Point", "提示點"), + ("Current Render", "目前算圖"), + ("Darker", "更深"), + ("Default Output:", "預設輸出:"), + ("Default: {}", "預設:{}"), + ("Defaults", "預設值"), + ("Delete", "刪除"), + ("Delete Selected Points", "刪除所選的頂點"), + ("Delete selected project items", "刪除所選的專案項"), + ("Delete this template", "刪除此範本"), + ("Depth of Field", "景深"), + ("Discord", "Discord"), + ("Distribute Layers:", "分佈圖層:"), + ("Don't Recover", "不恢復"), + ("Don't Save", "不儲存"), + ("Double-click a layer to open it in the Layer panel", "連按兩下圖層可在圖層面板中開啟"), + ("Double-click footage in the Project panel to open it here", "連按兩下專案面板中的素材可在此處開啟"), + ("Double-click to edit the comment", "連按兩下可編輯註解"), + ("Download", "下載"), + ( + "Drag properties here from the timeline,\nor use Animation > Add Property to Essential Graphics.", + "請從時間軸將屬性拖到此處,\n或使用 動畫 > 將屬性添加到基本圖形。", + ), + ("Drag to change", "拖曳可更改"), + ("Drag to create a guide", "拖曳可創建參考線"), + ("Drag to slip the source", "拖曳可滑移源"), + ("Drop Frame", "丟格"), + ("Duplicate", "重複"), + ("Duration:", "持續時間:"), + ("Edit Gradient…", "編輯漸變…"), + ("Edit Target: {}", "編輯目標:{}"), + ("Edit in the Timeline", "在時間軸中編輯"), + ("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常結束。"), + ("Effects & Presets options", "效果和預設集選項"), + ("Effects: {}", "效果:{}"), + ("Empty folder", "空資料夾"), + ("Enable Depth of Field", "啟用景深"), + ("Enable Expression", "啟用表達式"), + ("Enhance", "增強"), + ("Essential Properties — {}", "基本屬性 — {}"), + ("Event", "事件"), + ("Export Template…", "匯出範本…"), + ("Export...", "匯出..."), + ("Expression Language Menu", "表達式語言選單"), + ("Expression pick whip: drag onto a property", "表達式拾取線:拖到屬性上"), + ("Expression:", "表達式:"), + ("Eyedropper: click, then click in the Composition panel", "吸管:按一下後,在合成面板中按一下"), + ("Falloff", "衰減"), + ("Falloff Distance", "衰減距離"), + ("Favorites", "收藏"), + ("Field Render: {}", "場算圖:{}"), + ("Figures", "數字"), + ("Fill Target", "填滿目標"), + ("Finished", "已完成"), + ("Flowchart: {}", "流程圖:{}"), + ("Focus Distance", "焦距"), + ("Follow Feature Rotation and Scale", "跟隨特徵的旋轉和縮放"), + ("Footage: {}", "素材:{}"), + ("Format: {}", "格式:{}"), + ("Frame Aspect Ratio: {}", "影格長寬比:{}"), + ("Frame Blending: {}", "影格混合:{}"), + ("Frame Rate:", "影格速率:"), + ("Frame Render Time {}ms", "影格算圖時間 {} 毫秒"), + ("Frame Target:", "幀目標:"), + ("Grab bag", "清單"), + ("Grid", "格點"), + ("Guide Layers: {}", "參考圖層:{}"), + ("Guides", "參考線"), + ("Height:", "高度:"), + ("Hide This", "隱藏此項"), + ("Histogram", "直方圖"), + ("Hold In Place", "保持在原位"), + ("Home", "主頁"), + ("If Confidence is Below", "如果置信度低於"), + ("Import", "匯入"), + ("Import and Add to Composition", "匯入並添加到合成"), + ("Import...", "匯入..."), + ("Imported media", "已匯入的媒體"), + ("Influence:", "影響:"), + ("Install from File…", "從文件安裝…"), + ("Intensity", "強度"), + ("Inverted", "反轉"), + ("Join the ArtCraft community on Discord", "加入 ArtCraft 的 Discord 社區"), + ("Key: {}", "按鍵:{}"), + ("LEARN", "學習"), + ("Label", "標籤"), + ("Label Colors and Names:", "標籤顏色和名稱:"), + ("Label:", "標籤:"), + ("Latest auto-save: {}", "最新的自動儲存:{}"), + ("Layer", "圖層"), + ("Layer controls", "圖層控制"), + ("Layer {}", "圖層 {}"), + ("Layer:", "圖層:"), + ("Layer: {}", "圖層:{}"), + ("Learn", "學習"), + ("Leave all attributes in \"{}\"", "將所有屬性保留在“{}”中"), + ("Licence: {}", "許可:{}"), + ("Ligatures", "連字"), + ("Light Type", "燈光類型"), + ("Lighter", "更淺"), + ("Link Selected Property", "鏈接所選屬性"), + ("Lock Aspect Ratio to {}", "鎖定長寬比例為 {}"), + ("Lock to Zoom", "鎖定縮放"), + ("Locked: {} — {}", "已鎖定:{} — {}"), + ("Log:", "日志:"), + ("Loop (Trace Path)", "循環(追蹤路徑)"), + ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT 或 Apache-2.0 • 字型:Inter、JetBrains Mono(OFL)"), + ("Magnification ratio popup", "放大倍率快顯選單"), + ("Make Comp Size", "設為合成長寬"), + ("Masks", "遮色片"), + ("Maximize Panel", "最大化面板"), + ("Media Replacement", "媒體替換"), + ("Mesh:", "格點:"), + ("Method Used: {} Average Error: {} pixels", "使用方法:{} 平均誤差:{} 像素"), + ("Models folder: {}", "模型資料夾:{}"), + ("Modifiers:", "修飾鍵:"), + ("More", "更多"), + ("More ArtCraft apps", "更多 ArtCraft 應用"), + ("More tutorials", "更多教學"), + ("Motion Blur", "運動模糊"), + ("Motion Blur: {}", "動態模糊:{}"), + ("Motion Sketch: drag to record", "動態草圖:拖曳以記錄"), + ("Move Down", "下移"), + ("Move Up", "上移"), + ("Move to Project Root", "移到專案根目錄"), + ("Name", "名稱"), + ("Name:", "名稱:"), + ("Navigation", "導航"), + ("New", "新建"), + ("New Comp from Selection", "由所選專案新建合成"), + ("New Duration:", "新持續時間:"), + ("New composition name:", "新合成名稱:"), + ("New from Render Queue item:", "由算圖佇列項新建:"), + ("New from Template", "從範本新建"), + ("Next", "下一個"), + ("Nice work. Pick another tutorial on the Home screen's Learn tab.", "干得漂亮。請在主頁面的“學習”索引標籤中選取另一個教學。"), + ("No VR environments yet. Create one with Composition ▸ VR ▸ Create VR Environment…", "還沒有 VR 環境。請用 合成 ▸ VR ▸ 創建 VR 環境… 新建一個。"), + ("No audio layer selected", "未選取音訊圖層"), + ("No background jobs running", "沒有正在執行的後台任務"), + ("No composition markers", "沒有合成標記"), + ("No contributor data was built into this copy.", "此副本未內置貢獻者數據。"), + ("No device list from this host; the system default output is used.", "此主機無法獲取設備清單;將使用系統預設輸出。"), + ("No layer selected", "未選取圖層"), + ("No matching effects or presets", "沒有匹配的效果或預設集"), + ("No model credits were built into this copy.", "此副本未內置模型致謝。"), + ("No nested compositions", "沒有嵌套合成"), + ("No shortcut", "無快速鍵"), + ("No unlocked layer selected", "未選取已解鎖的圖層"), + ("No video", "無視訊"), + ("None", "無"), + ("Not applied: tick it in the list to add it.", "未應用:在清單中勾選即可添加。"), + ("Not assigned", "未分配"), + ("Note: Shadows are only cast from layers with Cast Shadows enabled.", "注意:只有啟用了“投射陰影”的圖層才會投射陰影。"), + ("Notify", "通知"), + ("OK", "確定"), + ("Offset:", "偏移:"), + ("On ({}) stop:", "在({})時停止:"), + ("One-Node Camera", "單節點攝影機"), + ("Opacity:", "不透明度:"), + ("Open Composition", "開啟合成"), + ("Open Last Saved Project", "開啟上次儲存的專案"), + ("Open New Composition", "開啟新建合成"), + ("Open Output", "開啟輸出"), + ("Open a composition to build Essential Graphics", "開啟一個合成以構建基本圖形"), + ("Open a composition to see its scopes", "開啟一個合成以查看其示波器"), + ("Open a composition to track motion.", "開啟一個合成以追蹤運動。"), + ("Opus Tuning: {}", "Opus 調優:{}"), + ("Original Duration:", "原始持續時間:"), + ("Output Cycle: {}", "輸出循環:{}"), + ("Output To:", "輸出到:"), + ("Parameter Name:", "參數名稱:"), + ("Parent pick whip: drag onto a layer", "上層拾取線:拖到圖層上"), + ("Pixel Aspect Ratio:", "像素長寬比例:"), + ("Playing on Video Preview", "在視訊預覽中播放"), + ("Post-Render Action: {}", "算圖後動作:{}"), + ("Preserve frame rate when nested or in render queue", "嵌套或在算圖佇列中時保留影格速率"), + ("Preserve resolution when nested", "嵌套時保留分辨率"), + ("Preset", "預設集"), + ("Preset:", "預設集:"), + ("Presets", "預設集"), + ("Previous", "上一個"), + ("Primary:", "主要:"), + ("Process Before Match", "匹配前處理"), + ("Profile: {}", "設定檔:{}"), + ( + "Project color depth: click to cycle 8/16/32 bpc, Alt-click for Project Settings", + "專案顏色深度:按一下可在 8/16/32 bpc 之間循環,按住 Alt 按一下可開啟專案設置", + ), + ("Project: {}", "專案:{}"), + ("Projects", "專案"), + ("Protected Region", "受保護區域"), + ("Proxy Use: {}", "代理使用:{}"), + ("Push All to Comp", "全部推送到合成"), + ("Radius", "半徑"), + ("Rate Control: {}", "位元速率控制:{}"), + ("Recent", "最近使用"), + ("Recording", "錄製中"), + ("Remove", "移除"), + ("Rename", "重命名"), + ("Render: {} ms • UI {} fps{}", "算圖:{} 毫秒 • 介面 {} fps{}"), + ("Renderer:", "算圖引擎:"), + ("Request Persistent Storage", "要求持久化儲存"), + ("Reset", "重置"), + ("Reset Labels", "重置標籤"), + ("Reset Preset", "重置預設集"), + ("Reset to Default", "重置為預設"), + ("Reset to Saved Layout", "重置為已儲存的布局"), + ("Resolution/Down Sample Factor Popup", "分辨率/向下取樣係數快顯選單"), + ("Restore Panel Size", "恢復面板大小"), + ("Reverse the order", "反轉順序"), + ("Revert All", "全部還原"), + ("Roman Hanging Punctuation", "西文懸掛標點"), + ("Roving:", "游動:"), + ("Ruler zero point (drag to move, double-click to reset)", "標尺零點(拖曳可移動,連按兩下可重置)"), + ("Rulers", "標尺"), + ("Run", "執行"), + ("Sample Format: {}", "取樣格式:{}"), + ("Samples Per Frame:", "每個影格取樣數:"), + ("Save from #{}", "從 #{} 儲存"), + ("Saved {}", "已儲存 {}"), + ("Script… (⌘/Ctrl+Enter runs)", "指令碼…(⌘/Ctrl+Enter 執行)"), + ("Search commands or shortcuts", "搜尋命令或快速鍵"), + ("Select", "選取"), + ("Select a command to change its shortcut.", "選取一條命令以更改其快速鍵。"), + ("Select a layer to see its effects", "選取圖層以查看其效果"), + ("Select a layer to see its properties", "選取圖層以查看其屬性"), + ( + "Select a layer with a mask or shape path (or the path property), or Puppet pins.", + "請選取帶遮色片或形狀路徑(或路徑屬性)的圖層,或帶有人偶釘選點的圖層。", + ), + ("Select a text layer", "選取文字圖層"), + ("Select an item in the Project panel", "在專案面板中選取一個項"), + ("Select animated properties to show them in the Graph Editor", "選取已制作動畫的屬性以在圖表編輯器中顯示"), + ( + "Select the leader pin, then Shift-click the pins that should trail it (hair, cloth, tails)", + "先選取引導釘選點,再按住 Shift 按一下應跟隨它的釘選點(頭發、布料、尾巴)", + ), + ("Set Ground Plane and Origin", "設置地平面和原點"), + ("Set Paint Colors to Black and White (D)", "將繪畫顏色設為黑白(D)"), + ("Settings", "設置"), + ("Settings Name:", "設置名稱:"), + ("Shadow Darkness", "陰影暗度"), + ("Shadow Diffusion", "陰影擴散"), + ("Shadows", "陰影"), + ("Shape…", "形狀…"), + ("Show", "顯示"), + ("Show Layer Name", "顯示圖層名稱"), + ("Show Source Name", "顯示源名稱"), + ("Show me", "演示一下"), + ("Show:", "顯示:"), + ("Shutter Angle:", "快門角度:"), + ("Shutter Phase:", "快門相位:"), + ("Size", "大小"), + ("Skip", "跳過"), + ("Snapping", "對齊"), + ("Snapping options", "對齊選項"), + ("Snapshot", "快照"), + ("Solo Supported Properties", "支援獨奏的屬性"), + ("Solo Switches: {}", "獨奏開關:{}"), + ("Sort", "排序"), + ("Spacing:", "間距:"), + ("Spatial Interpolation:", "空間插值:"), + ("Speed:", "速度:"), + ("Start Timecode:", "開始時間碼:"), + ("Start: {} Duration: {}", "開始:{} 持續時間:{}"), + ("Step {} of {}: {}", "第 {} 步,共 {} 步:{}"), + ("Stop {} at {}°", "停止點 {} 位於 {}°"), + ("Stored in: {}", "儲存位置:{}"), + ("Stretch", "伸展"), + ("Stretch Factor:", "伸展係數:"), + ("Stylistic Sets", "樣式集"), + ("Subpixel Positioning", "次像素定位"), + ("Swap Paint Colors (X)", "交換繪畫顏色(X)"), + ("System Default", "系統預設"), + ("Table", "表格"), + ("Template name", "範本名稱"), + ("Template: {}\n{}", "範本:{}\n{}"), + ("Temporal Interpolation:", "時間插值:"), + ("The Media Browser is not available here (use File ▸ Import)", "此處不支援媒體瀏覽器(請使用 文件 ▸ 匯入)"), + ("The OpenEXR layer shown in red, green, blue and alpha", "以紅、綠、藍和 Alpha 顯示的 OpenEXR 圖層"), + ("The render queue is empty.", "算圖佇列為空。"), + ("This ScriptUI panel has closed", "此 ScriptUI 面板已關閉"), + ("This change will create a new solid for this layer.", "此更改將為該圖層創建新的純色。"), + ("Time", "時間"), + ("Time Stretch…", "時間伸縮…"), + ("Time:", "時間:"), + ("Time: {}", "時間:{}"), + ("Title/action safe", "字幕/動作安全框"), + ("Toggle Switches / Modes", "切換開關/模式"), + ("Track Name:", "追蹤名稱:"), + ("Trained models are available in the desktop app; the browser uses the classic engine.", "訓練好的模型僅在桌面應用中可用;瀏覽器版使用經典引擎。"), + ("Transform", "變換"), + ("Transparency grid", "透明格點"), + ("Tutorial complete", "教學完成"), + ("Two-Node Camera", "雙節點攝影機"), + ("Type", "文字"), + ("Type a command or effect…", "輸入命令或效果…"), + ("URL:", "URL:"), + ("Undock Panel", "取消停靠面板"), + ("Unlink {}", "取消鏈接 {}"), + ( + "Use this option to place the currently selected layers together into a new intermediate composition.", + "使用此選項可將目前所選圖層一起放入一個新的中間合成。", + ), + ("VR Environments", "VR 環境"), + ("Value:", "值:"), + ("Variable Font Axes", "可變字型軸"), + ("Video Codec: {}", "視訊轉碼器:{}"), + ("View:", "檢視:"), + ("Welcome to EffectCraft", "歡迎使用 EffectCraft"), + ("Width:", "寬度:"), + ("You can undo this.", "您可以復原此操作。"), + ("Your changes will be lost if you don't save them.", "如果不儲存,你的更改將會丟失。"), + ("Zoom", "縮放"), + ("built-in", "內建"), + ("frames per second", "影格/秒"), + ("template name", "範本名稱"), + ("{} contributors · {} commits", "{} 位貢獻者 · {} 次提交"), + ("{} x {}, {}MB per 8bpc frame", "{} x {},每 8bpc 幀 {}MB"), + ("{} — coming soon", "{} — 即將推出"), + ("{} — double-click to open, {}-click to reveal", "{} — 連按兩下開啟,{}-按一下可顯示位置"), + ("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl 拖曳釘選點可記錄其運動"), + ("▣ {}", "▣ {}"), + ("⚠ {} is used by:", "⚠ {} 已被以下專案使用:"), +]; + +#[cfg(test)] +mod tests { + use super::*; + + /// The interface text that has moved into this catalog. Add a panel here when you convert it; + /// the tests below then keep it translated. + const CONVERTED: &[(&str, &str)] = &[ + ("credits.rs", include_str!("../credits.rs")), + ("header.rs", include_str!("../header.rs")), + ("menus.rs", include_str!("../menus.rs")), + ("panels/anim_tools.rs", include_str!("../panels/anim_tools.rs")), + ("panels/camera_tracker_ui.rs", include_str!("../panels/camera_tracker_ui.rs")), + ("panels/comp_settings.rs", include_str!("../panels/comp_settings.rs")), + ("panels/content_fill_panel.rs", include_str!("../panels/content_fill_panel.rs")), + ("panels/delete_items.rs", include_str!("../panels/delete_items.rs")), + ("panels/dialogs.rs", include_str!("../panels/dialogs.rs")), + ("panels/dialogs_3d.rs", include_str!("../panels/dialogs_3d.rs")), + ("panels/effect_controls.rs", include_str!("../panels/effect_controls.rs")), + ("panels/effects_presets.rs", include_str!("../panels/effects_presets.rs")), + ("panels/essential.rs", include_str!("../panels/essential.rs")), + ("panels/expr_bar.rs", include_str!("../panels/expr_bar.rs")), + ("panels/flowchart.rs", include_str!("../panels/flowchart.rs")), + ("panels/footage_panel.rs", include_str!("../panels/footage_panel.rs")), + ("panels/forms.rs", include_str!("../panels/forms.rs")), + ("panels/fx_editors.rs", include_str!("../panels/fx_editors.rs")), + ("panels/graph.rs", include_str!("../panels/graph.rs")), + ("panels/home.rs", include_str!("../panels/home.rs")), + ("panels/info.rs", include_str!("../panels/info.rs")), + ("panels/key_dialogs.rs", include_str!("../panels/key_dialogs.rs")), + ("panels/layer_panel.rs", include_str!("../panels/layer_panel.rs")), + ("panels/layer_styles_dialog.rs", include_str!("../panels/layer_styles_dialog.rs")), + ("panels/learn.rs", include_str!("../panels/learn.rs")), + ("panels/markers_ui.rs", include_str!("../panels/markers_ui.rs")), + ("panels/media_panels.rs", include_str!("../panels/media_panels.rs")), + ("panels/misc.rs", include_str!("../panels/misc.rs")), + ("panels/mod.rs", include_str!("../panels/mod.rs")), + ("panels/paint_panels.rs", include_str!("../panels/paint_panels.rs")), + ("panels/path_vr_panels.rs", include_str!("../panels/path_vr_panels.rs")), + ("panels/precomp.rs", include_str!("../panels/precomp.rs")), + ("panels/project.rs", include_str!("../panels/project.rs")), + ("panels/properties.rs", include_str!("../panels/properties.rs")), + ("panels/render_queue.rs", include_str!("../panels/render_queue.rs")), + ("panels/rq_templates.rs", include_str!("../panels/rq_templates.rs")), + ("panels/scopes_panel.rs", include_str!("../panels/scopes_panel.rs")), + ("panels/script_console.rs", include_str!("../panels/script_console.rs")), + ("panels/scriptui_view.rs", include_str!("../panels/scriptui_view.rs")), + ("panels/settings.rs", include_str!("../panels/settings.rs")), + ("panels/shortcut_editor.rs", include_str!("../panels/shortcut_editor.rs")), + ("panels/templates.rs", include_str!("../panels/templates.rs")), + ("panels/text_panels.rs", include_str!("../panels/text_panels.rs")), + ("panels/timeline.rs", include_str!("../panels/timeline.rs")), + ("panels/tracker.rs", include_str!("../panels/tracker.rs")), + ("panels/unsaved.rs", include_str!("../panels/unsaved.rs")), + ("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")), + ]; + + /// Call shapes that hand a string literal straight to the user. + const CALLS: &[&str] = &[ + ".label(\"", + ".button(\"", + ".small(\"", + ".strong(\"", + ".heading(\"", + ".monospace(\"", + ".on_hover_text(\"", + ".hint_text(\"", + ".text(\"", + ".prefix(\"", + ".suffix(\"", + "RichText::new(\"", + "Label::new(\"", + "Button::new(\"", + "Window::new(\"", + "CollapsingHeader::new(\"", + ]; + + /// Literals that stay as they are: a unit or a separator reads the same in every language. + const UNITS: &[&str] = &[" px", "%", "·", "s", "ms", "°", "fps", " bpc", " mm", " s", " %", " pixels"]; + + fn catalogs() -> Vec<(&'static str, &'static str)> { + SIMPLIFIED_CHINESE.iter().copied().chain(TRADITIONAL_CHINESE.iter().copied()).collect() + } + + /// A row that translates nothing, repeats a source string, or changes the number of values the + /// English source carries is a bug in the catalog itself. + #[test] + fn catalogs_are_unique_and_keep_the_english_shape() { + let hans: Vec<&str> = SIMPLIFIED_CHINESE.iter().map(|(en, _)| *en).collect(); + let hant: Vec<&str> = TRADITIONAL_CHINESE.iter().map(|(en, _)| *en).collect(); + assert_eq!(hans, hant, "the two Chinese catalogs list different source strings"); + let mut sorted = hans.clone(); + sorted.sort_unstable(); + let listed = sorted.len(); + sorted.dedup(); + assert_eq!(listed, sorted.len(), "a source string is listed twice"); + for (en, translated) in catalogs() { + assert!(!en.is_empty() && !translated.is_empty(), "empty row for {en:?}"); + assert!(en.contains("{}") || !en.contains('{'), "{en:?} has a placeholder but no `{{}}`"); + assert_eq!(en.matches("{}").count(), translated.matches("{}").count(), "{en:?} and {translated:?} carry a different number of values"); + } + } + + /// English, a language without rows yet, and anything the catalog does not know stay verbatim: + /// a filename or layer name must never be translated. + #[test] + fn english_and_unknown_text_pass_through() { + set_current("en"); + assert_eq!(tr("Composition Name:"), "Composition Name:"); + set_current("ja"); // no rows for Japanese yet + assert_eq!(tr("Composition Name:"), "Composition Name:"); + set_current("zh-hans"); + assert_eq!(tr("Composition Name:"), "合成名称:"); + assert_eq!(tr("my layer.mp4"), "my layer.mp4"); + set_current("en"); + } + + /// Values fill the placeholders in order, and a missing value is dropped instead of panicking. + #[test] + fn tr_args_fills_values_in_order() { + set_current("zh-hans"); + assert_eq!(tr_args("Proxy Use: {}", &[&"Best"]), "代理使用:Best"); + assert_eq!(tr_args("Step {} of {}: {}", &[&1, &3, &"Colour"]), "第 1 步,共 3 步:Colour"); + assert_eq!(tr_args("Proxy Use: {}", &[]), "代理使用:"); + set_current("en"); + assert_eq!(tr_args("Proxy Use: {}", &[&"Best"]), "Proxy Use: Best"); + } + + /// Every row is asked for by a converted panel: a leftover row means the panel went back to its + /// English literal. + #[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:?}"); + assert!( + CONVERTED.iter().any(|(_, src)| src.contains(&plain) || src.contains(&formatted)), + "{en:?} is in the catalog but no converted panel asks for it" + ); + } + } + + /// 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. + #[test] + fn converted_panels_have_no_unwrapped_literals() { + for (path, source) in CONVERTED { + for (n, line) in source.lines().enumerate() { + let code = line.split("//").next().unwrap_or(line); + for call in CALLS { + let Some(at) = code.find(call) else { continue }; + let rest = &code[at + call.len()..]; + let Some(end) = rest.find('"') else { continue }; + let literal = &rest[..end]; + assert!( + literal.chars().count() < 2 || UNITS.contains(&literal), + "{path}:{}: interface text `{literal}` in `{call}` is not in the catalog", + n + 1 + ); + } + } + } + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b51bb84b..01a02881 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1579,6 +1579,7 @@ impl eframe::App for EffectcraftApp { // The CJK fallback fonts depend on the interface language, so they follow a change of // Settings ▸ General ▸ Language instead of only the first frame. let language = crate::i18n::language(self); + crate::i18n::set_current(language); if !self.styled || self.styled_language != language { theme::install(ctx, &self.tokens, language); self.styled_language = language; diff --git a/crates/ui-egui/src/panels/anim_tools.rs b/crates/ui-egui/src/panels/anim_tools.rs index 5da48415..15a4f4f8 100644 --- a/crates/ui-egui/src/panels/anim_tools.rs +++ b/crates/ui-egui/src/panels/anim_tools.rs @@ -6,6 +6,7 @@ //! Start Capture, pressing in the viewer records the pointer path in real time while the comp //! plays; releasing turns it into Position keys. +use crate::i18n::{tr, tr_args}; use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2}; use serde_json::json; @@ -170,7 +171,7 @@ pub fn motion_sketch(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { label(&p, x0, y, "Smoothing:", &t); st.sketch_smoothing = number(app, ui, pos2(xv, y), "motionSketch.smoothing", st.sketch_smoothing, 0.1, (0.0, 100.0), 1, ""); y += ROW + 2.0; - p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Show:", Tokens::ui(12.0), t.text_dim); + p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Show:"), Tokens::ui(12.0), t.text_dim); y += ROW - 6.0; st.sketch_wireframe = checkbox(app, ui, pos2(x0 + 8.0, y), "motionSketch.wireframe", "Wireframe", st.sketch_wireframe); y += ROW - 4.0; @@ -183,7 +184,7 @@ pub fn motion_sketch(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let fr = c.frame_rate; let (s, e) = (app.session.time(), c.work_area.1); let tc = |x| crate::panels::timecode(&app.session, c, x); - p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, format!("Start: {} Duration: {}", tc(s), tc(e - s)), Tokens::ui(11.0), t.text_faint); + p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr_args("Start: {} Duration: {}", &[&(tc(s)), &(tc(e - s))]), Tokens::ui(11.0), t.text_faint); let _ = fr; y += ROW; let b = Rect::from_min_size(pos2(x0, y), vec2(120.0, 24.0)); @@ -290,7 +291,7 @@ pub fn sketch_overlay(app: &mut EffectcraftApp, ui: &mut egui::Ui) { ctx.data_mut(|d| d.insert_temp(capture_id(), c)); } None => { - painter.text(area.center_top() + vec2(0.0, 18.0), Align2::CENTER_CENTER, "Motion Sketch: drag to record", Tokens::ui(12.0), Color32::WHITE); + painter.text(area.center_top() + vec2(0.0, 18.0), Align2::CENTER_CENTER, tr("Motion Sketch: drag to record"), Tokens::ui(12.0), Color32::WHITE); } } } diff --git a/crates/ui-egui/src/panels/camera_tracker_ui.rs b/crates/ui-egui/src/panels/camera_tracker_ui.rs index 9434f537..18e24813 100644 --- a/crates/ui-egui/src/panels/camera_tracker_ui.rs +++ b/crates/ui-egui/src/panels/camera_tracker_ui.rs @@ -13,6 +13,7 @@ //! //! Everything is dispatched to the `camera.*` commands. +use crate::i18n::{tr, tr_args}; use std::sync::Arc; use effectcraft_engine::Session; @@ -297,7 +298,7 @@ pub fn viewer_hook( let some = !pts.is_empty(); let many = pts.len() > 1; let mut pick: Option<(&str, serde_json::Value)> = None; - if ui.add_enabled(some, egui::Button::new("Set Ground Plane and Origin")).clicked() { + if ui.add_enabled(some, egui::Button::new(tr("Set Ground Plane and Origin"))).clicked() { pick = Some(("camera.setGroundPlane", with(None, false))); } ui.separator(); @@ -320,7 +321,7 @@ pub fn viewer_hook( } } ui.separator(); - if ui.add_enabled(some, egui::Button::new("Delete Selected Points")).clicked() { + if ui.add_enabled(some, egui::Button::new(tr("Delete Selected Points"))).clicked() { pick = Some(("camera.deletePoints", with(None, false))); } if let Some((cmd, q)) = pick { @@ -419,7 +420,7 @@ pub fn editor( let status = match app.session.camera_progress() { Some(pr) if running => pr.banner(), _ => match &solve { - Some(sv) => format!("Method Used: {} Average Error: {:.2} pixels", sv.method_used.label(), sv.average_error), + Some(sv) => tr_args("Method Used: {} Average Error: {} pixels", &[&(sv.method_used.label()), &format!("{:.2}", sv.average_error)]), None if ct::tracks(¶ms).is_some() && app.session.camera_pending.iter().any(|(_, l, u)| *l == layer.id && *u == g.uid) => { "Waiting to solve".into() } diff --git a/crates/ui-egui/src/panels/comp_settings.rs b/crates/ui-egui/src/panels/comp_settings.rs index 2befd5e6..39b7580e 100644 --- a/crates/ui-egui/src/panels/comp_settings.rs +++ b/crates/ui-egui/src/panels/comp_settings.rs @@ -2,6 +2,7 @@ //! Basic / Advanced / 3D Renderer tabs, Cancel / OK. Every control registers an automation id //! (`dialog.comp.`). +use crate::i18n::{tr, tr_args}; use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode}; use egui::{Color32, vec2}; use serde_json::json; @@ -174,7 +175,7 @@ fn timecode_field(ui: &mut egui::Ui, buf: &mut Option, secs: &mut f64, r pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registry) -> Option { let mut out = None; ui.horizontal(|ui| { - ui.label("Composition Name:"); + ui.label(tr("Composition Name:")); let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(300.0)); auto.add("dialog.comp.name", r.rect, "Composition Name"); }); @@ -193,7 +194,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr match d.tab { Tab::Basic => { egui::Grid::new("comp-basic").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| { - ui.label("Preset:"); + ui.label(tr("Preset:")); let current = PRESETS .iter() .find(|(_, w, h, par, f)| *w == d.width && *h == d.height && (par - d.pixel_aspect).abs() < 1e-3 && (f - d.fps).abs() < 1e-3) @@ -208,7 +209,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr }); auto.add("dialog.comp.preset", r.response.rect, "Preset"); ui.end_row(); - ui.label("Width:"); + ui.label(tr("Width:")); ui.horizontal(|ui| { let (ow, oh) = (d.width, d.height); let r = ui.add(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px")); @@ -221,7 +222,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr } }); ui.end_row(); - ui.label("Height:"); + ui.label(tr("Height:")); let (ow, oh) = (d.width, d.height); let r = ui.add(egui::DragValue::new(&mut d.height).range(4..=30000).suffix(" px")); auto.add("dialog.comp.height", r.rect, "Height"); @@ -229,7 +230,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr d.width = ((d.height as f64) * ow as f64 / oh as f64).round().max(4.0) as u32; } ui.end_row(); - ui.label("Pixel Aspect Ratio:"); + ui.label(tr("Pixel Aspect Ratio:")); ui.horizontal(|ui| { let cur = PIXEL_ASPECTS .iter() @@ -245,12 +246,12 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr }); auto.add("dialog.comp.pixelAspect", r.response.rect, "Pixel Aspect Ratio"); ui.label( - egui::RichText::new(format!("Frame Aspect Ratio: {}", aspect_label(d.width as f64 * d.pixel_aspect, d.height as f64))) + egui::RichText::new(tr_args("Frame Aspect Ratio: {}", &[&(aspect_label(d.width as f64 * d.pixel_aspect, d.height as f64))])) .color(Color32::GRAY), ); }); ui.end_row(); - ui.label("Frame Rate:"); + ui.label(tr("Frame Rate:")); ui.horizontal(|ui| { // Any rate can be typed; the list holds the common ones. let r = ui.add(egui::DragValue::new(&mut d.fps).range(1.0..=999.0).speed(0.01).max_decimals(3)); @@ -263,25 +264,25 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr } }); auto.add("dialog.comp.frameRates", r.response.rect, "Frame Rate presets"); - ui.label("frames per second"); + ui.label(tr("frames per second")); if rate.supports_drop_frame() { - ui.label(egui::RichText::new("Drop Frame").color(Color32::GRAY)); + ui.label(egui::RichText::new(tr("Drop Frame")).color(Color32::GRAY)); } }); ui.end_row(); ui.label(""); let mb = d.width as f64 * d.height as f64 * 4.0 / (1024.0 * 1024.0); - ui.label(egui::RichText::new(format!("{} x {}, {mb:.1}MB per 8bpc frame", d.width, d.height)).color(Color32::GRAY)); + ui.label(egui::RichText::new(tr_args("{} x {}, {}MB per 8bpc frame", &[&d.width, &d.height, &format!("{mb:.1}")])).color(Color32::GRAY)); ui.end_row(); - ui.label("Start Timecode:"); + ui.label(tr("Start Timecode:")); let r = ui.horizontal(|ui| timecode_field(ui, &mut d.start_tc, &mut d.start, rate, i64::MIN / 4)).inner; auto.add("dialog.comp.startTimecode", r.rect, "Start Timecode"); ui.end_row(); - ui.label("Duration:"); + ui.label(tr("Duration:")); let r = ui.horizontal(|ui| timecode_field(ui, &mut d.dur_tc, &mut d.duration, rate, 1)).inner; auto.add("dialog.comp.duration", r.rect, "Duration"); ui.end_row(); - ui.label("Background Color:"); + ui.label(tr("Background Color:")); ui.horizontal(|ui| { let r = crate::widgets::srgb_color_button(ui, &mut d.bg); auto.add("dialog.comp.background", r.rect, "Background Color"); @@ -292,7 +293,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr } Tab::Advanced => { egui::Grid::new("comp-adv").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| { - ui.label("Anchor:"); + ui.label(tr("Anchor:")); egui::Grid::new("comp-anchor").spacing([2.0, 2.0]).show(ui, |ui| { for i in 0..9u8 { let on = d.anchor == i; @@ -309,28 +310,28 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr }); ui.end_row(); ui.label(""); - let r = ui.checkbox(&mut d.preserve_frame_rate, "Preserve frame rate when nested or in render queue"); + let r = ui.checkbox(&mut d.preserve_frame_rate, tr("Preserve frame rate when nested or in render queue")); auto.add("dialog.comp.preserveFrameRate", r.rect, "Preserve frame rate when nested or in render queue"); ui.end_row(); ui.label(""); - let r = ui.checkbox(&mut d.preserve_resolution, "Preserve resolution when nested"); + let r = ui.checkbox(&mut d.preserve_resolution, tr("Preserve resolution when nested")); auto.add("dialog.comp.preserveResolution", r.rect, "Preserve resolution when nested"); ui.end_row(); - ui.label(egui::RichText::new("Motion Blur").strong()); + ui.label(egui::RichText::new(tr("Motion Blur")).strong()); ui.end_row(); - ui.label("Shutter Angle:"); + ui.label(tr("Shutter Angle:")); let r = ui.add(egui::DragValue::new(&mut d.shutter_angle).range(0.0..=720.0).suffix("°")); auto.add("dialog.comp.shutterAngle", r.rect, "Shutter Angle"); ui.end_row(); - ui.label("Shutter Phase:"); + ui.label(tr("Shutter Phase:")); let r = ui.add(egui::DragValue::new(&mut d.shutter_phase).range(-360.0..=360.0).suffix("°")); auto.add("dialog.comp.shutterPhase", r.rect, "Shutter Phase"); ui.end_row(); - ui.label("Samples Per Frame:"); + ui.label(tr("Samples Per Frame:")); let r = ui.add(egui::DragValue::new(&mut d.samples).range(2..=64)); auto.add("dialog.comp.samples", r.rect, "Samples Per Frame"); ui.end_row(); - ui.label("Adaptive Sample Limit:"); + ui.label(tr("Adaptive Sample Limit:")); let r = ui.add(egui::DragValue::new(&mut d.adaptive_limit).range(16..=256)); auto.add("dialog.comp.adaptiveLimit", r.rect, "Adaptive Sample Limit"); ui.end_row(); @@ -338,11 +339,11 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr } Tab::Renderer => { ui.horizontal(|ui| { - ui.label("Renderer:"); + 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| { - ui.selectable_value(&mut d.advanced_3d, false, "Classic 3D"); - ui.selectable_value(&mut d.advanced_3d, true, "Advanced 3D"); + 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"); }); @@ -351,12 +352,12 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr ui.add_space(14.0); ui.horizontal(|ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)); + let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); auto.add("dialog.comp.ok", r.rect, "OK"); if r.clicked() { out = Some(params(d)); } - let r = ui.button("Cancel"); + let r = ui.button(tr("Cancel")); auto.add("dialog.comp.cancel", r.rect, "Cancel"); if r.clicked() { out = Some(serde_json::Value::Null); diff --git a/crates/ui-egui/src/panels/content_fill_panel.rs b/crates/ui-egui/src/panels/content_fill_panel.rs index 4b87d678..24bd8854 100644 --- a/crates/ui-egui/src/panels/content_fill_panel.rs +++ b/crates/ui-egui/src/panels/content_fill_panel.rs @@ -4,6 +4,7 @@ //! live in `EditorState::content_fill` (`contentFill.set`); the button runs //! `contentFill.generate` as a background job (Window ▸ Progress). +use crate::i18n::tr; use egui::{Align2, Rect, pos2, vec2}; use serde_json::json; @@ -25,7 +26,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let comp = app.session.active_comp_arc(); let layer = comp.as_ref().and_then(|c| app.session.state.selected_layers.first().and_then(|l| c.layer(*l)).cloned()); // Fill Target. - p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, "Fill Target", Tokens::semibold(12.0), t.text); + p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, tr("Fill Target"), Tokens::semibold(12.0), t.text); y += ROW - 6.0; let target = match &layer { Some(l) => { diff --git a/crates/ui-egui/src/panels/delete_items.rs b/crates/ui-egui/src/panels/delete_items.rs index dbda93d7..7ccc936a 100644 --- a/crates/ui-egui/src/panels/delete_items.rs +++ b/crates/ui-egui/src/panels/delete_items.rs @@ -5,6 +5,7 @@ //! //! Automation ids: `dialog.deleteItems.delete`, `dialog.deleteItems.cancel`. +use crate::i18n::tr; use egui::{Color32, vec2}; use serde_json::Value; @@ -56,15 +57,15 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ); ui.label(egui::RichText::new(text).color(t.tab_text_active)); ui.add_space(4.0); - ui.label(egui::RichText::new("You can undo this.").color(t.text_dim).size(11.0)); + ui.label(egui::RichText::new(tr("You can undo this.")).color(t.text_dim).size(11.0)); ui.add_space(18.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let r = ui.add(egui::Button::new(egui::RichText::new(" Delete ").color(Color32::WHITE)).fill(t.danger)); + let r = ui.add(egui::Button::new(egui::RichText::new(tr(" Delete ")).color(Color32::WHITE)).fill(t.danger)); app.auto.add("dialog.deleteItems.delete", r.rect, "Delete"); if r.clicked() { answer = Some(true); } - let r = ui.button("Cancel"); + let r = ui.button(tr("Cancel")); app.auto.add("dialog.deleteItems.cancel", r.rect, "Cancel"); if r.clicked() { answer = Some(false); diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 8e707d36..f123906b 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -1,6 +1,7 @@ //! Modal dialogs: About (community links, Contributors and Models credits), New Composition / //! Composition Settings, Solid Settings and the command palette (Camera/Light Settings live in `dialogs_3d`). +use crate::i18n::{tr, tr_args}; use effectcraft_engine::project::{ItemId, ItemKind, LayerId, LayerSource}; use egui::{Align2, Color32, Rect, Sense, pos2, vec2}; use serde_json::{Value, json}; @@ -232,9 +233,9 @@ fn about(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { } ui.add_space(8.0); ui.horizontal(|ui| { - ui.label(egui::RichText::new("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)").small().color(t.text_faint)); + ui.label(egui::RichText::new(tr("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)")).small().color(t.text_faint)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let r = ui.button("Close"); + let r = ui.button(tr("Close")); app.auto.add("about.close", r.rect, "Close"); if r.clicked() { close = true; @@ -265,7 +266,7 @@ fn about_main(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, cmd: &mut t.text_dim, ); ui.add_space(12.0); - ui.label("A clean-room, open-source compositor for motion graphics and visual effects: native on macOS, Windows and Linux, and in the browser. Part of the ArtCraft family of creative apps."); + ui.label(tr("A clean-room, open-source compositor for motion graphics and visual effects: native on macOS, Windows and Linux, and in the browser. Part of the ArtCraft family of creative apps.")); ui.add_space(14.0); let links: [(Icon, &str, &str, &'static str); 4] = [ (Icon::Chat, "Join the ArtCraft Discord", effectcraft_engine::links::DISCORD, "help.discord"), @@ -349,23 +350,23 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let auto = &mut app.auto; modal(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, |ui| { ui.horizontal(|ui| { - ui.label("Name:"); + ui.label(tr("Name:")); let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(320.0)); auto.add("dialog.solid.name", r.rect, "Name"); }); ui.add_space(8.0); - ui.label(egui::RichText::new("Size").strong()); + ui.label(egui::RichText::new(tr("Size")).strong()); egui::Grid::new("solid-grid").num_columns(2).spacing([14.0, 8.0]).show(ui, |ui| { let (ow, oh) = (size[0], size[1]); - ui.label("Width:"); + ui.label(tr("Width:")); ui.horizontal(|ui| { let r = ui.add(egui::DragValue::new(&mut size[0]).range(1..=30000).suffix(" px")); auto.add("dialog.solid.width", r.rect, "Width"); - let r = ui.checkbox(&mut lock, format!("Lock Aspect Ratio to {}", super::comp_settings::aspect_label(ow as f64, oh as f64))); + let r = ui.checkbox(&mut lock, tr_args("Lock Aspect Ratio to {}", &[&(super::comp_settings::aspect_label(ow as f64, oh as f64))])); auto.add("dialog.solid.lockAspect", r.rect, "Lock Aspect Ratio"); }); ui.end_row(); - ui.label("Height:"); + ui.label(tr("Height:")); let r = ui.add(egui::DragValue::new(&mut size[1]).range(1..=30000).suffix(" px")); auto.add("dialog.solid.height", r.rect, "Height"); ui.end_row(); @@ -376,7 +377,7 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { size[0] = ((size[1] as f64) * ow as f64 / oh as f64).round().max(1.0) as u32; } } - ui.label("Pixel Aspect Ratio:"); + ui.label(tr("Pixel Aspect Ratio:")); let cur = super::comp_settings::PIXEL_ASPECTS .iter() .find(|(_, v)| (v - par).abs() < 1e-3) @@ -403,14 +404,14 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { .color(Color32::GRAY), ); ui.add_space(4.0); - let r = ui.button("Make Comp Size"); + let r = ui.button(tr("Make Comp Size")); auto.add("dialog.solid.makeCompSize", r.rect, "Make Comp Size"); if r.clicked() { size = [comp_size.0, comp_size.1]; } ui.add_space(8.0); ui.horizontal(|ui| { - ui.label("Color:"); + ui.label(tr("Color:")); let r = crate::widgets::srgb_color_button(ui, &mut color); auto.add("dialog.solid.color", r.rect, "Color"); }); @@ -421,15 +422,15 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { .on_disabled_hover_text("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("This change will create a new solid for this layer.").color(t.text_dim).size(11.0)); + ui.label(egui::RichText::new(tr("This change will create a new solid for this layer.")).color(t.text_dim).size(11.0)); } } ui.add_space(14.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)); + let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); auto.add("dialog.solid.ok", r.rect, "OK"); ok = r.clicked(); - let r = ui.button("Cancel"); + let r = ui.button(tr("Cancel")); auto.add("dialog.solid.cancel", r.rect, "Cancel"); close = r.clicked(); }); @@ -493,7 +494,7 @@ fn palette(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { .take(14) .collect(); modal(ctx, "Command Palette", vec2(560.0, 470.0), t, |ui| { - let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text("Type a command or effect…").desired_width(f32::INFINITY).font(Tokens::ui(14.0))); + let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tr("Type a command or effect…")).desired_width(f32::INFINITY).font(Tokens::ui(14.0))); r.request_focus(); ui.add_space(8.0); let sel = app.dialog_state.palette_sel.min(matches.len().saturating_sub(1)); diff --git a/crates/ui-egui/src/panels/dialogs_3d.rs b/crates/ui-egui/src/panels/dialogs_3d.rs index 87fc1661..485e2e8b 100644 --- a/crates/ui-egui/src/panels/dialogs_3d.rs +++ b/crates/ui-egui/src/panels/dialogs_3d.rs @@ -2,6 +2,7 @@ //! Settings, Layer Settings on a camera or light). OK runs `layer.newCamera` / `layer.newLight` //! or `layer.cameraSettings` / `layer.lightSettings` with the dialog's values. +use crate::i18n::tr; use effectcraft_engine::project::{AutoOrient, LayerSource, LightKind}; use effectcraft_engine::render::three_d::camera::{PRESETS, angle_of_view, default_aperture, focal_for_zoom, zoom_for_focal}; use egui::{Color32, vec2}; @@ -144,12 +145,12 @@ pub fn open_light(app: &mut EffectcraftApp, layer: Option) -> Result<(), St fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (egui::Rect, egui::Rect) { let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let b = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)); + let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); r.0 = b.rect; if b.clicked() { *ok = true; } - let c = ui.button("Cancel"); + let c = ui.button(tr("Cancel")); r.1 = c.rect; if c.clicked() { *close = true; @@ -165,16 +166,16 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING); super::dialogs::modal(ctx, "Camera Settings", vec2(520.0, 420.0), t, |ui| { egui::Grid::new("cam-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| { - ui.label("Type"); + ui.label(tr("Type")); egui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| { - ui.selectable_value(&mut d.two_node, true, "Two-Node Camera"); - ui.selectable_value(&mut d.two_node, false, "One-Node Camera"); + ui.selectable_value(&mut d.two_node, true, tr("Two-Node Camera")); + ui.selectable_value(&mut d.two_node, false, tr("One-Node Camera")); }); ui.end_row(); - ui.label("Name"); + ui.label(tr("Name")); ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(240.0)); ui.end_row(); - ui.label("Preset"); + ui.label(tr("Preset")); let focal = focal_for_zoom(w, d.zoom); let cur = PRESETS.iter().find(|(_, f)| (f - focal).abs() < 1e-6).map(|(n, _)| *n).unwrap_or("Custom"); egui::ComboBox::from_id_salt("cam-preset").selected_text(cur).show_ui(ui, |ui| { @@ -185,7 +186,7 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { } }); ui.end_row(); - ui.label("Zoom"); + ui.label(tr("Zoom")); ui.horizontal(|ui| { ui.add(egui::DragValue::new(&mut d.zoom).range(1.0..=100000.0).speed(1.0).suffix(" px")); let mut aov = angle_of_view(w, d.zoom); @@ -198,19 +199,19 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { } }); ui.end_row(); - ui.label("Depth of Field"); - ui.checkbox(&mut d.dof, "Enable Depth of Field"); + ui.label(tr("Depth of Field")); + ui.checkbox(&mut d.dof, tr("Enable Depth of Field")); ui.end_row(); - ui.label("Focus Distance"); + ui.label(tr("Focus Distance")); ui.horizontal(|ui| { ui.add_enabled(!d.lock_to_zoom, egui::DragValue::new(&mut d.focus).range(0.0..=100000.0).suffix(" px")); - ui.checkbox(&mut d.lock_to_zoom, "Lock to Zoom"); + ui.checkbox(&mut d.lock_to_zoom, tr("Lock to Zoom")); }); ui.end_row(); - ui.label("Aperture"); + ui.label(tr("Aperture")); ui.add(egui::DragValue::new(&mut d.aperture).range(0.0..=10000.0).speed(0.1).suffix(" px")); ui.end_row(); - ui.label("Blur Level"); + ui.label(tr("Blur Level")); ui.add(egui::DragValue::new(&mut d.blur).range(0.0..=10000.0).suffix(" %")); ui.end_row(); }); @@ -256,33 +257,33 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING); super::dialogs::modal(ctx, "Light Settings", vec2(480.0, 470.0), t, |ui| { egui::Grid::new("light-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| { - ui.label("Name"); + ui.label(tr("Name")); ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0)); ui.end_row(); - ui.label("Light Type"); + ui.label(tr("Light Type")); egui::ComboBox::from_id_salt("light-type").selected_text(KINDS[d.kind.min(3)].label()).show_ui(ui, |ui| { for (i, k) in KINDS.iter().enumerate() { ui.selectable_value(&mut d.kind, i, k.label()); } }); ui.end_row(); - ui.label("Color"); + ui.label(tr("Color")); ui.color_edit_button_rgb(&mut d.color); ui.end_row(); - ui.label("Intensity"); + ui.label(tr("Intensity")); ui.add(egui::DragValue::new(&mut d.intensity).range(-10000.0..=10000.0).suffix(" %")); ui.end_row(); let kind = KINDS[d.kind.min(3)]; if kind == LightKind::Spot { - ui.label("Cone Angle"); + ui.label(tr("Cone Angle")); ui.add(egui::DragValue::new(&mut d.cone_angle).range(0.0..=180.0).suffix("°")); ui.end_row(); - ui.label("Cone Feather"); + ui.label(tr("Cone Feather")); ui.add(egui::DragValue::new(&mut d.cone_feather).range(0.0..=100.0).suffix(" %")); ui.end_row(); } if kind != LightKind::Ambient { - ui.label("Falloff"); + ui.label(tr("Falloff")); egui::ComboBox::from_id_salt("light-falloff").selected_text(FALLOFFS[d.falloff.min(2)]).show_ui(ui, |ui| { for (i, f) in FALLOFFS.iter().enumerate() { ui.selectable_value(&mut d.falloff, i, *f); @@ -290,26 +291,26 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { }); ui.end_row(); if d.falloff > 0 { - ui.label("Radius"); + ui.label(tr("Radius")); ui.add(egui::DragValue::new(&mut d.radius).range(0.0..=100000.0).suffix(" px")); ui.end_row(); - ui.label("Falloff Distance"); + ui.label(tr("Falloff Distance")); ui.add(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px")); ui.end_row(); } - ui.label("Shadows"); - ui.checkbox(&mut d.casts_shadows, "Casts Shadows"); + ui.label(tr("Shadows")); + ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows")); ui.end_row(); - ui.label("Shadow Darkness"); + ui.label(tr("Shadow Darkness")); ui.add_enabled(d.casts_shadows, egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %")); ui.end_row(); - ui.label("Shadow Diffusion"); + ui.label(tr("Shadow Diffusion")); ui.add_enabled(d.casts_shadows, egui::DragValue::new(&mut d.shadow_diffusion).range(0.0..=1000.0).suffix(" px")); ui.end_row(); } }); ui.add_space(14.0); - ui.label(egui::RichText::new("Note: Shadows are only cast from layers with Cast Shadows enabled.").font(Tokens::ui(11.0)).color(t.text_dim)); + ui.label(egui::RichText::new(tr("Note: Shadows are only cast from layers with Cast Shadows enabled.")).font(Tokens::ui(11.0)).color(t.text_dim)); ui.add_space(8.0); rects = buttons(ui, t, &mut ok, &mut close); }); diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index a1223b91..0d8f983b 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -292,7 +292,9 @@ fn prop_row( if matches!(prop.ui, ParamUi::Point | ParamUi::Point3) { let cr = Rect::from_center_size(pos2(x + 8.0, cy), vec2(18.0, 18.0)); let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == "point"); - if fw::crosshair_button(ui, cr, active, egui::Id::new(("ec-xh", uid)), &t).on_hover_text("Click, then click in the Composition panel").clicked() + if fw::crosshair_button(ui, cr, active, egui::Id::new(("ec-xh", uid)), &t) + .on_hover_text(crate::i18n::tr("Click, then click in the Composition panel")) + .clicked() { toggle_pick(app, "point", layer, prop); } @@ -320,7 +322,7 @@ fn prop_row( let er = Rect::from_center_size(pos2(sr.max.x + 14.0, cy), vec2(18.0, 18.0)); let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == "color"); if fw::eyedropper_button(ui, er, active, egui::Id::new(("ec-eye", uid)), &t) - .on_hover_text("Eyedropper: click, then click in the Composition panel") + .on_hover_text(crate::i18n::tr("Eyedropper: click, then click in the Composition panel")) .clicked() { toggle_pick(app, "color", layer, prop); @@ -724,7 +726,7 @@ fn channel_popup( auto: &str, ) -> Option { let t = app.tokens; - p.text(pos2(x, cy), Align2::LEFT_CENTER, "Channel:", Tokens::ui(12.0), t.text_dim); + p.text(pos2(x, cy), Align2::LEFT_CENTER, crate::i18n::tr("Channel:"), Tokens::ui(12.0), t.text_dim); let dr = Rect::from_min_size(pos2(x + 58.0, cy - 9.0), vec2(80.0, 18.0)); let pop = id.with("pop"); if widgets::dropdown(ui, dr, names.get(cur).copied().unwrap_or(""), &t, id).clicked() { @@ -777,7 +779,7 @@ fn curves_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, }; let rr = Rect::from_min_size(pos2(mr.max.x + 8.0, cy - 9.0), vec2(44.0, 18.0)); let rresp = ui.interact(rr, egui::Id::new(("ec-creset", euid)), Sense::click()); - p.text(rr.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim }); + p.text(rr.center(), Align2::CENTER_CENTER, crate::i18n::tr("Reset"), Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim }); app.auto.add(&format!("effectControls.effect.{euid}.curves.reset"), rr, "Reset curve"); if rresp.clicked() { actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": prop.uid, "value": fw::CurvePoints::identity().format()}))); @@ -846,7 +848,7 @@ fn levels_editor( app.ui.fx_levels_channel.insert(euid, n); } } else { - p.text(pos2(x0, cy), Align2::LEFT_CENTER, "Histogram", Tokens::ui(12.0), t.text_dim); + p.text(pos2(x0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Histogram"), Tokens::ui(12.0), t.text_dim); } let w = (r.width() - 72.0).clamp(120.0, 256.0); let hr = Rect::from_min_size(pos2(x0 + 8.0, r.min.y + 34.0), vec2(w, 80.0)); @@ -911,7 +913,7 @@ fn levels_editor( fn histogram_only(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, g: &PropGroup, r: Rect) { let t = app.tokens; let x0 = r.min.x + 30.0; - p.text(pos2(x0, r.min.y + 12.0), Align2::LEFT_CENTER, "Histogram", Tokens::ui(12.0), t.text_dim); + p.text(pos2(x0, r.min.y + 12.0), Align2::LEFT_CENTER, crate::i18n::tr("Histogram"), Tokens::ui(12.0), t.text_dim); let w = (r.width() - 72.0).clamp(120.0, 256.0); let hr = Rect::from_min_size(pos2(x0 + 8.0, r.min.y + 24.0), vec2(w, 80.0)); match frame_histogram(app, ui.ctx()) { @@ -934,7 +936,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let p = ui.painter().with_clip_rect(rect); let ctx = ui.ctx().clone(); let Some(layer) = selected_layer(app) else { - p.text(rect.center(), Align2::CENTER_CENTER, "Select a layer to see its effects", Tokens::ui(12.0), t.text_faint); + p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Select a layer to see its effects"), Tokens::ui(12.0), t.text_faint); return; }; let Some(cid) = app.session.active_comp_id() else { return }; @@ -1000,7 +1002,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { app.auto.add(&format!("effectControls.effect.{}.name", g.uid), name, &g.name); let reset = Rect::from_min_size(pos2(r.max.x - 96.0, r.min.y + 4.0), vec2(40.0, 18.0)); let rresp = ui.interact(reset, egui::Id::new(("ec-reset", g.uid)), Sense::click()); - bp.text(reset.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim }); + bp.text(reset.center(), Align2::CENTER_CENTER, crate::i18n::tr("Reset"), Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim }); app.auto.add(&format!("effectControls.effect.{}.reset", g.uid), reset, "Reset"); if rresp.clicked() { actions.push(("effect.reset".into(), json!({"layer": layer.id.0, "effect": g.uid}))); @@ -1008,7 +1010,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // About…: the effect's name and category (AE's About dialog). let about = Rect::from_min_size(pos2(r.max.x - 50.0, r.min.y + 4.0), vec2(44.0, 18.0)); let aresp = ui.interact(about, egui::Id::new(("ec-about", g.uid)), Sense::click()); - bp.text(about.center(), Align2::CENTER_CENTER, "About...", Tokens::ui(11.5), if aresp.hovered() { t.hot_text } else { t.text_dim }); + bp.text(about.center(), Align2::CENTER_CENTER, crate::i18n::tr("About..."), Tokens::ui(11.5), if aresp.hovered() { t.hot_text } else { t.text_dim }); app.auto.add(&format!("effectControls.effect.{}.about", g.uid), about, "About"); let apop = egui::Id::new(("ec-about-pop", g.uid)); if aresp.clicked() { diff --git a/crates/ui-egui/src/panels/effects_presets.rs b/crates/ui-egui/src/panels/effects_presets.rs index c5d48095..7b68ba7f 100644 --- a/crates/ui-egui/src/panels/effects_presets.rs +++ b/crates/ui-egui/src/panels/effects_presets.rs @@ -5,6 +5,7 @@ //! selected layers; dragging an effect onto a layer (timeline, viewer, Effect Controls) applies //! it there. +use crate::i18n::tr; use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2}; use serde_json::json; @@ -276,7 +277,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Contents menu. let mr = Rect::from_center_size(pos2(rect.max.x - 18.0, sr.center().y), vec2(22.0, 22.0)); let mpop = egui::Id::new("fx-contents-menu"); - if widgets::icon_button(ui, mr, Icon::Hamburger, false, &t, egui::Id::new("fx-menu")).on_hover_text("Effects & Presets options").clicked() { + if widgets::icon_button(ui, mr, Icon::Hamburger, false, &t, egui::Id::new("fx-menu")).on_hover_text(tr("Effects & Presets options")).clicked() { widgets::open_popup(ui, mpop); } app.auto.add("effects.menu", mr, "Effects & Presets options"); @@ -394,7 +395,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } } if y == list.min.y - scroll.offset && searching { - lp.text(pos2(list.center().x, list.min.y + 24.0), Align2::CENTER_CENTER, "No matching effects or presets", Tokens::ui(12.0), t.text_faint); + lp.text(pos2(list.center().x, list.min.y + 24.0), Align2::CENTER_CENTER, tr("No matching effects or presets"), Tokens::ui(12.0), t.text_faint); } let content = y + scroll.offset - list.min.y; scroll.end(ui, &mut app.auto, "effects.scroll", content, &t); diff --git a/crates/ui-egui/src/panels/essential.rs b/crates/ui-egui/src/panels/essential.rs index dd014d31..26d458d4 100644 --- a/crates/ui-egui/src/panels/essential.rs +++ b/crates/ui-egui/src/panels/essential.rs @@ -20,6 +20,7 @@ //! id (`essential.primary`, `essential.solo`, `essential.name`, `essential.control..value`, //! `essential.instance..value`, `essential.export`…). +use crate::i18n::{tr, tr_args}; use effectcraft_engine::keyframe::Value as KV; use effectcraft_engine::project::essential::{self, ControlType, EgControl, EgKind}; use effectcraft_engine::project::{ItemId, ItemKind, LayerSource, ParamUi, Property}; @@ -61,7 +62,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { let mut actions: Actions = vec![]; let mut invoke: Vec<(String, Value)> = vec![]; let Some(cid) = primary(app) else { - ui.label(RichText::new("Open a composition to build Essential Graphics").color(t.text_faint)); + ui.label(RichText::new(tr("Open a composition to build Essential Graphics")).color(t.text_faint)); return (actions, invoke); }; let project = app.session.project.clone(); @@ -69,7 +70,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { // ---- Primary + Solo. ui.horizontal(|ui| { - ui.label(RichText::new("Primary:").color(t.text_dim)); + ui.label(RichText::new(tr("Primary:")).color(t.text_dim)); let r = egui::ComboBox::from_id_salt("eg-primary").selected_text(&comp_name).width(150.0).show_ui(ui, |ui| { for (id, _) in project.comps() { let name = project.item(*id).map(|i| i.name.clone()).unwrap_or_default(); @@ -81,7 +82,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { app.auto.add("essential.primary", r.response.rect, &comp_name); }); let solo = app.session.state.essential_solo; - let r = ui.add(egui::Button::new("Solo Supported Properties").selected(solo)); + let r = ui.add(egui::Button::new(tr("Solo Supported Properties")).selected(solo)); app.auto.add("essential.solo", r.rect, "Solo Supported Properties"); if r.clicked() { actions.push(("essential.soloSupported".into(), json!({"on": !solo}))); @@ -91,7 +92,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { let shown_name = if eg.name.is_empty() { comp_name.clone() } else { eg.name.clone() }; let name_id = egui::Id::new("eg-name-buf"); let mut name: String = ctx.data(|d| d.get_temp(name_id)).unwrap_or_else(|| shown_name.clone()); - let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text("Template name").desired_width(f32::INFINITY)); + let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text(tr("Template name")).desired_width(f32::INFINITY)); app.auto.add("essential.name", r.rect, &shown_name); if r.has_focus() { ctx.data_mut(|d| d.insert_temp(name_id, name.clone())); @@ -107,7 +108,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { let list = ui.scope(|ui| { ui.set_min_height(60.0); if eg.controls.is_empty() { - ui.label(RichText::new("Drag properties here from the timeline,\nor use Animation > Add Property to Essential Graphics.").color(t.text_faint)); + ui.label(RichText::new(tr("Drag properties here from the timeline,\nor use Animation > Add Property to Essential Graphics.")).color(t.text_faint)); } controls(app, ui, cid, &eg.controls, None, &mut actions); }); @@ -128,12 +129,12 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { // ---- Bottom buttons. ui.horizontal_wrapped(|ui| { - let r = ui.menu_button("Add Formatting", |ui| { - if ui.button("Add Group").clicked() { + let r = ui.menu_button(tr("Add Formatting"), |ui| { + if ui.button(tr("Add Group")).clicked() { actions.push(("essential.addGroup".into(), json!({"comp": cid.0}))); ui.close(); } - if ui.button("Add Comment").clicked() { + if ui.button(tr("Add Comment")).clicked() { actions.push(("essential.addComment".into(), json!({"comp": cid.0}))); ui.close(); } @@ -142,7 +143,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { // The selected property as a Font control (Source Text) or a uniform Scale slider. let has_sel = !app.session.state.selected_props.is_empty(); let r = ui.add_enabled_ui(has_sel, |ui| { - ui.menu_button("Add Selected As", |ui| { + ui.menu_button(tr("Add Selected As"), |ui| { for (label, as_type) in [("Font Control", "font"), ("Uniform Scale", "scale")] { if ui.button(label).clicked() { actions.push(("essential.addProperty".into(), json!({"as": as_type}))); @@ -155,14 +156,14 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { let footage_layer = app.session.active_comp().and_then(|c| { app.session.state.selected_layers.iter().filter_map(|l| c.layer(*l)).find(|l| matches!(l.source, LayerSource::Footage { .. })).map(|l| l.id.0) }); - let r = ui.add_enabled(footage_layer.is_some(), egui::Button::new("Media Replacement")); + let r = ui.add_enabled(footage_layer.is_some(), egui::Button::new(tr("Media Replacement"))); app.auto.add("essential.addMedia", r.rect, "Add Media Replacement"); if r.clicked() && let Some(l) = footage_layer { actions.push(("essential.addMedia".into(), json!({"layer": l}))); } - let r = ui.add_enabled(!eg.controls.is_empty(), egui::Button::new("Export Template…")); + let r = ui.add_enabled(!eg.controls.is_empty(), egui::Button::new(tr("Export Template…"))); app.auto.add("essential.export", r.rect, "Export Template"); if r.clicked() { invoke.push(("essential.exportTemplate".into(), json!({"comp": cid.0}))); @@ -234,7 +235,7 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg } } None => { - ui.label(RichText::new("(missing)").color(t.danger)); + ui.label(RichText::new(tr("(missing)")).color(t.danger)); } } link_menu(app, ui, cid, c, group, actions); @@ -243,10 +244,10 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg name_label(app, ui, cid, c, RichText::new(&c.name), actions); let item = essential::source_media(&app.session.project, cid, c); let name = item.and_then(|i| app.session.project.item(i)).map(|i| i.name.clone()).unwrap_or_default(); - ui.label(RichText::new(format!("▣ {name}")).color(t.text_dim)); + ui.label(RichText::new(tr_args("▣ {}", &[&name])).color(t.text_dim)); } } - let r = ui.add(egui::Button::new(RichText::new("×").size(10.0)).small().frame(false)).on_hover_text("Remove"); + let r = ui.add(egui::Button::new(RichText::new("×").size(10.0)).small().frame(false)).on_hover_text(tr("Remove")); app.auto.add(&format!("essential.control.{}.remove", c.id), r.rect, "Remove"); if r.clicked() { actions.push(("essential.remove".into(), json!({"comp": cid.0, "control": c.id}))); @@ -270,16 +271,16 @@ fn link_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, c: &EgCon }; let has_sel = !app.session.state.selected_props.is_empty(); let r = ui.menu_button(RichText::new("⋯").size(10.0), |ui| { - if ui.button("Add Mirror").clicked() { + if ui.button(tr("Add Mirror")).clicked() { actions.push(("essential.addMirror".into(), json!({"comp": cid.0, "control": c.id, "group": group}))); ui.close(); } - if ui.add_enabled(has_sel, egui::Button::new("Link Selected Property")).clicked() { + if ui.add_enabled(has_sel, egui::Button::new(tr("Link Selected Property"))).clicked() { actions.push(("essential.linkProperty".into(), json!({"comp": cid.0, "control": c.id}))); ui.close(); } for (layer, prop, name) in &links { - if ui.button(format!("Unlink {name}")).clicked() { + if ui.button(tr_args("Unlink {}", &[&name])).clicked() { actions.push(("essential.unlinkProperty".into(), json!({"comp": cid.0, "control": c.id, "layer": layer, "prop": prop}))); ui.close(); } @@ -460,7 +461,7 @@ fn instance(app: &mut EffectcraftApp, ui: &mut egui::Ui, actions: &mut Actions) let over = essential::overridden(&layer); let lt = layer.layer_time(app.session.time()); ui.separator(); - ui.label(RichText::new(format!("Essential Properties — {}", layer.name)).strong()); + ui.label(RichText::new(tr_args("Essential Properties — {}", &[&layer.name])).strong()); let group = essential::group(&layer).cloned().unwrap_or_else(|| effectcraft_engine::project::PropGroup::new(0, "", "")); for c in eg.flat() { let m = essential::match_id(c.id); @@ -527,12 +528,12 @@ fn instance(app: &mut EffectcraftApp, ui: &mut egui::Ui, actions: &mut Actions) } ui.horizontal(|ui| { let any = !over.is_empty(); - let r = ui.add_enabled(any, egui::Button::new("Push All to Comp")); + let r = ui.add_enabled(any, egui::Button::new(tr("Push All to Comp"))); app.auto.add("essential.instance.pushAll", r.rect, "Push Override Values to Source"); if r.clicked() { actions.push(("essential.pushToComp".into(), json!({"layer": layer.id.0}))); } - let r = ui.add_enabled(any, egui::Button::new("Revert All")); + let r = ui.add_enabled(any, egui::Button::new(tr("Revert All"))); app.auto.add("essential.instance.revertAll", r.rect, "Revert All"); if r.clicked() { actions.push(("essential.revert".into(), json!({"layer": layer.id.0}))); diff --git a/crates/ui-egui/src/panels/expr_bar.rs b/crates/ui-egui/src/panels/expr_bar.rs index 09d4a608..fc985d15 100644 --- a/crates/ui-egui/src/panels/expr_bar.rs +++ b/crates/ui-egui/src/panels/expr_bar.rs @@ -3,6 +3,7 @@ //! Clicking the message selects the layer and reveals the property in the timeline. //! Automation ids: `viewer.exprErrors`, `viewer.exprErrors.prev`, `viewer.exprErrors.next`. +use crate::i18n::tr; use effectcraft_engine::commands::expr_tools::{self, ExprError}; use effectcraft_engine::project::ItemId; use egui::{Align2, Color32, Rect, Sense, pos2, vec2}; @@ -77,7 +78,7 @@ pub fn draw(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, cid: ItemId let comp_name = app.session.project.item(cid).map(|it| it.name.clone()).unwrap_or_default(); let e = c.errors[i].clone(); let msg_rect = Rect::from_min_max(pos2(rect.min.x + 26.0, rect.min.y), pos2(prev.min.x - 6.0, rect.max.y)); - let resp = ui.interact(msg_rect, egui::Id::new("expr-bar-msg"), Sense::click()).on_hover_text("Click to reveal the expression in the timeline"); + let resp = ui.interact(msg_rect, egui::Id::new("expr-bar-msg"), Sense::click()).on_hover_text(tr("Click to reveal the expression in the timeline")); p.with_clip_rect(msg_rect).text( pos2(msg_rect.min.x, rect.center().y), Align2::LEFT_CENTER, diff --git a/crates/ui-egui/src/panels/flowchart.rs b/crates/ui-egui/src/panels/flowchart.rs index 2ba345cf..f59d3598 100644 --- a/crates/ui-egui/src/panels/flowchart.rs +++ b/crates/ui-egui/src/panels/flowchart.rs @@ -6,6 +6,7 @@ //! [`build`] is pure and cycle-safe (a comp nested in itself is drawn once, with an edge back), //! so agents get the same graph through `flowchart.graph`. +use crate::i18n::tr_args; use std::collections::{BTreeMap, BTreeSet}; use effectcraft_engine::project::{ItemId, ItemKind, LayerSource, Project}; @@ -256,7 +257,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let bar = Rect::from_min_size(rect.min, vec2(rect.width(), 30.0)); p.rect_filled(bar, 0.0, t.panel_bg); let root_name = app.session.project.item(rid).map(|i| i.name.clone()).unwrap_or_default(); - p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, format!("Flowchart: {root_name}"), Tokens::medium(12.0), t.text); + p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, tr_args("Flowchart: {}", &[&root_name]), Tokens::medium(12.0), t.text); let mut x = bar.max.x - 8.0; let mut cmds: Vec<(String, Value)> = vec![]; let o = app.ui.flowchart.clone(); @@ -363,7 +364,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { _ => {} } } - resp.on_hover_text(format!("{} — double-click to open, {}-click to reveal", n.label, if cfg!(target_os = "macos") { "Cmd" } else { "Ctrl" })); + resp.on_hover_text(tr_args("{} — double-click to open, {}-click to reveal", &[&n.label, &(if cfg!(target_os = "macos") { "Cmd" } else { "Ctrl" })])); } for (id, params) in cmds { if let Err(e) = crate::menus::invoke(app, &ctx, &id, params) { diff --git a/crates/ui-egui/src/panels/footage_panel.rs b/crates/ui-egui/src/panels/footage_panel.rs index 57f2f500..a8651464 100644 --- a/crates/ui-egui/src/panels/footage_panel.rs +++ b/crates/ui-egui/src/panels/footage_panel.rs @@ -3,6 +3,7 @@ //! Insert Edit — that cut the marked range into the active composition at its current time. //! Everything runs `footage.*` commands (state in `EditorState::footage_panel`). +use crate::i18n::{tr, tr_args}; use effectcraft_engine::project::{FootageKind, ItemKind}; use effectcraft_engine::time::Tick; use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2}; @@ -18,7 +19,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let p = ui.painter().with_clip_rect(rect); p.rect_filled(rect, 0.0, t.pasteboard); let Some(view) = app.session.state.footage_panel.clone() else { - p.text(rect.center(), Align2::CENTER_CENTER, "Double-click footage in the Project panel to open it here", Tokens::ui(12.0), t.text_faint); + p.text(rect.center(), Align2::CENTER_CENTER, tr("Double-click footage in the Project panel to open it here"), Tokens::ui(12.0), t.text_faint); app.auto.add("footage.empty", rect, "Footage panel (empty)"); return; }; @@ -38,7 +39,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Title strip. let top = Rect::from_min_size(rect.min, vec2(rect.width(), 24.0)); p.rect_filled(top, 0.0, t.panel_bg); - p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, format!("Footage: {}", item.name), Tokens::semibold(12.0), t.text); + p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, tr_args("Footage: {}", &[&item.name]), Tokens::semibold(12.0), t.text); app.auto.add("footage.title", top, &item.name); // Bottom: ruler and controls. // Rows, bottom up: Edit Target buttons, transport, ruler. @@ -73,7 +74,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let c = so.color; p.rect_filled(area.shrink(20.0), 0.0, Color32::from_rgb((c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8)); } else { - p.text(area.center(), Align2::CENTER_CENTER, "No video", Tokens::ui(12.0), t.text_faint); + p.text(area.center(), Align2::CENTER_CENTER, tr("No video"), Tokens::ui(12.0), t.text_faint); } // Ruler: ticks, In/Out span, CTI; click or drag to scrub. let x_of = |tt: Tick| ruler.min.x + (tt.seconds() / dur.seconds().max(1e-9)) as f32 * ruler.width(); @@ -139,7 +140,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let ey = edit_row.min.y + 5.0; let bx = (edit_row.max.x - 238.0).max(edit_row.min.x + 10.0); if !target.is_empty() && bx > edit_row.min.x + 140.0 { - p.text(pos2(bx - 8.0, edit_row.center().y), Align2::RIGHT_CENTER, format!("Edit Target: {target}"), Tokens::ui(11.0), t.text_dim); + p.text(pos2(bx - 8.0, edit_row.center().y), Align2::RIGHT_CENTER, tr_args("Edit Target: {}", &[&target]), Tokens::ui(11.0), t.text_dim); } if kit::button(app, ui, Rect::from_min_size(pos2(bx, ey), vec2(110.0, 20.0)), "footage.rippleInsertEdit", "Ripple Insert Edit", false) && comp_ok { kit::exec(app, "footage.rippleInsertEdit", json!({})); diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index aca5f47c..279a437f 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -4,6 +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 egui::{Color32, vec2}; use serde_json::{Map, Value, json}; @@ -761,7 +762,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { FieldKind::Path { value, .. } => { ui.horizontal(|ui| { let r = ui.add(egui::TextEdit::singleline(value).desired_width(150.0)).rect; - let b = ui.button("Browse…"); + let b = ui.button(tr("Browse…")); regs.push((format!("form.field.{}.browse", fl.key), b.rect, "Browse…".into())); if b.clicked() { browse = Some(i); @@ -773,7 +774,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { FieldKind::SavePath(value) => { ui.horizontal(|ui| { let r = ui.add(egui::TextEdit::singleline(value).desired_width(150.0)).rect; - let b = ui.button("Browse…"); + let b = ui.button(tr("Browse…")); regs.push((format!("form.field.{}.browse", fl.key), b.rect, "Browse…".into())); if b.clicked() { browse = Some(i); @@ -813,12 +814,12 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { }); ui.add_space(14.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let b = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)); + let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); regs.push(("form.ok".into(), b.rect, "OK".into())); if b.clicked() { ok = true; } - let c = ui.button("Cancel"); + let c = ui.button(tr("Cancel")); regs.push(("form.cancel".into(), c.rect, "Cancel".into())); if c.clicked() { close = true; @@ -872,7 +873,7 @@ pub fn show_info(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.label(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(" OK ").color(Color32::WHITE)).fill(t.accent)).clicked() { + if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() { close = true; } }); @@ -886,16 +887,16 @@ pub fn show_view_options(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Toke let mut close = false; super::dialogs::modal(ctx, "View Options", vec2(360.0, 330.0), t, |ui| { let v = &mut app.ui.viewer; - ui.checkbox(&mut v.show_layer_controls, "Layer controls"); - ui.checkbox(&mut v.show_masks, "Masks"); - ui.checkbox(&mut v.rulers, "Rulers"); - ui.checkbox(&mut v.guides, "Guides"); - ui.checkbox(&mut v.grid, "Grid"); - ui.checkbox(&mut v.safe_margins, "Title/action safe"); - ui.checkbox(&mut v.transparency_grid, "Transparency grid"); + ui.checkbox(&mut v.show_layer_controls, tr("Layer controls")); + ui.checkbox(&mut v.show_masks, tr("Masks")); + ui.checkbox(&mut v.rulers, tr("Rulers")); + ui.checkbox(&mut v.guides, tr("Guides")); + ui.checkbox(&mut v.grid, tr("Grid")); + ui.checkbox(&mut v.safe_margins, tr("Title/action safe")); + ui.checkbox(&mut v.transparency_grid, tr("Transparency grid")); ui.add_space(12.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)).clicked() { + if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() { close = true; } }); diff --git a/crates/ui-egui/src/panels/fx_editors.rs b/crates/ui-egui/src/panels/fx_editors.rs index c9ff2b79..51b146af 100644 --- a/crates/ui-egui/src/panels/fx_editors.rs +++ b/crates/ui-egui/src/panels/fx_editors.rs @@ -16,6 +16,7 @@ //! Every edit is a `prop.set` engine command (undoable; one drag = one undo step) and every //! handle registers an automation id. +use crate::i18n::{tr, tr_args}; use effectcraft_engine::effects::{ColoramaPalette, OffsetCurve}; use effectcraft_engine::geom::Mat3; use effectcraft_engine::keyframe::Value; @@ -110,7 +111,7 @@ fn tabs( fn reset_link(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, r: Rect, id: &str) -> bool { let t = app.tokens; let resp = ui.interact(r, egui::Id::new(id), Sense::click()); - p.text(r.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if resp.hovered() { t.hot_text } else { t.text_dim }); + p.text(r.center(), Align2::CENTER_CENTER, tr("Reset"), Tokens::ui(11.5), if resp.hovered() { t.hot_text } else { t.text_dim }); app.auto.add(id, r, "Reset"); resp.clicked() } @@ -196,7 +197,7 @@ fn rgb_curves( if let Some(np) = edit.changed { set_str(actions, layer, g, id, np.format(), Some(key)); } - p.text(pos2(gr.min.x, gr.max.y + 11.0), Align2::LEFT_CENTER, "Click adds a point; drag one off to remove", Tokens::ui(10.5), t.text_faint); + p.text(pos2(gr.min.x, gr.max.y + 11.0), Align2::LEFT_CENTER, tr("Click adds a point; drag one off to remove"), Tokens::ui(10.5), t.text_faint); } /// A hue / saturation curve's points (`x,y` in 0..1, y = ½ neutral), any count (empty = flat). @@ -394,7 +395,7 @@ fn huesat_curves( if reset_link(app, ui, p, Rect::from_min_size(pos2(gr.max.x - 44.0, gr.max.y + 4.0), vec2(44.0, 18.0)), &format!("{prefix}.reset")) { set_str(actions, layer, g, id, String::new(), None); } - p.text(pos2(gr.min.x, gr.max.y + 13.0), Align2::LEFT_CENTER, "Click adds a point (½ height = no change)", Tokens::ui(10.5), t.text_faint); + p.text(pos2(gr.min.x, gr.max.y + 13.0), Align2::LEFT_CENTER, tr("Click adds a point (½ height = no change)"), Tokens::ui(10.5), t.text_faint); } // --------------------------------------------------------------------------------------------- @@ -458,7 +459,7 @@ fn colorama_wheel( let pal = if custom { ColoramaPalette::parse(&str_value(layer, ectx, g, "palette")) } else { None }.unwrap_or_else(|| ColoramaPalette::preset(preset)); let x0 = r.min.x + 38.0; let label = if custom { "Custom palette" } else { effectcraft_engine::effects::COLORAMA_PRESETS.get(preset as usize).copied().unwrap_or("Preset") }; - p.text(pos2(x0, r.min.y + 15.0), Align2::LEFT_CENTER, format!("Output Cycle: {label}"), Tokens::ui(11.5), t.text_dim); + p.text(pos2(x0, r.min.y + 15.0), Align2::LEFT_CENTER, tr_args("Output Cycle: {}", &[&label]), Tokens::ui(11.5), t.text_dim); let size = wheel_size(r.width()); let c = pos2(x0 + size / 2.0, r.min.y + 30.0 + size / 2.0); let (outer, inner) = (size / 2.0 - 8.0, size / 2.0 - 26.0); @@ -572,7 +573,13 @@ fn colorama_wheel( widgets::open_popup(ui, pop); } app.auto.add(&format!("{prefix}.color"), sr, "Stop color"); - p.text(pos2(sr.max.x + 8.0, sr.center().y), Align2::LEFT_CENTER, format!("Stop {} at {:.0}°", sel + 1, ph * 360.0), Tokens::ui(11.0), t.text_dim); + p.text( + pos2(sr.max.x + 8.0, sr.center().y), + Align2::LEFT_CENTER, + tr_args("Stop {} at {}°", &[&(sel + 1), &format!("{:.0}", ph * 360.0)]), + Tokens::ui(11.0), + t.text_dim, + ); let mut rgb = [col[0], col[1], col[2]]; if crate::header::color_popup(ui, pop, sr.left_bottom(), &mut rgb) { let mut st = pal.stops.clone(); @@ -687,7 +694,7 @@ fn glow_map(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, laye } p.rect_stroke(strip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Outside); app.auto.add(&format!("{prefix}.colorMap"), strip, "Glow colour map"); - p.text(pos2(strip.max.x + 8.0, strip.center().y), Align2::LEFT_CENTER, "Colour map", Tokens::ui(10.5), t.text_faint); + p.text(pos2(strip.max.x + 8.0, strip.center().y), Align2::LEFT_CENTER, tr("Colour map"), Tokens::ui(10.5), t.text_faint); if mode != 2 { return; } @@ -844,12 +851,12 @@ fn extractor_layers( None => "Custom".to_string(), }; // Lined up with the parameter names and value popups below (Effect Controls' row layout). - p.text(pos2(r.min.x + 37.0, r.center().y), Align2::LEFT_CENTER, "Layer", Tokens::ui(12.0), t.text); + p.text(pos2(r.min.x + 37.0, r.center().y), Align2::LEFT_CENTER, tr("Layer"), Tokens::ui(12.0), t.text); let vx = (r.min.x + r.width() * 0.48).max(r.min.x + 161.0); let dr = Rect::from_min_size(pos2(vx, r.center().y - 9.0), vec2((r.max.x - vx - 56.0).clamp(80.0, 200.0), 18.0)); let pop = egui::Id::new(("extractor-layers", euid)); if widgets::dropdown(ui, dr, &label, &t, egui::Id::new(("extractor-layer", euid))) - .on_hover_text("The OpenEXR layer shown in red, green, blue and alpha") + .on_hover_text(tr("The OpenEXR layer shown in red, green, blue and alpha")) .clicked() { widgets::open_popup(ui, pop); diff --git a/crates/ui-egui/src/panels/graph.rs b/crates/ui-egui/src/panels/graph.rs index 8d7bcbdd..7369c634 100644 --- a/crates/ui-egui/src/panels/graph.rs +++ b/crates/ui-egui/src/panels/graph.rs @@ -244,7 +244,13 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte gv += step; } if cs.is_empty() { - p.text(plot.center(), Align2::CENTER_CENTER, "Select animated properties to show them in the Graph Editor", Tokens::ui(12.0), t.text_faint); + p.text( + plot.center(), + Align2::CENTER_CENTER, + crate::i18n::tr("Select animated properties to show them in the Graph Editor"), + Tokens::ui(12.0), + t.text_faint, + ); } // Show Reference Graph: the other graph type, faint, scaled to the plot. diff --git a/crates/ui-egui/src/panels/home.rs b/crates/ui-egui/src/panels/home.rs index 53b5e047..861d7d49 100644 --- a/crates/ui-egui/src/panels/home.rs +++ b/crates/ui-egui/src/panels/home.rs @@ -281,7 +281,7 @@ fn rail_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, rail: let foot_h = 22.0 + links.len() as f32 * 30.0 + 22.0 + sib_rows * 26.0; let mut cy = (rail.max.y - 16.0 - foot_h).max(y + 16.0); p.line_segment([pos2(x0, cy - 10.0), pos2(x0 + w, cy - 10.0)], Stroke::new(1.0, t.separator)); - p.text(pos2(x0, cy + 6.0), Align2::LEFT_CENTER, "Community", Tokens::semibold(12.0), t.text_dim); + p.text(pos2(x0, cy + 6.0), Align2::LEFT_CENTER, crate::i18n::tr("Community"), Tokens::semibold(12.0), t.text_dim); cy += 22.0; for (icon, label, cmd) in links { let r = Rect::from_min_size(pos2(x0, cy), vec2(w, 26.0)); @@ -301,7 +301,7 @@ fn rail_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, rail: } cy += 30.0; } - p.text(pos2(x0, cy + 8.0), Align2::LEFT_CENTER, "More ArtCraft apps", Tokens::ui(11.0), t.text_faint); + p.text(pos2(x0, cy + 8.0), Align2::LEFT_CENTER, crate::i18n::tr("More ArtCraft apps"), Tokens::ui(11.0), t.text_faint); cy += 20.0; let sw = (w - 6.0) / 2.0; for (i, (name, slug)) in sib.iter().enumerate() { @@ -328,7 +328,7 @@ fn home_page( let t = app.tokens; let (x0, w) = (area.min.x, area.width()); let mut y = area.min.y; - p.text(pos2(x0, y + 14.0), Align2::LEFT_CENTER, "Welcome to EffectCraft", Tokens::semibold(26.0), t.tab_text_active); + p.text(pos2(x0, y + 14.0), Align2::LEFT_CENTER, crate::i18n::tr("Welcome to EffectCraft"), Tokens::semibold(26.0), t.tab_text_active); y += 40.0; p.text( pos2(x0, y + 8.0), @@ -369,7 +369,7 @@ fn home_page( y += 64.0 + 36.0; // Recent: heading, filter, clear. - p.text(pos2(x0, y + 12.0), Align2::LEFT_CENTER, "Recent", Tokens::semibold(17.0), t.tab_text_active); + p.text(pos2(x0, y + 12.0), Align2::LEFT_CENTER, crate::i18n::tr("Recent"), Tokens::semibold(17.0), t.tab_text_active); let entries = recent_entries(&app.session.prefs); let fid = egui::Id::new("home-filter"); let mut filter: String = ctx.data(|d| d.get_temp(fid)).unwrap_or_default(); @@ -380,7 +380,7 @@ fn home_page( if !entries.is_empty() { let cr = Rect::from_min_size(pos2(fr.min.x - 96.0, y + 3.0), vec2(84.0, 20.0)); let resp = ui.interact(cr, egui::Id::new("home-clear-recent"), Sense::click()); - p.text(cr.right_center(), Align2::RIGHT_CENTER, "Clear list", Tokens::ui(11.5), if resp.hovered() { t.text } else { t.text_faint }); + p.text(cr.right_center(), Align2::RIGHT_CENTER, crate::i18n::tr("Clear list"), Tokens::ui(11.5), if resp.hovered() { t.text } else { t.text_faint }); app.auto.add("home.clearRecent", cr, "Clear Recent Projects"); if resp.clicked() { actions.push(("file.clearRecent", json!({}))); diff --git a/crates/ui-egui/src/panels/info.rs b/crates/ui-egui/src/panels/info.rs index b792538b..581e4334 100644 --- a/crates/ui-egui/src/panels/info.rs +++ b/crates/ui-egui/src/panels/info.rs @@ -1,5 +1,6 @@ //! Info panel: colour and position under the pointer, plus the current selection. +use crate::i18n::{tr, tr_args}; use egui::{Align2, Color32, Rect, pos2, vec2}; use crate::theme::Tokens; @@ -59,8 +60,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if let Some(c) = comp { let sel: Vec = app.session.state.selected_layers.iter().filter_map(|id| c.layer(*id)).map(|l| l.name.clone()).collect(); let tc = crate::panels::timecode(&app.session, &c, app.session.time()); - p.text(pos2(x0, y), Align2::LEFT_CENTER, if sel.is_empty() { "No layer selected".to_string() } else { sel.join(", ") }, Tokens::semibold(12.0), t.text); - p.text(pos2(x0, y + 18.0), Align2::LEFT_CENTER, format!("Time: {tc}"), Tokens::ui(12.0), t.text_dim); + p.text( + pos2(x0, y), + Align2::LEFT_CENTER, + if sel.is_empty() { tr("No layer selected").to_string() } else { sel.join(", ") }, + Tokens::semibold(12.0), + t.text, + ); + p.text(pos2(x0, y + 18.0), Align2::LEFT_CENTER, tr_args("Time: {}", &[&tc]), Tokens::ui(12.0), t.text_dim); // One layer selected: Duration, In and Out like AE's Info panel. let mut ly = y + 36.0; if let [id] = app.session.state.selected_layers.as_slice() @@ -93,7 +100,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if app.session.prefs.composition.show_rendering_progress { let ms = app.frames.last_ms.lock().map(|v| *v).unwrap_or(0.0); let rq = app.session.render_job.as_ref().map(|_| " • Rendering queue…").unwrap_or(""); - p.text(pos2(x0, ly), Align2::LEFT_CENTER, format!("Render: {ms:.0} ms • UI {:.0} fps{rq}", app.fps), Tokens::ui(11.5), t.text_faint); + p.text( + pos2(x0, ly), + Align2::LEFT_CENTER, + tr_args("Render: {} ms • UI {} fps{}", &[&format!("{ms:.0}"), &format!("{:.0}", app.fps), &rq]), + Tokens::ui(11.5), + t.text_faint, + ); } if !app.session.state.selected_keys.is_empty() { // One key: its property, time and value (as After Effects' Info panel shows them). diff --git a/crates/ui-egui/src/panels/key_dialogs.rs b/crates/ui-egui/src/panels/key_dialogs.rs index 52190f0d..3e170c12 100644 --- a/crates/ui-egui/src/panels/key_dialogs.rs +++ b/crates/ui-egui/src/panels/key_dialogs.rs @@ -1,6 +1,7 @@ //! Animation dialogs: Keyframe Velocity, Keyframe Interpolation and Time Stretch. Each one is //! pre-filled from the engine (`keys.info`, the layer) and commits through one engine command. +use crate::i18n::tr; use egui::{Color32, vec2}; use serde_json::{Value, json}; @@ -114,10 +115,10 @@ fn buttons(ui: &mut egui::Ui, app: &mut EffectcraftApp, t: &Tokens, prefix: &str let (mut ok, mut cancel) = (false, false); ui.add_space(14.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)); + let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)); app.auto.add(&format!("{prefix}.ok"), r.rect, "OK"); ok = r.clicked(); - let r = ui.button("Cancel"); + let r = ui.button(tr("Cancel")); app.auto.add(&format!("{prefix}.cancel"), r.rect, "Cancel"); cancel = r.clicked(); }); @@ -144,13 +145,13 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.label(egui::RichText::new(tag).color(t.text_dim)); let side = if out { "out" } else { "in" }; ui.horizontal(|ui| { - ui.label("Speed:"); + ui.label(tr("Speed:")); let v = if out { &mut d.out_speed[dim] } else { &mut d.in_speed[dim] }; drag(ui, app, &format!("dialog.velocity.{side}Speed.{dim}"), v, 1.0, -1e7..=1e7, "", enabled); ui.label(egui::RichText::new(&d.units).color(t.text_dim)); }); ui.horizontal(|ui| { - ui.label("Influence:"); + ui.label(tr("Influence:")); let v = if out { &mut d.out_inf[dim] } else { &mut d.in_inf[dim] }; drag(ui, app, &format!("dialog.velocity.{side}Influence.{dim}"), v, 0.2, 0.1..=100.0, " %", enabled); }); @@ -159,7 +160,7 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { }); ui.add_space(8.0); } - let r = ui.checkbox(&mut d.continuous, "Continuous (Lock Outgoing to Incoming)"); + let r = ui.checkbox(&mut d.continuous, tr("Continuous (Lock Outgoing to Incoming)")); app.auto.add("dialog.velocity.continuous", r.rect, "Continuous"); if d.continuous { d.out_speed = d.in_speed.clone(); @@ -203,13 +204,13 @@ pub fn interpolation(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) let (mut ok, mut cancel) = (false, false); super::dialogs::modal(ctx, "Keyframe Interpolation", vec2(440.0, 250.0), t, |ui| { egui::Grid::new("interp-grid").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| { - ui.label("Temporal Interpolation:"); + ui.label(tr("Temporal Interpolation:")); combo(ui, app, "dialog.interpolation.temporal", &mut d.temporal, &TEMPORAL.map(|x| x.0), true); ui.end_row(); - ui.label("Spatial Interpolation:"); + ui.label(tr("Spatial Interpolation:")); combo(ui, app, "dialog.interpolation.spatial", &mut d.spatial, &SPATIAL.map(|x| x.0), d.has_spatial); ui.end_row(); - ui.label("Roving:"); + ui.label(tr("Roving:")); combo(ui, app, "dialog.interpolation.roving", &mut d.roving, &ROVING, d.has_spatial); ui.end_row(); }); @@ -245,19 +246,19 @@ pub fn time_stretch(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let mut d = app.dialog_state.stretch.clone(); let (mut ok, mut cancel) = (false, false); super::dialogs::modal(ctx, "Time Stretch", vec2(420.0, 300.0), t, |ui| { - ui.label(egui::RichText::new("Stretch").font(Tokens::medium(12.5))); + ui.label(egui::RichText::new(tr("Stretch")).font(Tokens::medium(12.5))); egui::Grid::new("stretch-grid").num_columns(2).spacing([14.0, 8.0]).show(ui, |ui| { - ui.label("Original Duration:"); + ui.label(tr("Original Duration:")); ui.label(format!("{:.2} s", d.orig_duration)); ui.end_row(); - ui.label("Stretch Factor:"); + ui.label(tr("Stretch Factor:")); let before = d.percent; drag(ui, app, "dialog.timeStretch.percent", &mut d.percent, 0.5, -10000.0..=10000.0, " %", true); if (before - d.percent).abs() > 1e-9 && d.orig_percent.abs() > 1e-9 { d.duration = d.orig_duration * d.percent.abs() / d.orig_percent.abs(); } ui.end_row(); - ui.label("New Duration:"); + ui.label(tr("New Duration:")); let before = d.duration; drag(ui, app, "dialog.timeStretch.duration", &mut d.duration, 0.05, 0.01..=100000.0, " s", true); if (before - d.duration).abs() > 1e-9 && d.orig_duration > 1e-9 { @@ -266,7 +267,7 @@ pub fn time_stretch(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.end_row(); }); ui.add_space(10.0); - ui.label(egui::RichText::new("Hold In Place").font(Tokens::medium(12.5))); + ui.label(egui::RichText::new(tr("Hold In Place")).font(Tokens::medium(12.5))); for (i, l) in ["Layer In-point", "Current Frame", "Layer Out-point"].into_iter().enumerate() { let r = ui.radio_value(&mut d.hold, i, l); app.auto.add(&format!("dialog.timeStretch.hold.{i}"), r.rect, l); diff --git a/crates/ui-egui/src/panels/layer_panel.rs b/crates/ui-egui/src/panels/layer_panel.rs index 7426495f..a5952d60 100644 --- a/crates/ui-egui/src/panels/layer_panel.rs +++ b/crates/ui-egui/src/panels/layer_panel.rs @@ -11,6 +11,7 @@ //! overlay, the segmentation span bar and the view / Freeze buttons; the Roto Brush and Refine //! Edge tools paint their strokes here (see [`super::roto_tool`]). +use crate::i18n::{tr, tr_args}; use effectcraft_engine::effects::paint; use effectcraft_engine::project::{Layer, LayerId}; use effectcraft_engine::render::{EvalCtx, RenderOpts, Renderer}; @@ -67,7 +68,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let p = ui.painter().with_clip_rect(rect); let (Some(cid), Some(layer)) = (app.session.active_comp_id(), current_layer(app)) else { p.rect_filled(rect, 0.0, t.pasteboard); - p.text(rect.center(), Align2::CENTER_CENTER, "Double-click a layer to open it in the Layer panel", Tokens::ui(12.0), t.text_faint); + p.text(rect.center(), Align2::CENTER_CENTER, tr("Double-click a layer to open it in the Layer panel"), Tokens::ui(12.0), t.text_faint); app.auto.add("layerPanel.empty", rect, "Layer panel (empty)"); return; }; @@ -80,12 +81,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Title strip. let top = Rect::from_min_size(rect.min, vec2(rect.width(), 24.0)); p.rect_filled(top, 0.0, t.panel_bg); - p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, format!("Layer: {}", layer.name), Tokens::semibold(12.0), t.text); + p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, tr_args("Layer: {}", &[&layer.name]), Tokens::semibold(12.0), t.text); app.auto.add("layerPanel.title", top, &layer.name); // Bottom bar: View menu. let bar = Rect::from_min_max(pos2(rect.min.x, rect.max.y - 30.0), rect.max); p.rect_filled(bar, 0.0, t.panel_bg); - p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, "View:", Tokens::ui(12.0), t.text_dim); + p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, tr("View:"), Tokens::ui(12.0), t.text_dim); let mut names = vec!["Masks".to_string()]; if let Some(fx) = layer.effects() { names.extend(fx.groups().map(|g| g.name.clone())); diff --git a/crates/ui-egui/src/panels/layer_styles_dialog.rs b/crates/ui-egui/src/panels/layer_styles_dialog.rs index b84cd8ff..7738fbea 100644 --- a/crates/ui-egui/src/panels/layer_styles_dialog.rs +++ b/crates/ui-egui/src/panels/layer_styles_dialog.rs @@ -4,6 +4,7 @@ //! `layerStyles/