From cceaba0452c2e3f8fd152e4f3b11d41cba561302 Mon Sep 17 00:00:00 2001 From: FeiLiuEM Date: Fri, 9 Oct 2026 20:17:05 +0800 Subject: [PATCH 1/4] i18n: a catalog for the interface below the menu bar The menu bar has had a catalog since #229 (keyed by `(command, English label)`); everything below it - panel labels, buttons, field names, tooltips, the sentences a panel draws itself - was written straight into the code and stayed English in every language (#333). This adds `crates/ui-egui/src/i18n/ui.rs`: - Rows are keyed by the English source string, so a panel wraps a literal it already has instead of inventing an id, and a row that is missing falls back to English rather than to an empty label. - `tr("Rename")` translates a plain string; `tr_args("Proxy Use: {}", &[&quality])` fills the values of a row that carries them. `tr_args` writes into its own buffer, so it cannot panic on a mismatch, and a test keeps every row's placeholder count equal to its English source. - The language in force is set once per frame from the resolved interface language - the same value the CJK fonts are installed for - and kept in a thread-local, so the panels do not have to thread it through every function. The render threads never draw interface text. - Simplified and Traditional Chinese are complete (384 strings); the Japanese rows are still open, and the catalog takes them without any other change. Tests: the two Chinese catalogs must list the same source strings, no empty or duplicated rows, every row must be asked for by a converted panel, and a converted panel that gains a hard-coded string in a common call shape fails the build. --- ROADMAP.md | 1 + crates/ui-egui/src/i18n.rs | 4 + crates/ui-egui/src/i18n/ui.rs | 1067 +++++++++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 2 + docs/gaps.md | 2 +- docs/preferences.md | 2 +- 6 files changed, 1076 insertions(+), 2 deletions(-) create mode 100644 crates/ui-egui/src/i18n/ui.rs 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/i18n.rs b/crates/ui-egui/src/i18n.rs index 3735df98..55170793 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; 3] = ["ja", "zh-hans", "zh-hant"]; 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 aba24063..cb09da59 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1573,6 +1573,8 @@ impl eframe::App for EffectcraftApp { fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.apply_gpu_failure(ctx); + // The panels read the interface language from here (see `crate::i18n::ui`). + crate::i18n::set_current(crate::i18n::language(self)); if !self.styled { theme::install(ctx, &self.tokens); fit_window(ctx); diff --git a/docs/gaps.md b/docs/gaps.md index a40b4cb5..87aa2778 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -34,7 +34,7 @@ of them have been reconciled. | Behaves like After Effects | largely unmeasured, ≈ 60–80% | One outside contributor found four bugs in features marked done within a day (PRs #6–#10): Hold keyframes eased the motion into them, `keyInSpatialTangent` / `keyOutSpatialTangent` had the wrong names, a zero frame rate crashed, Find and Enter Full Screen shared Ctrl+F off macOS. There are about 10 After Effects reference captures in total. The first live G1 comparison now checks eight scalar Rotation/ease cases (168 samples) against AE 26.3x87 via AEsync 2.0.4: all pass after fixing overlapping temporal influences, with maximum error below 3e-11 degrees ([scores and reproduction](fidelity/README.md)). Other animation behavior and rendered frames remain unmeasured. User reports of 6 October found five more in features marked done (the Render Queue's template menu, angle revolutions, switches wiping the RAM preview, audio playback skipping frames, preview crashing when video memory ran out; all fixed 7 October) | | Opening existing After Effects work | ≈ 0% | `.aep` / `.aepx` projects cannot be opened. Third-party After Effects plug-ins cannot run. Expressions and the scripting object model are strong, so scripts and expressions carry over | | Stability | improving | 23 never-crash PRs landed on 4 October; community PRs on 7 October fixed a hang on projects whose parent chains loop (#142), a panic on non-ASCII label colours (#135), and contained GPU initialization and device failures (#139); [AGENTS.md](../AGENTS.md) "Never crash" now binds every crate. On 5 October `cargo xtask ci` failed on main under Rust 1.99's clippy (a fix is in progress) | -| Real-user experience | ≈ 70%, uneven by platform | Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the viewer snaps back, panels can't be resized or rearranged, drag-and-drop and double-click import don't work, layer rename gets stuck, the Layer Settings arrow does nothing, the Project panel clips, the Wayland window icon is generic. Issues #63–#68 (macOS, 5 October): scaling a layer by its handles goes wrong and can stick at 0, two 3D compasses, the viewer lags while a layer is dragged (a frame renders in 20 ms but showed after ≈ 100 ms, behind RAM preview prefetch), the Discord and other links do nothing, Delete doesn't delete in the Project panel, hidden layers can be selected in the viewer. All but the last two were fixed or confirmed fixed with a test on 5–6 October (those two have community PRs); file drops still can't work on Wayland (winit has no support). The panning, rename and arrow bugs had been fixed in v0.2.0 already; nobody had told the reporter. A Windows user (6 October, on Discord) found the font menus listed only the three bundled fonts: installed fonts were read only when a project asked for one, on every platform. Fixed with a regression test on 6 October; the menus now list every installed family and its own styles, and agents get `text.fonts` / `list_fonts`. Seven reports of 6 October (Linux .deb and Windows): Project items and files couldn't be dropped on the viewer (#85), nor effects on a layer there (#88); drops in the Timeline ignored where they landed (#89); an angle's revolutions couldn't be edited (#93); toggling Audio, Lock or Shy threw away the RAM preview and playback with audio skipped frames (#103); running out of video memory panicked every frame thread and left frames stuck (#106); choosing a Render Settings template in the Render Queue did nothing (#117: any popup menu moved up to fit the window closed on the press). All seven were fixed with regression tests on 7 October. Reports of 7 October (Linux, macOS, Windows): the app opened the demo project on every launch, so a new comp landed in it (#204); a comp wider than the GPU's texture limit (11000 × 2200) at Full resolution stopped the window drawing (#201); an Intel HD Graphics 5500 couldn't start the app (#198: most likely the window's device asked for limits that GPU doesn't have, and the failure was silent); Audio Spectrum / Waveform stood still in the viewer (#209) and preview sound waited for the whole work area to cache (#208); a layer couldn't be deselected from the time graph, a shape layer's Fill and Stroke showed only with a shape tool and a drawn shape had no stroke to change, Alt+Shift+P didn't reveal Position (#205); a mask selected in the Timeline wasn't selected in the viewer, Mask Feather had no link and took negative values, the Project panel had no selection box (#203); shape layers had no Add menu for Trim Paths and the other operations, and Classic 3D comps hid Geometry Options (#206); dragging Puppet pins re-rendered the layer under the Puppet at every move (#212). All were fixed with regression tests the same day (#219–#222); the #198 fix couldn't be checked on that GPU. Reports of 8 October (browser, Windows, macOS): an empty project at 1280 × 800 with device pixel ratio 2 crashed on its first frame, because the Project panel's scroll bar clamped its thumb to a track shorter than the thumb (#231, any short panel on every platform); Media Browser ▸ Add Files… dropped a file it couldn't read without a word (#226); after the page's WebGPU device was lost, the browser editor froze on its last frame while edits went on (#225); a press in the viewer always took the topmost layer, so a selected layer behind others couldn't be dragged (#230); the language didn't follow the system's (#229); and a list of missing After Effects behaviour (#227): drawing into, copying, pasting and duplicating shape contents, Tool Creates Shape / Mask and Fill / Stroke Options, Turbulent Displace, Wave Warp and Bulge pinning all but a shape layer's bottom-right quadrant, a Spacebar hold that previewed instead of being the Hand tool, the Effect menu in Effect Controls, dropping items on New Composition and dragging over layer switches. All were fixed with regression tests the same day (#235, #237–#241); multi-monitor support and the Project panel's empty-area menu (community PR #210) remain from #227, and effect points on shape and text layers still sit half a comp off. A list of tweaks from an Intel iMac (#290): the layer bar didn't show where extended footage runs past its source, and Enable Time Remapping then ended at the extended out point; footage items had no Interpret Footage in their menu and the work area bar no menu; Label menus named the labels without their colours; the arrow keys didn't nudge layers; a straight motion path had no Bezier handles; Delete on Time Remap deleted the layer; a mask being drawn with the Pen blacked out its layer until it was closed; a double-click on a mask point didn't select the whole mask. All were fixed with regression tests the same day; the rest of #290 had been fixed already (#221, #238, #255) or came with community PRs #210 (the Timeline's empty-area menu) and #213 (tool flyouts on a long press). Most checking happens on macOS. Localisation covers Japanese menus only; no accessibility work | +| Real-user experience | ≈ 70%, uneven by platform | Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the viewer snaps back, panels can't be resized or rearranged, drag-and-drop and double-click import don't work, layer rename gets stuck, the Layer Settings arrow does nothing, the Project panel clips, the Wayland window icon is generic. Issues #63–#68 (macOS, 5 October): scaling a layer by its handles goes wrong and can stick at 0, two 3D compasses, the viewer lags while a layer is dragged (a frame renders in 20 ms but showed after ≈ 100 ms, behind RAM preview prefetch), the Discord and other links do nothing, Delete doesn't delete in the Project panel, hidden layers can be selected in the viewer. All but the last two were fixed or confirmed fixed with a test on 5–6 October (those two have community PRs); file drops still can't work on Wayland (winit has no support). The panning, rename and arrow bugs had been fixed in v0.2.0 already; nobody had told the reporter. A Windows user (6 October, on Discord) found the font menus listed only the three bundled fonts: installed fonts were read only when a project asked for one, on every platform. Fixed with a regression test on 6 October; the menus now list every installed family and its own styles, and agents get `text.fonts` / `list_fonts`. Seven reports of 6 October (Linux .deb and Windows): Project items and files couldn't be dropped on the viewer (#85), nor effects on a layer there (#88); drops in the Timeline ignored where they landed (#89); an angle's revolutions couldn't be edited (#93); toggling Audio, Lock or Shy threw away the RAM preview and playback with audio skipped frames (#103); running out of video memory panicked every frame thread and left frames stuck (#106); choosing a Render Settings template in the Render Queue did nothing (#117: any popup menu moved up to fit the window closed on the press). All seven were fixed with regression tests on 7 October. Reports of 7 October (Linux, macOS, Windows): the app opened the demo project on every launch, so a new comp landed in it (#204); a comp wider than the GPU's texture limit (11000 × 2200) at Full resolution stopped the window drawing (#201); an Intel HD Graphics 5500 couldn't start the app (#198: most likely the window's device asked for limits that GPU doesn't have, and the failure was silent); Audio Spectrum / Waveform stood still in the viewer (#209) and preview sound waited for the whole work area to cache (#208); a layer couldn't be deselected from the time graph, a shape layer's Fill and Stroke showed only with a shape tool and a drawn shape had no stroke to change, Alt+Shift+P didn't reveal Position (#205); a mask selected in the Timeline wasn't selected in the viewer, Mask Feather had no link and took negative values, the Project panel had no selection box (#203); shape layers had no Add menu for Trim Paths and the other operations, and Classic 3D comps hid Geometry Options (#206); dragging Puppet pins re-rendered the layer under the Puppet at every move (#212). All were fixed with regression tests the same day (#219–#222); the #198 fix couldn't be checked on that GPU. Reports of 8 October (browser, Windows, macOS): an empty project at 1280 × 800 with device pixel ratio 2 crashed on its first frame, because the Project panel's scroll bar clamped its thumb to a track shorter than the thumb (#231, any short panel on every platform); Media Browser ▸ Add Files… dropped a file it couldn't read without a word (#226); after the page's WebGPU device was lost, the browser editor froze on its last frame while edits went on (#225); a press in the viewer always took the topmost layer, so a selected layer behind others couldn't be dragged (#230); the language didn't follow the system's (#229); and a list of missing After Effects behaviour (#227): drawing into, copying, pasting and duplicating shape contents, Tool Creates Shape / Mask and Fill / Stroke Options, Turbulent Displace, Wave Warp and Bulge pinning all but a shape layer's bottom-right quadrant, a Spacebar hold that previewed instead of being the Hand tool, the Effect menu in Effect Controls, dropping items on New Composition and dragging over layer switches. All were fixed with regression tests the same day (#235, #237–#241); multi-monitor support and the Project panel's empty-area menu (community PR #210) remain from #227, and effect points on shape and text layers still sit half a comp off. A list of tweaks from an Intel iMac (#290): the layer bar didn't show where extended footage runs past its source, and Enable Time Remapping then ended at the extended out point; footage items had no Interpret Footage in their menu and the work area bar no menu; Label menus named the labels without their colours; the arrow keys didn't nudge layers; a straight motion path had no Bezier handles; Delete on Time Remap deleted the layer; a mask being drawn with the Pen blacked out its layer until it was closed; a double-click on a mask point didn't select the whole mask. All were fixed with regression tests the same day; the rest of #290 had been fixed already (#221, #238, #255) or came with community PRs #210 (the Timeline's empty-area menu) and #213 (tool flyouts on a long press). Most checking happens on macOS. Localisation covers the menus in Japanese, Simplified and Traditional Chinese, and since 9 October the panels, dialogs and tooltips too (Simplified and Traditional Chinese; the Japanese rows of the panel catalog are still open); no accessibility work | | Performance | unknown against After Effects | Internal numbers only (e.g. Advanced 3D 290 ms/frame at 1080p on the GPU, an M4 Pro under load). Nothing benchmarked against After Effects; no large real projects (4K footage, hundreds of layers) tested | | Media formats | ≈ 80% | H.264, ProRes, HEVC, AV1, image sequences and audio exist. The new HEVC / AV1 encoders have no B-frames, multi-reference or SAO / CDEF, so files are larger than from mature encoders. Camera formats (BRAW, R3D, ProRes RAW, variable-frame-rate phone video) are unverified | | AI-assisted tools | ≈ 50% | Both tools can use trained models (pure-Rust inference, optional downloads), but nothing compares them with After Effects yet. Roto Brush 2.0 / 3.0 with MobileSAM (M13.35) scores IoU 0.989 on the base frame of our synthetic moving-disc test and ≥ 0.980 over 20 propagated frames (classic: 0.973). Face tracking with MediaPipe Face Landmarker (M13.36) matches Google's own pipeline to 0.85 px on average on a test portrait; on our synthetic clip the eyes and chin stay within 4% of the face height | diff --git a/docs/preferences.md b/docs/preferences.md index df948f0d..a37a5f63 100644 --- a/docs/preferences.md +++ b/docs/preferences.md @@ -29,7 +29,7 @@ Commands (CLI, MCP, control channel): ### Settings that change behaviour -- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`; the browser build stays in English with it. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. +- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`; the browser build stays in English with it. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. Two catalogs back the text: the menus are keyed by `(command, English label)` (`crates/ui-egui/src/i18n.rs`), and the rest of the interface - panels, dialogs, buttons, tooltips - is keyed by its English source string (`crates/ui-egui/src/i18n/ui.rs`), where a panel writes `tr("Rename")`, or `tr_args("Proxy Use: {}", &[&quality])` when the row carries a value. A row that is missing falls back to English, so a new string shows up untranslated rather than empty; the tests in that module fail on an unused row and on a hard-coded string in a converted panel. - `general.undoLevels`: Levels of Undo - `general.pathPointSize`: Path Point and Handle Size - `general.recentItems`: Recent Projects Shown From add2e977f6b66c6d1be448ba118c4c865e458dd1 Mon Sep 17 00:00:00 2001 From: FeiLiuEM Date: Fri, 9 Oct 2026 20:17:05 +0800 Subject: [PATCH 2/4] panels: dialogs, settings and forms read their text from the catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dialogs, Settings pages, unsaved-work and template prompts, the script console and the media panels now go through `crate::i18n`, so their buttons, field names and hints follow Settings ▸ General ▸ Language. --- .../ui-egui/src/panels/content_fill_panel.rs | 3 +- crates/ui-egui/src/panels/delete_items.rs | 7 ++- crates/ui-egui/src/panels/dialogs.rs | 31 +++++----- crates/ui-egui/src/panels/dialogs_3d.rs | 57 +++++++++--------- crates/ui-egui/src/panels/forms.rs | 27 +++++---- crates/ui-egui/src/panels/home.rs | 10 ++-- crates/ui-egui/src/panels/key_dialogs.rs | 27 +++++---- .../ui-egui/src/panels/layer_styles_dialog.rs | 9 +-- crates/ui-egui/src/panels/media_panels.rs | 21 +++---- crates/ui-egui/src/panels/mod.rs | 9 +-- crates/ui-egui/src/panels/script_console.rs | 7 ++- crates/ui-egui/src/panels/scriptui_view.rs | 3 +- crates/ui-egui/src/panels/settings.rs | 60 ++++++++++--------- crates/ui-egui/src/panels/templates.rs | 4 +- crates/ui-egui/src/panels/unsaved.rs | 7 ++- 15 files changed, 148 insertions(+), 134 deletions(-) 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 80a3e88c..dce4642d 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/forms.rs b/crates/ui-egui/src/panels/forms.rs index f4de5f21..12f2c2f9 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}; @@ -684,7 +685,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); @@ -696,7 +697,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); @@ -727,12 +728,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; @@ -786,7 +787,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; } }); @@ -800,16 +801,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/home.rs b/crates/ui-egui/src/panels/home.rs index 904e5f2f..600bc551 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/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_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/