i18n: an effect's name draws in the interface language, the generated Effect menu included (#821)

The interface below the menu bar is localized (618 fixed menu entries, ~490 panel rows) but an
effect's name still rendered in English everywhere it appeared: the generated Effect menu, Effect
Controls, the Timeline and Effects & Presets.

The registry's English name is the effect's identity: `effect.list`, `effect.apply {"effect": …}`,
scripts, expressions and the property tree's group name all match on it. Only the drawing changes.

`crates/ui-egui/src/i18n/effects.rs` keys the names by effect **id** rather than by the English
string — a repeat is stored as `Gaussian Blur 2` (an English key would miss it) and upstream
renames a display name now and then, while the id is stable. It carries 306 rows for Simplified
Chinese, one per effect in the registry, taken from the installed After Effects 2026 (26.5.0.89)
Simplified Chinese interface, with the `CC` series following Adobe's Chinese documentation (which
names 68 of the 69 that its interface leaves in English). `Cryptomatte`, `EXtractoR`, `IDentifier`,
`Key Light` and `Unmult` stay in English as brand names; `Face Measurements` and `Face Track
Points` are EffectCraft's own effects. The 21 category folder names are in the same file, worded
after the effect names (channel → 通道, noise → 杂色, matte → 遮罩 …).

What draws the catalog now:

- **Effect menu:** the entries and the category submenus. The command and its `{"effect": <id>}`
  params are untouched, so the menu still applies by id.
- **Effect Controls:** the header, its Rename field, the accessible name and the About popup.
- **Timeline:** effect group rows, and the search (a Chinese interface is searchable in Chinese).
- **Effects & Presets:** the rows, the drag ghost, the folder rows and the search.

Kept as they are: the effect id, the property tree's `PropGroup.name`, every automation id and
label, `effect.list`'s `name`/`category`, and the undo label (`Apply Gaussian Blur`). A name the
user typed in Effect Controls stays verbatim, and a repeated instance keeps the index the engine
appends (`Gaussian Blur 2` → `高斯模糊 2`). No other language has rows yet, Traditional Chinese
included, so those draw the registry's English name.

Tests: nine unit tests under `cargo test -p effectcraft-ui-egui --lib i18n` cover the data (every
effect has a row, the row still carries the registry's English name, ids are unique, only the five
brand names repeat their English name, every category has a row, a row names an effect the registry
knows) and the behaviour (the instance index, user renames, English and untranslated languages,
search) plus a scan pinning the drawing sites. `crates/ui-egui/tests/ui_effect_names.rs` checks the
Effect Controls header, the generated Effect menu and that the ids, automation labels and
`effect.list` stay English.

`docs/localization-parity.md` and `docs/gaps.md` no longer record effect names as deliberately
English; effect *parameter* names are still untranslated and stay the next piece.

Refs #821
This commit is contained in:
WaWu committed 2026-10-13 13:39:16 +08:00
1 parent 470ea7a3af
commit 0aa2ea5e12
10 files changed
+783 -27

No files matched your search

+22 -1
View File
@@ -5,9 +5,11 @@
use crate::EffectcraftApp;
/// Effect names, keyed by effect id (#821).
pub(crate) mod effects;
/// Translations for the panels, tooltips and dialogs below the menu bar (see the module docs).
mod ui;
use effectcraft_engine::menus::MenuEntry;
use effectcraft_engine::menus::{MenuEntry, MenuNode};
pub(crate) use ui::{set_current, tr, tr_args};
/// Every language with a catalog, in the order Settings ▸ General ▸ Language lists them.
@@ -138,6 +140,15 @@ fn lookup<'a>(catalog: &[(&str, &str, &'a str)], command: &str, source: &'a str)
}
pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> String {
// The generated Effect menu's entries label themselves with the effect's registry name; the
// catalog replaces it with the interface language's (#821). The entry keeps its command and
// its `{"effect": <id>}` params, so a script, an expression or the control channel is
// unaffected by what the menu draws.
if e.command == "effect.apply"
&& let Some(id) = e.params.get("effect").and_then(|v| v.as_str())
{
return effects::name(id, &e.label);
}
// Undo/redo names come from the history, so they are formatted per language here rather than
// looked up in a catalog. An English (or untranslated) interface keeps the shown label.
let (undo, no_undo, redo, no_redo) = match language(app) {
@@ -168,6 +179,16 @@ pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> Strin
}
}
/// The label to draw for a generated Effect-menu category submenu, or `None` for every other
/// submenu (whose label goes through [`submenu`]).
///
/// The generated submenus are the only ones whose children are all `effect.apply` entries, which
/// is what identifies them: their label is one of the registry's 21 categories, drawn as a
/// submenu in the Effect menu and as a folder row in Effects & Presets (#821).
pub(crate) fn effect_category(children: &[MenuNode], source: &str) -> Option<String> {
(!children.is_empty() && children.iter().all(|c| matches!(c, MenuNode::Item(e) if e.command == "effect.apply"))).then(|| effects::category(source))
}
pub(crate) fn submenu(app: &EffectcraftApp, source: &str, shown: String) -> String {
match language(app) {
"cs" => {
+582
View File
@@ -0,0 +1,582 @@
//! Effect names in the interface language, keyed by effect id (#821).
//!
//! The generated Effect menu, Effect Controls, the Timeline and Effects & Presets draw an
//! effect's name. The registry (`effectcraft_effects`) keeps the English name: scripts,
//! expressions, MCP and the control channel match on it, and the property tree stores it as the
//! effect group's name. Only the *drawing* goes through this catalog, so a project file stays
//! identical whichever interface language opens it.
//!
//! Rows are keyed by id rather than by the English source string, for two reasons:
//!
//! - a second instance of an effect is stored as `Gaussian Blur 2` (the engine appends the
//! index to the group name), which an English-string key would miss;
//! - upstream renames a display name now and then (26.5 turned `Twirl` into `Twirl (Legacy)`);
//! the id is stable, so a translation never becomes an orphan.
//!
//! Each row repeats the registry's English name so that a rename upstream fails
//! [`tests::every_effect_has_a_row`] instead of silently pointing a translation at another
//! effect.
//!
//! Only Simplified Chinese has rows. Every other language — Traditional Chinese included —
//! draws the registry's English name; a translator adds rows to [`TRADITIONAL_CHINESE`] keyed
//! by the same ids (the test below refuses an id that Simplified Chinese does not have).
//!
//! The names come from the installed After Effects 2026 (26.5.0.89) Simplified Chinese
//! interface; the `CC` series follows Adobe's Chinese *documentation*, which names 68 of the 69
//! that the interface leaves in English (see the PR for #821). `Cryptomatte`, `EXtractoR`,
//! `IDentifier`, `Key Light` and `Unmult` stay in English as brand names, and `Face
//! Measurements` / `Face Track Points` are EffectCraft's own effects, so their wording is ours.
use std::collections::HashMap;
use std::sync::OnceLock;
use super::ui::current;
/// One row: effect id, the registry's English name, the translation.
type Row = (&'static str, &'static str, &'static str);
/// The effect's name to draw: the catalog row for `id`, else the registry's `english`.
pub(crate) fn name(id: &str, english: &str) -> String {
row(id).map_or_else(|| english.to_string(), |(_, translated)| translated.to_string())
}
/// The name to draw for an effect group the property tree stores as `stored`: the catalog name,
/// plus the instance index the engine appends to a repeat (`Gaussian Blur 2` → `高斯模糊 2`).
///
/// A name that is neither the registry's nor the registry's plus an index was typed by the user
/// (Effect Controls ▸ Rename), so it stays verbatim — as does the whole string in a language
/// with no row.
pub(crate) fn group(id: &str, stored: &str) -> String {
let Some((english, translated)) = row(id) else { return stored.to_string() };
if stored == english {
return translated.to_string();
}
if let Some(index) = stored.strip_prefix(english).and_then(|rest| rest.strip_prefix(' '))
&& !index.is_empty()
&& index.bytes().all(|b| b.is_ascii_digit())
{
return format!("{translated} {index}");
}
stored.to_string()
}
/// The label to draw for the effect category `source` (the generated Effect menu's submenus and
/// Effects & Presets' folder rows).
pub(crate) fn category(source: &str) -> String {
if current() != "zh-hans" {
return source.to_string();
}
CATEGORIES.iter().find(|(english, _)| *english == source).map_or_else(|| source.to_string(), |(_, translated)| translated.to_string())
}
/// Does effect `id` (`english` when the language has no row) match the Effects & Presets search
/// `query` (lowercase)? Both names match, so a Chinese interface can be searched either way.
pub(crate) fn matches(id: &str, english: &str, query: &str) -> bool {
english.to_lowercase().contains(query) || row(id).is_some_and(|(_, translated)| translated.to_lowercase().contains(query))
}
/// `id`'s (English name, translation) in the current language.
fn row(id: &str) -> Option<(&'static str, &'static str)> {
index(current()).and_then(|rows| rows.get(id).copied())
}
type Index = HashMap<&'static str, (&'static str, &'static str)>;
/// The catalog for `language`, indexed by effect id (built on first use); `None` for English and
/// any language with no rows.
fn index(language: &str) -> Option<&'static Index> {
static HANS: OnceLock<Index> = OnceLock::new();
static HANT: OnceLock<Index> = OnceLock::new();
let (slot, rows) = match language {
"zh-hans" => (&HANS, SIMPLIFIED_CHINESE),
"zh-hant" => (&HANT, TRADITIONAL_CHINESE),
_ => return None,
};
Some(slot.get_or_init(|| rows.iter().map(|(id, english, translated)| (*id, (*english, *translated))).collect()))
}
/// Simplified Chinese (`zh-hans`): one row per effect in the registry.
const SIMPLIFIED_CHINESE: &[Row] = &[
("ec.3d.channelextract", "3D Channel Extract", "3D 通道提取"),
("ec.3d.cryptomatte", "Cryptomatte", "Cryptomatte"),
("ec.3d.depthmatte", "Depth Matte", "深度遮罩"),
("ec.3d.depthoffield", "Depth of Field", "场深度"),
("ec.3d.extractor", "EXtractoR", "EXtractoR"),
("ec.3d.fog3d", "Fog 3D", "雾 3D"),
("ec.3d.identifier", "IDentifier", "IDentifier"),
("ec.3d.idmatte", "ID Matte", "ID 遮罩"),
("ec.audio.backwards", "Backwards", "倒放"),
("ec.audio.basstreble", "Bass & Treble", "低音和高音"),
("ec.audio.compressor", "Compressor", "压缩程序"),
("ec.audio.delay", "Delay", "延迟"),
("ec.audio.distortion", "Distortion", "扭曲"),
("ec.audio.flangechorus", "Flange & Chorus", "变调与合声"),
("ec.audio.gate", "Gate", "门"),
("ec.audio.highlowpass", "High-Low Pass", "高通/低通"),
("ec.audio.modulator", "Modulator", "调制器"),
("ec.audio.parametriceq", "Parametric EQ", "参数均衡"),
("ec.audio.reverb", "Reverb", "混响"),
("ec.audio.stereomixer", "Stereo Mixer", "立体声混合器"),
("ec.audio.tone", "Tone", "音调"),
("ec.blur.bilateral", "Bilateral Blur", "双向模糊"),
("ec.blur.cameralens", "Camera Lens Blur", "摄像机镜头模糊"),
("ec.blur.camerashakedeblur", "Camera-Shake Deblur", "摄像机抖动去模糊"),
("ec.blur.cccross", "CC Cross Blur", "CC 交叉模糊"),
("ec.blur.ccradial", "CC Radial Blur", "CC 径向模糊"),
("ec.blur.ccradialfast", "CC Radial Fast Blur", "CC 径向快速模糊"),
("ec.blur.ccvector", "CC Vector Blur", "CC 矢量模糊"),
("ec.blur.channel", "Channel Blur", "通道模糊"),
("ec.blur.compound", "Compound Blur", "复合模糊"),
("ec.blur.directional", "Directional Blur", "定向模糊"),
("ec.blur.fastbox", "Fast Box Blur", "快速方框模糊"),
("ec.blur.gaussian", "Gaussian Blur", "高斯模糊"),
("ec.blur.radial", "Radial Blur", "径向模糊"),
("ec.blur.reduceflicker", "Reduce Interlace Flicker", "减少交错闪烁"),
("ec.blur.sharpen", "Sharpen", "锐化"),
("ec.blur.smart", "Smart Blur", "智能模糊"),
("ec.blur.unsharp", "Unsharp Mask", "钝化蒙版"),
("ec.channel.arithmetic", "Arithmetic", "算术"),
("ec.channel.blend", "Blend", "混合"),
("ec.channel.calculations", "Calculations", "计算"),
("ec.channel.cccomposite", "CC Composite", "CC 合成"),
("ec.channel.combiner", "Channel Combiner", "通道合成器"),
("ec.channel.compoundarithmetic", "Compound Arithmetic", "复合运算"),
("ec.channel.invert", "Invert", "反转"),
("ec.channel.minimax", "Minimax", "最小/最大"),
("ec.channel.removecolormatting", "Remove Color Matting", "移除颜色遮罩"),
("ec.channel.setchannels", "Set Channels", "设置通道"),
("ec.channel.setmatte", "Set Matte", "设置遮罩"),
("ec.channel.shiftchannels", "Shift Channels", "转换通道"),
("ec.channel.solidcomposite", "Solid Composite", "固态层合成"),
("ec.color.autocolor", "Auto Color", "自动颜色"),
("ec.color.autocontrast", "Auto Contrast", "自动对比度"),
("ec.color.autolevels", "Auto Levels", "自动色阶"),
("ec.color.blackwhite", "Black & White", "黑色和白色"),
("ec.color.brightnesscontrast", "Brightness & Contrast", "亮度和对比度"),
("ec.color.broadcast", "Broadcast Colors", "广播颜色"),
("ec.color.cccolorneutralizer", "CC Color Neutralizer", "CC 颜色中和器"),
("ec.color.cccoloroffset", "CC Color Offset", "CC 颜色偏移"),
("ec.color.cckernel", "CC Kernel", "CC 内核"),
("ec.color.cctoner", "CC Toner", "CC 调色器"),
("ec.color.changecolor", "Change Color", "更改颜色"),
("ec.color.changetocolor", "Change to Color", "更改为颜色"),
("ec.color.channelmixer", "Channel Mixer", "通道混合器"),
("ec.color.colorama", "Colorama", "色光"),
("ec.color.colorbalance", "Color Balance", "颜色平衡"),
("ec.color.colorbalancehls", "Color Balance (HLS)", "颜色平衡 (HLS)"),
("ec.color.colorlink", "Color Link", "颜色链接"),
("ec.color.colorstabilizer", "Color Stabilizer", "颜色稳定器"),
("ec.color.curves", "Curves", "曲线"),
("ec.color.equalize", "Equalize", "色调均化"),
("ec.color.exposure", "Exposure", "曝光度"),
("ec.color.gammapedestalgain", "Gamma/Pedestal/Gain", "灰度系数/基值/增益"),
("ec.color.huesaturation", "Hue/Saturation", "色相/饱和度"),
("ec.color.leavecolor", "Leave Color", "保留颜色"),
("ec.color.levels", "Levels", "色阶"),
("ec.color.levelsic", "Levels (Individual Controls)", "色阶 (单独控件)"),
("ec.color.lumetri", "Lumetri Color", "Lumetri 颜色"),
("ec.color.ociocdl", "OCIO CDL Transform", "OCIO CDL 变换"),
("ec.color.ociocolorspace", "OCIO Color Space Transform", "OCIO 颜色空间变换"),
("ec.color.ociodisplay", "OCIO Display Transform", "OCIO 显示变换"),
("ec.color.ociofile", "OCIO File Transform", "OCIO 文件转换"),
("ec.color.ociolook", "OCIO Look Transform", "OCIO 外观变换"),
("ec.color.photofilter", "Photo Filter", "照片滤镜"),
("ec.color.psarbitrarymap", "PS Arbitrary Map", "PS 任意映射"),
("ec.color.selectivecolor", "Selective Color", "可选颜色"),
("ec.color.shadowhighlight", "Shadow/Highlight", "阴影/高光"),
("ec.color.tint", "Tint", "色调"),
("ec.color.tritone", "Tritone", "三色调"),
("ec.color.vibrance", "Vibrance", "自然饱和度"),
("ec.color.videolimiter", "Video Limiter", "视频限幅器"),
("ec.control.angle", "Angle Control", "角度控制"),
("ec.control.checkbox", "Checkbox Control", "复选框控件"),
("ec.control.color", "Color Control", "颜色控制"),
("ec.control.dropdown", "Dropdown Menu Control", "下拉菜单控件"),
("ec.control.layer", "Layer Control", "图层控制"),
("ec.control.point", "Point Control", "点控制"),
("ec.control.point3d", "3D Point Control", "3D 点控制"),
("ec.control.slider", "Slider Control", "滑块控制"),
("ec.control.stereo3d", "Stereo 3D Controls", "立体 3D 控制"),
("ec.distort.bezierwarp", "Bezier Warp", "贝塞尔曲线变形"),
("ec.distort.bulge", "Bulge", "凸出"),
("ec.distort.ccbender", "CC Bender", "CC 弯曲器"),
("ec.distort.ccbendit", "CC Bend It", "CC 弯曲"),
("ec.distort.ccblobbylize", "CC Blobbylize", "CC 融化"),
("ec.distort.ccflomotion", "CC Flo Motion", "CC 流动运动"),
("ec.distort.ccgriddler", "CC Griddler", "CC 网格生成器"),
("ec.distort.cclens", "CC Lens", "CC 镜头"),
("ec.distort.ccpageturn", "CC Page Turn", "CC 翻页"),
("ec.distort.ccpowerpin", "CC Power Pin", "CC 强力边角定位"),
("ec.distort.ccripplepulse", "CC Ripple Pulse", "CC 波纹脉冲"),
("ec.distort.ccslant", "CC Slant", "CC 倾斜"),
("ec.distort.ccsmear", "CC Smear", "CC 漩涡条纹"),
("ec.distort.ccsplit", "CC Split", "CC 拆分"),
("ec.distort.ccsplit2", "CC Split 2", "CC 拆分 2"),
("ec.distort.cctiler", "CC Tiler", "CC 拼贴器"),
("ec.distort.cornerpin", "Corner Pin", "边角定位"),
("ec.distort.displacementmap", "Displacement Map", "置换图"),
("ec.distort.liquify", "Liquify", "液化"),
("ec.distort.magnify", "Magnify", "放大"),
("ec.distort.meshwarp", "Mesh Warp", "网格变形"),
("ec.distort.mirror", "Mirror", "镜像"),
("ec.distort.offset", "Offset", "偏移"),
("ec.distort.opticscompensation", "Optics Compensation", "光学补偿"),
("ec.distort.polar", "Polar Coordinates", "极坐标"),
("ec.distort.puppet", "Puppet", "操控"),
("ec.distort.reshape", "Reshape", "改变形状"),
("ec.distort.ripple", "Ripple", "波纹"),
("ec.distort.rollingshutterrepair", "Rolling Shutter Repair", "果冻效应修复"),
("ec.distort.smear", "Smear", "涂抹"),
("ec.distort.spherize", "Spherize", "球面化"),
("ec.distort.transform", "Transform", "变换"),
("ec.distort.turbulentdisplace", "Turbulent Displace", "湍流置换"),
("ec.distort.twirl", "Twirl", "旋转扭曲"),
("ec.distort.twirllegacy", "Twirl (Legacy)", "旋转扭曲 (旧版)"),
("ec.distort.upscale", "Detail-preserving Upscale", "保留细节放大"),
("ec.distort.warp", "Warp", "变形"),
("ec.distort.warpstabilizer", "Warp Stabilizer", "变形稳定器"),
("ec.distort.wavewarp", "Wave Warp", "波形变形"),
("ec.generate.advancedlightning", "Advanced Lightning", "高级闪电"),
("ec.generate.audiospectrum", "Audio Spectrum", "音频频谱"),
("ec.generate.audiowaveform", "Audio Waveform", "音频波形"),
("ec.generate.beam", "Beam", "光束"),
("ec.generate.ccgluegun", "CC Glue Gun", "CC 胶水枪"),
("ec.generate.cclightburst", "CC Light Burst 2.5", "CC 光束放射 2.5"),
("ec.generate.cclightrays", "CC Light Rays", "CC 光线"),
("ec.generate.cclightsweep", "CC Light Sweep", "CC 光线扫描"),
("ec.generate.ccthreads", "CC Threads", "CC 编织条"),
("ec.generate.cellpattern", "Cell Pattern", "单元格图案"),
("ec.generate.checkerboard", "Checkerboard", "棋盘"),
("ec.generate.circle", "Circle", "圆形"),
("ec.generate.ellipse", "Ellipse", "椭圆"),
("ec.generate.eyedropperfill", "Eyedropper Fill", "吸管填充"),
("ec.generate.fill", "Fill", "填充"),
("ec.generate.fourcolor", "4-Color Gradient", "四色渐变"),
("ec.generate.fractal", "Fractal", "分形"),
("ec.generate.gradientramp", "Gradient Ramp", "梯度渐变"),
("ec.generate.grid", "Grid", "网格"),
("ec.generate.lensflare", "Lens Flare", "镜头光晕"),
("ec.generate.paintbucket", "Paint Bucket", "油漆桶"),
("ec.generate.radiowaves", "Radio Waves", "无线电波"),
("ec.generate.scribble", "Scribble", "涂写"),
("ec.generate.stroke", "Stroke", "描边"),
("ec.generate.vegas", "Vegas", "勾画"),
("ec.generate.writeon", "Write-on", "写入"),
("ec.key.advancedspill", "Advanced Spill Suppressor", "高级溢出抑制器"),
("ec.key.ccsimplewireremoval", "CC Simple Wire Removal", "CC 简单丝线移除"),
("ec.key.colordifference", "Color Difference Key", "颜色差值键"),
("ec.key.colorkey", "Color Key", "颜色键"),
("ec.key.colorrange", "Color Range", "颜色范围"),
("ec.key.differencematte", "Difference Matte", "差值遮罩"),
("ec.key.extract", "Extract", "提取"),
("ec.key.innerouter", "Inner/Outer Key", "内部/外部键"),
("ec.key.keycleaner", "Key Cleaner", "抠像清除键"),
("ec.key.linearcolor", "Linear Color Key", "线性颜色键"),
("ec.key.luma", "Luma Key", "亮度键"),
("ec.key.screen", "Screen Key", "屏幕键"),
("ec.key.spill", "Spill Suppressor", "溢出抑制"),
("ec.key.unmult", "Unmult", "Unmult"),
("ec.keying.keylight", "Key Light", "Key Light"),
("ec.matte.mattechoker", "Matte Choker", "遮罩阻塞工具"),
("ec.matte.refinehard", "Refine Hard Matte", "调整实边遮罩"),
("ec.matte.refinesoft", "Refine Soft Matte", "调整柔和遮罩"),
("ec.matte.rotobrush", "Roto Brush & Refine Edge", "Roto 笔刷和细化边缘"),
("ec.matte.simplechoker", "Simple Choker", "简单阻塞工具"),
("ec.noise.addgrain", "Add Grain", "添加颗粒"),
("ec.noise.curlnoise", "Curl Noise", "旋度噪声"),
("ec.noise.dustscratches", "Dust & Scratches", "蒙尘与划痕"),
("ec.noise.fractal", "Fractal Noise", "分形杂色"),
("ec.noise.matchgrain", "Match Grain", "匹配颗粒"),
("ec.noise.median", "Median", "中间值"),
("ec.noise.medianlegacy", "Median (Legacy)", "中间值 (旧版)"),
("ec.noise.noise", "Noise", "杂色"),
("ec.noise.noisealpha", "Noise Alpha", "杂色 Alpha"),
("ec.noise.noisehls", "Noise HLS", "杂色 HLS"),
("ec.noise.noisehlsauto", "Noise HLS Auto", "杂色 HLS 自动"),
("ec.noise.removegrain", "Remove Grain", "移除颗粒"),
("ec.noise.turbulent", "Turbulent Noise", "湍流杂色"),
("ec.obsolete.basic3d", "Basic 3D", "基本 3D"),
("ec.obsolete.basictext", "Basic Text", "基本文字"),
("ec.obsolete.gaussianlegacy", "Gaussian Blur (Legacy)", "高斯模糊 (旧版)"),
("ec.obsolete.lightning", "Lightning", "闪电"),
("ec.obsolete.mochashape", "Mocha shape", "Mocha 形状"),
("ec.obsolete.pathtext", "Path Text", "路径文本"),
("ec.paint.paint", "Paint", "绘画"),
("ec.perspective.3dglasses", "3D Glasses", "3D 眼镜"),
("ec.perspective.bevelalpha", "Bevel Alpha", "斜面 Alpha"),
("ec.perspective.beveledges", "Bevel Edges", "边缘斜面"),
("ec.perspective.cameratracker", "3D Camera Tracker", "3D 摄像机跟踪器"),
("ec.perspective.cccylinder", "CC Cylinder", "CC 圆柱体"),
("ec.perspective.ccenvironment", "CC Environment", "CC 环境"),
("ec.perspective.ccsphere", "CC Sphere", "CC 球体"),
("ec.perspective.ccspotlight", "CC Spotlight", "CC 聚光"),
("ec.perspective.dropshadow", "Drop Shadow", "投影"),
("ec.perspective.radialshadow", "Radial Shadow", "径向阴影"),
("ec.sim.carddance", "Card Dance", "卡片动画"),
("ec.sim.caustics", "Caustics", "焦散"),
("ec.sim.ccballaction", "CC Ball Action", "CC 球动作"),
("ec.sim.ccbubbles", "CC Bubbles", "CC 气泡"),
("ec.sim.ccdrizzle", "CC Drizzle", "CC 细雨波纹"),
("ec.sim.cchair", "CC Hair", "CC 毛发"),
("ec.sim.ccmrmercury", "CC Mr. Mercury", "CC 水银流体"),
("ec.sim.ccparticlesystems2", "CC Particle Systems II", "CC 粒子系统 II"),
("ec.sim.ccparticleworld", "CC Particle World", "CC 粒子世界"),
("ec.sim.ccpixelpolly", "CC Pixel Polly", "CC 像素多边形"),
("ec.sim.ccrainfall", "CC Rainfall", "CC 降雨"),
("ec.sim.ccscatterize", "CC Scatterize", "CC 散射"),
("ec.sim.ccsnowfall", "CC Snowfall", "CC 降雪"),
("ec.sim.ccstarburst", "CC Star Burst", "CC 星爆"),
("ec.sim.foam", "Foam", "泡沫"),
("ec.sim.particleplayground", "Particle Playground", "粒子运动场"),
("ec.sim.shatter", "Shatter", "碎片"),
("ec.sim.waveworld", "Wave World", "波形环境"),
("ec.stylize.brushstrokes", "Brush Strokes", "画笔描边"),
("ec.stylize.cartoon", "Cartoon", "卡通"),
("ec.stylize.ccblockload", "CC Block Load", "CC 块加载"),
("ec.stylize.ccburnfilm", "CC Burn Film", "CC 烧毁胶片"),
("ec.stylize.ccglass", "CC Glass", "CC 玻璃"),
("ec.stylize.cchextile", "CC HexTile", "CC 六边形拼贴"),
("ec.stylize.cckaleida", "CC Kaleida", "CC 万花筒"),
("ec.stylize.ccmrsmoothie", "CC Mr. Smoothie", "CC 像素溶解"),
("ec.stylize.ccplastic", "CC Plastic", "CC 塑料"),
("ec.stylize.ccrepetile", "CC RepeTile", "CC 重复拼贴"),
("ec.stylize.ccthreshold", "CC Threshold", "CC 阈值"),
("ec.stylize.ccthresholdrgb", "CC Threshold RGB", "CC 阈值 RGB"),
("ec.stylize.ccvignette", "CC Vignette", "CC 暗角"),
("ec.stylize.coloremboss", "Color Emboss", "彩色浮雕"),
("ec.stylize.emboss", "Emboss", "浮雕"),
("ec.stylize.findedges", "Find Edges", "查找边缘"),
("ec.stylize.glow", "Glow", "发光"),
("ec.stylize.mosaic", "Mosaic", "马赛克"),
("ec.stylize.motiontile", "Motion Tile", "动态拼贴"),
("ec.stylize.posterize", "Posterize", "色调分离"),
("ec.stylize.roughenedges", "Roughen Edges", "毛边"),
("ec.stylize.scatter", "Scatter", "散布"),
("ec.stylize.strobe", "Strobe Light", "闪光灯"),
("ec.stylize.texturize", "Texturize", "纹理化"),
("ec.stylize.threshold", "Threshold", "阈值"),
("ec.text.numbers", "Numbers", "编号"),
("ec.text.timecode", "Timecode", "时间码"),
("ec.time.ccforcemotionblur", "CC Force Motion Blur", "CC 强制运动模糊"),
("ec.time.ccwidetime", "CC Wide Time", "CC 宽泛时间"),
("ec.time.echo", "Echo", "残影"),
("ec.time.pixelmotionblur", "Pixel Motion Blur", "像素运动模糊"),
("ec.time.posterizetime", "Posterize Time", "抽帧"),
("ec.time.timedifference", "Time Difference", "时差"),
("ec.time.timedisplacement", "Time Displacement", "时间置换"),
("ec.time.timewarp", "Timewarp", "时间扭曲"),
("ec.transition.blockdissolve", "Block Dissolve", "块溶解"),
("ec.transition.cardwipe", "Card Wipe", "卡片擦除"),
("ec.transition.ccglasswipe", "CC Glass Wipe", "CC 玻璃擦除"),
("ec.transition.ccgridwipe", "CC Grid Wipe", "CC 网格擦除"),
("ec.transition.ccimagewipe", "CC Image Wipe", "CC 图像擦除"),
("ec.transition.ccjaws", "CC Jaws", "CC 锯齿"),
("ec.transition.cclightwipe", "CC Light Wipe", "CC 光线擦除"),
("ec.transition.cclinesweep", "CC Line Sweep", "CC 线条扫掠"),
("ec.transition.ccradialscalewipe", "CC Radial ScaleWipe", "CC 径向缩放擦除"),
("ec.transition.ccscalewipe", "CC Scale Wipe", "CC 缩放擦除"),
("ec.transition.cctwister", "CC Twister", "CC 扭曲器"),
("ec.transition.ccwarpomatic", "CC WarpoMatic", "CC 自动扭曲"),
("ec.transition.gradientwipe", "Gradient Wipe", "渐变擦除"),
("ec.transition.iriswipe", "Iris Wipe", "光圈擦除"),
("ec.transition.linearwipe", "Linear Wipe", "线性擦除"),
("ec.transition.radialwipe", "Radial Wipe", "径向擦除"),
("ec.transition.venetian", "Venetian Blinds", "百叶窗"),
("ec.utility.applylut", "Apply Color LUT", "应用颜色 LUT"),
("ec.utility.ccoverbrights", "CC Overbrights", "CC 高光溢出"),
("ec.utility.cineon", "Cineon Converter", "Cineon 转换器"),
("ec.utility.colorprofileconverter", "Color Profile Converter", "颜色配置文件转换器"),
("ec.utility.facemeasurements", "Face Measurements", "面部测量"),
("ec.utility.facetrackpoints", "Face Track Points", "面部追踪点"),
("ec.utility.growbounds", "Grow Bounds", "范围扩散"),
("ec.utility.hdrcompander", "HDR Compander", "HDR 压缩扩展器"),
("ec.utility.hdrcompression", "HDR Highlight Compression", "HDR 高光压缩"),
("ec.vr.blur", "VR Blur", "VR 模糊"),
("ec.vr.chromaticaberrations", "VR Chromatic Aberrations", "VR 色差"),
("ec.vr.colorgradients", "VR Color Gradients", "VR 颜色渐变"),
("ec.vr.converter", "VR Converter", "VR 转换器"),
("ec.vr.denoise", "VR De-Noise", "VR 降噪"),
("ec.vr.digitalglitch", "VR Digital Glitch", "VR 数字故障"),
("ec.vr.fractalnoise", "VR Fractal Noise", "VR 分形杂色"),
("ec.vr.glow", "VR Glow", "VR 发光"),
("ec.vr.planetosphere", "VR Plane to Sphere", "VR 平面到球面"),
("ec.vr.rotatesphere", "VR Rotate Sphere", "VR 旋转球面"),
("ec.vr.sharpen", "VR Sharpen", "VR 锐化"),
("ec.vr.spheretoplane", "VR Sphere to Plane", "VR 球面到平面"),
];
/// Traditional Chinese (`zh-hant`): no rows yet, so the language draws the English names.
const TRADITIONAL_CHINESE: &[Row] = &[];
/// The 21 category folder names the generated Effect menu and Effects & Presets draw. The wording
/// follows the effect names above (channel → 通道, blur → 模糊, noise → 杂色, matte → 遮罩 …);
/// Adobe's Chinese interface has not been read for these 21, so they are our wording.
const CATEGORIES: &[(&str, &str)] = &[
("3D Channel", "3D 通道"),
("Audio", "音频"),
("Blur & Sharpen", "模糊和锐化"),
("Channel", "通道"),
("Color Correction", "颜色校正"),
("Distort", "扭曲"),
("Expression Controls", "表达式控制"),
("Generate", "生成"),
("Immersive Video", "沉浸式视频"),
("Keying", "键控"),
("Matte", "遮罩"),
("Noise & Grain", "杂色和颗粒"),
("Obsolete", "已废弃"),
("Paint", "绘画"),
("Perspective", "透视"),
("Simulation", "模拟"),
("Stylize", "风格化"),
("Text", "文本"),
("Time", "时间"),
("Transition", "过渡"),
("Utility", "实用工具"),
];
#[cfg(test)]
mod tests {
use super::*;
use crate::i18n::set_current;
use effectcraft_engine::effects;
/// Every effect in the registry has a row, and that row still carries the registry's English
/// name. A new effect, a rename or a removal fails here rather than drawing an English name
/// in a Chinese interface, or attaching a translation to a different effect.
#[test]
fn every_effect_has_a_row() {
let registry = effects::registry();
assert_eq!(SIMPLIFIED_CHINESE.len(), registry.len(), "the catalog has {} rows for {} effects", SIMPLIFIED_CHINESE.len(), registry.len());
for spec in registry {
let Some((_, english, translated)) = SIMPLIFIED_CHINESE.iter().find(|(id, _, _)| *id == spec.id) else {
panic!("no catalog row for {} ({})", spec.id, spec.name);
};
assert_eq!(*english, spec.name, "{}: the catalog says {english:?}, the registry {:?}", spec.id, spec.name);
assert!(!translated.is_empty(), "{}: empty translation", spec.id);
}
}
/// Rows are keyed by id, so a repeated id would shadow a translation.
#[test]
fn ids_are_unique() {
let mut ids: Vec<&str> = SIMPLIFIED_CHINESE.iter().map(|(id, _, _)| *id).collect();
let listed = ids.len();
ids.sort_unstable();
ids.dedup();
assert_eq!(listed, ids.len(), "an effect id is listed twice");
}
/// The five names Adobe leaves in English as brand names, and the only rows that may: any
/// other row repeating its English name is a translation that never happened.
#[test]
fn only_the_brand_names_stay_in_english() {
const BRANDS: [&str; 5] = ["ec.3d.cryptomatte", "ec.3d.extractor", "ec.3d.identifier", "ec.keying.keylight", "ec.key.unmult"];
for (id, english, translated) in SIMPLIFIED_CHINESE {
assert_eq!(english == translated, BRANDS.contains(id), "{id}: {english:?} is drawn as {translated:?}");
}
}
/// Traditional Chinese may lag Simplified Chinese but may not invent ids or disagree about
/// which effect an id names: a translator fills this table row by row.
#[test]
fn traditional_chinese_only_uses_known_ids() {
for (id, english, translated) in TRADITIONAL_CHINESE {
let Some((_, hans_english, _)) = SIMPLIFIED_CHINESE.iter().find(|(hans_id, _, _)| hans_id == id) else {
panic!("zh-hant row {id} is not an effect the zh-hans catalog knows");
};
assert_eq!(*english, *hans_english, "{id}: the two catalogs name the effect differently");
assert!(!translated.is_empty(), "{id}: empty translation");
}
}
/// Every category the registry and its plug-ins report has a row, and every row names a
/// category that exists: the generated Effect menu and Effects & Presets draw both.
#[test]
fn every_category_has_a_row() {
let categories = effects::categories();
for category in &categories {
assert!(CATEGORIES.iter().any(|(english, _)| english == category), "no catalog row for category {category}");
}
for (english, translated) in CATEGORIES {
assert!(categories.contains(english), "the catalog lists {english}, which is not a category");
assert!(!translated.is_empty(), "empty translation for {english}");
}
}
/// A repeated effect carries the index the engine appends to the group name; a name the user
/// typed in Effect Controls stays verbatim.
#[test]
fn group_names_keep_the_instance_index_and_user_renames() {
set_current("zh-hans");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur"), "高斯模糊");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur 2"), "高斯模糊 2");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur 12"), "高斯模糊 12");
assert_eq!(group("ec.blur.gaussian", "My Blur"), "My Blur", "a rename is not a row");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur X"), "Gaussian Blur X");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur 2x"), "Gaussian Blur 2x");
assert_eq!(group("ec.blur.gaussian", "高斯模糊"), "高斯模糊", "an already translated name stays");
set_current("en");
}
/// English, and every language without rows (Traditional Chinese included), draws the
/// registry's name; an id with no row keeps it too rather than drawing an empty label.
#[test]
fn languages_without_rows_draw_the_registry_name() {
for language in ["en", "ja", "zh-hant", "uk", "cs"] {
set_current(language);
assert_eq!(name("ec.blur.gaussian", "Gaussian Blur"), "Gaussian Blur", "{language}");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur 2"), "Gaussian Blur 2", "{language}");
assert_eq!(category("Blur & Sharpen"), "Blur & Sharpen", "{language}");
}
set_current("zh-hans");
assert_eq!(name("ec.blur.gaussian", "Gaussian Blur"), "高斯模糊");
assert_eq!(group("ec.blur.gaussian", "Gaussian Blur 2"), "高斯模糊 2");
assert_eq!(category("Blur & Sharpen"), "模糊和锐化");
assert_eq!(name("ec.unknown.effect", "Mystery"), "Mystery");
assert_eq!(category("Not A Category"), "Not A Category");
set_current("en");
}
/// The Effects & Presets search answers to either name, so a Chinese interface can be
/// searched in Chinese while an English one still finds the effect by its registry name.
#[test]
fn search_matches_either_name() {
set_current("zh-hans");
assert!(matches("ec.blur.gaussian", "Gaussian Blur", "blur"));
assert!(matches("ec.blur.gaussian", "Gaussian Blur", "gaussian"));
assert!(matches("ec.blur.gaussian", "Gaussian Blur", "高斯"));
assert!(!matches("ec.blur.gaussian", "Gaussian Blur", "sharpen"));
set_current("en");
assert!(matches("ec.blur.gaussian", "Gaussian Blur", "blur"));
assert!(!matches("ec.blur.gaussian", "Gaussian Blur", "高斯"));
}
/// The drawing sites ask this catalog instead of the registry's English name (#821). Like the
/// panel catalog's literal scan this is a heuristic: it pins the expressions that used to draw
/// the English name, so a revert fails here.
#[test]
fn the_drawing_sites_ask_the_catalog() {
/// A file that must ask the catalog.
const ASKS: &[(&str, &str, &str)] = &[
("i18n.rs", include_str!("../i18n.rs"), "effects::name("),
("i18n.rs", include_str!("../i18n.rs"), "effects::category("),
("panels/effects_presets.rs", include_str!("../panels/effects_presets.rs"), "effects::name("),
("panels/effects_presets.rs", include_str!("../panels/effects_presets.rs"), "effects::category("),
("panels/effect_controls.rs", include_str!("../panels/effect_controls.rs"), "effects::group("),
("panels/timeline.rs", include_str!("../panels/timeline.rs"), "effects::group("),
];
/// An expression that drew the registry's English name and must be gone.
const RAW: &[(&str, &str, &str)] = &[
("panels/effects_presets.rs", include_str!("../panels/effects_presets.rs"), "pos2(r.min.x + 56.0 + indent, r.center().y), e.name, query, &t)"),
("panels/effect_controls.rs", include_str!("../panels/effect_controls.rs"), "Align2::LEFT_CENTER, &g.name,"),
("panels/timeline.rs", include_str!("../panels/timeline.rs"), "Align2::LEFT_CENTER, name,"),
];
for (path, source, call) in ASKS {
assert!(source.contains(call), "{path} does not ask the effect-name catalog (`{call}`)");
}
for (path, source, raw) in RAW {
assert!(!source.contains(raw), "{path} draws the registry's English name again: `{raw}`");
}
}
}
+4 -1
View File
@@ -1914,7 +1914,10 @@ fn menu_nodes(
}
MenuNode::Submenu { label, children } => {
let ws = app.ui.workspace.clone();
let shown = crate::i18n::submenu(app, label, effectcraft_engine::menus::submenu_label(&app.session, label, &dyn_ctx(&ws, &[])));
// The generated Effect menu's category submenus draw the catalog's name (#821);
// every other submenu goes through the menu catalog.
let shown = crate::i18n::effect_category(children, label)
.unwrap_or_else(|| crate::i18n::submenu(app, label, effectcraft_engine::menus::submenu_label(&app.session, label, &dyn_ctx(&ws, &[]))));
let r = ui.menu_button((gutter(false), shown.as_str()), |ui| {
ui.set_min_width(if children.len() > 30 { 200.0 } else { 240.0 });
crate::widgets::menu_scroll(ui, |ui| menu_nodes(app, ui, children, clicked, nav, depth + 1));
+10 -2
View File
@@ -169,8 +169,16 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
MenuNode::Submenu { label, children } => {
let mut kids = vec![];
rec(app, children, &id, &mut kids);
let shown = effectcraft_engine::menus::submenu_label(&app.session, label, &crate::menus::dyn_ctx(&app.ui.workspace, &[]));
out.push(NativeNode::Submenu { label: crate::i18n::submenu(app, label, shown), children: kids });
// The generated Effect menu's category submenus draw the catalog's name
// (#821); every other submenu goes through the menu catalog.
let shown = crate::i18n::effect_category(children, label).unwrap_or_else(|| {
crate::i18n::submenu(
app,
label,
effectcraft_engine::menus::submenu_label(&app.session, label, &crate::menus::dyn_ctx(&app.ui.workspace, &[])),
)
});
out.push(NativeNode::Submenu { label: shown, children: kids });
}
MenuNode::Item(e) => {
let role = match e.command.as_str() {
+15 -7
View File
@@ -1100,6 +1100,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let mut tops: Vec<(u64, f32)> = vec![];
for (fi, g) in fx.iter().enumerate() {
let GroupKind::Effect { effect } = &g.kind else { continue };
// The header draws the effect's name in the interface language; the automation labels
// below keep the property tree's name, which is the registry's English name (#821).
let shown = crate::i18n::effects::group(effect, &g.name);
let r = Rect::from_min_size(pos2(body.min.x, y), vec2(body.width(), ROW + 2.0));
tops.push((g.uid, r.min.y));
y += ROW + 2.0;
@@ -1140,14 +1143,15 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let commit = response.lost_focus() || (response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)));
if escape || commit {
ctx.data_mut(|d| d.remove::<String>(rename_id));
if !escape && !value.trim().is_empty() && value != g.name {
// The field opens with the drawn name, so committing it unchanged is not a rename.
if !escape && !value.trim().is_empty() && value != shown {
actions.push(("prop.renameGroup".into(), json!({"layer": layer.id.0, "prop": g.uid, "name": value})));
}
} else {
ctx.data_mut(|d| d.insert_temp(rename_id, value));
}
} else {
let name = bp.text(pos2(tw.max.x + 6.0, r.center().y), Align2::LEFT_CENTER, &g.name, Tokens::semibold(12.0), t.text);
let name = bp.text(pos2(tw.max.x + 6.0, r.center().y), Align2::LEFT_CENTER, &shown, Tokens::semibold(12.0), t.text);
app.auto.add(&format!("effectControls.effect.{}.name", g.uid), name, &g.name);
}
let rresp = ui.interact(reset, egui::Id::new(("ec-reset", g.uid)), Sense::click());
@@ -1166,7 +1170,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
widgets::open_popup(ui, apop);
}
if let Some(spec) = effectcraft_engine::effects::find(effect) {
let lines = vec![spec.name.to_string(), format!("Category: {}", spec.category), "EffectCraft built-in effect (MIT OR Apache-2.0)".to_string()];
let lines = vec![
crate::i18n::effects::name(spec.id, spec.name),
format!("Category: {}", crate::i18n::effects::category(spec.category)),
"EffectCraft built-in effect (MIT OR Apache-2.0)".to_string(),
];
let _ = widgets::popup_menu(ui, apop, about.left_bottom(), &lines, None);
}
let hresp = ui.interact(
@@ -1175,10 +1183,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if editing.is_some() { Sense::hover() } else { Sense::click_and_drag() },
);
if hresp.double_clicked() {
ctx.data_mut(|d| d.insert_temp(rename_id, g.name.clone()));
ctx.data_mut(|d| d.insert_temp(rename_id, shown.clone()));
ctx.data_mut(|d| d.insert_temp(rename_id.with("start"), true));
}
hresp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, active, &g.name));
hresp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, active, &shown));
app.auto.add(&format!("effectControls.effect.{}", g.uid), r, &g.name);
if hresp.clicked() || hresp.drag_started() || hresp.secondary_clicked() {
let add = ui.input(|i| i.modifiers.shift || i.modifiers.command);
@@ -1207,7 +1215,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
item(ui, "Paste", has_clip, "effect.paste", json!({"layers": [lid]}));
item(ui, "Duplicate", true, "effect.duplicate", json!({"layer": lid, "effect": g.uid}));
if ui.button(crate::i18n::tr("Rename")).clicked() {
ctx.data_mut(|d| d.insert_temp(rename_id, g.name.clone()));
ctx.data_mut(|d| d.insert_temp(rename_id, shown.clone()));
ctx.data_mut(|d| d.insert_temp(rename_id.with("start"), true));
ui.close();
}
@@ -1532,7 +1540,7 @@ pub fn viewer_hook(
let Some(resp) = resp else { continue };
if resp.hovered() {
ctx.set_cursor_icon(egui::CursorIcon::Move);
resp.clone().on_hover_text(format!("{}: {}", g.name, pr.name));
resp.clone().on_hover_text(format!("{}: {}", crate::i18n::effects::group(&g.match_id, &g.name), pr.name));
}
if resp.dragged()
&& let Some(pos) = resp.interact_pointer_pos()
+13 -6
View File
@@ -185,7 +185,10 @@ fn effect_row(
lp.rect_filled(r, 0.0, t.hover);
}
icons::paint(lp, Rect::from_center_size(pos2(r.min.x + 44.0 + indent, r.center().y), vec2(12.0, 12.0)), Icon::Fx, t.text_dim);
highlighted(lp, pos2(r.min.x + 56.0 + indent, r.center().y), e.name, query, &t);
// The row draws the effect's name in the interface language; the automation label keeps the
// registry's English name, which is what scripts and the control channel address (#821).
let shown = crate::i18n::effects::name(e.id, e.name);
highlighted(lp, pos2(r.min.x + 56.0 + indent, r.center().y), &shown, query, &t);
if e.float {
lp.text(pos2(r.max.x - 10.0, r.center().y), Align2::RIGHT_CENTER, "32", Tokens::ui(9.5), t.text_faint);
}
@@ -322,8 +325,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let mut y = list.min.y - scroll.offset;
let mut apply: Option<Apply> = None;
let reg = effectcraft_engine::effects::all();
let keep =
|e: &effectcraft_engine::effects::EffectSpec| depth_keeps(&view.depth, e) && (query.is_empty() || effectcraft_engine::effects::name_matches(e, &query));
let keep = |e: &effectcraft_engine::effects::EffectSpec| {
depth_keeps(&view.depth, e)
&& (query.is_empty() || effectcraft_engine::effects::name_matches(e, &query) || crate::i18n::effects::matches(e.id, e.name, &query))
};
let lookup = |id: &str| reg.iter().find(|e| e.id == id);
// Favorites and Recently Used.
@@ -405,7 +410,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if items.is_empty() {
continue;
}
if !folder_row(app, ui, &lp, list, &mut y, cat, cat, 0.0, searching, None) {
// The folder draws the category in the interface language; `cat` stays the key (the
// open/closed state and the automation id are the registry's English name).
if !folder_row(app, ui, &lp, list, &mut y, cat, &crate::i18n::effects::category(cat), 0.0, searching, None) {
continue;
}
for e in items {
@@ -421,9 +428,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if let Some(DragPayload::Effect(id)) = egui::DragAndDrop::payload::<DragPayload>(&ctx).as_deref()
&& let Some(pos) = ctx.pointer_hover_pos()
{
let name = effectcraft_engine::effects::find(id).map(|e| e.name).unwrap_or("");
let name = effectcraft_engine::effects::find(id).map(|e| crate::i18n::effects::name(e.id, e.name)).unwrap_or_default();
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("dnd-fx")));
let g = painter.layout_no_wrap(name.to_string(), Tokens::ui(12.0), t.text);
let g = painter.layout_no_wrap(name, Tokens::ui(12.0), t.text);
let r = Rect::from_min_size(pos + vec2(12.0, 8.0), g.size() + vec2(16.0, 8.0));
painter.rect_filled(r, 4.0, Color32::from_rgba_premultiplied(40, 40, 40, 230));
painter.rect_stroke(r, 4.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
+17 -2
View File
@@ -684,7 +684,11 @@ fn search_rows(l: &Layer, q: &str) -> Vec<Row> {
for c in &g.children {
match c {
Node::Group(sg) if depth > 1 || group_visible(sg, l) => {
if sg.name.to_lowercase().contains(q) {
// An effect group also matches the name the row draws, so a search finds
// `高斯模糊` in a Chinese interface and `Gaussian Blur` in an English one.
let hit = sg.name.to_lowercase().contains(q)
|| matches!(&sg.kind, GroupKind::Effect { effect } if crate::i18n::effects::matches(effect, &sg.name, q));
if hit {
out.push(group_row(l, sg, depth, g));
all(l, sg, depth + 1, out);
continue;
@@ -2158,6 +2162,17 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
RowKind::Group { uid, name, open, has_children, fx, eye } => {
// The row draws an effect's name in the interface language (#821); `name` stays
// the property tree's, which the automation labels below keep. Any other group
// (a mask, a shape group, a layer style) draws its own name.
let group = layer.props.find_group(*uid);
let shown = match group.and_then(|g| match &g.kind {
GroupKind::Effect { effect } => Some(effect.as_str()),
_ => None,
}) {
Some(effect) => crate::i18n::effects::group(effect, name),
None => name.clone(),
};
// Selected groups (an effect, a mask, a shape group…) highlight like selected
// property rows.
let sel_group = app.session.state.selected_props.contains(&(layer.id, *uid));
@@ -2197,7 +2212,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
app.auto.add(&format!("timeline.group.{uid}.eye"), er, name);
}
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, name, Tokens::ui(12.0), t.text);
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, &shown, Tokens::ui(12.0), t.text);
text_anim_popups(app, ui, &lp, layer, *uid, cw.switches, cy, &mut actions);
shape_add_popup(app, ui, &lp, layer, *uid, cw.switches, cy, &mut actions);
if *uid == GEOM_BIT | layer.id.0 {
+102
View File
@@ -0,0 +1,102 @@
//! Effect names in a Simplified Chinese interface (#821): the generated Effect menu and Effect
//! Controls draw the catalog's name, while the effect ids, the property tree's group name, the
//! automation labels and `effect.list` stay English.
use effectcraft_engine::Session;
use effectcraft_engine::project::LayerId;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use effectcraft_ui_egui::native_menu::{NativeNode, build as native_menu};
use egui::vec2;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
/// The effect under test, and the names the registry and the catalog give it.
const BLUR: &str = "ec.blur.gaussian";
const BLUR_EN: &str = "Gaussian Blur";
const BLUR_ZH: &str = "高斯模糊";
fn harness(language: &str) -> (Harness<'static, EffectcraftApp>, u64) {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360, "frameRate": 30, "duration": 10})).unwrap();
s.execute("layer.newSolid", json!({"name": "Back", "color": "#20c040"})).unwrap();
let layer = s.execute("layer.newSolid", json!({"name": "Blurred", "color": "#ff0000", "width": 200, "height": 100})).unwrap()["layer"].as_u64().unwrap();
let fx = s.execute("effect.apply", json!({"layers": [layer], "effect": BLUR})).unwrap()["effects"][0].as_u64().unwrap();
// A second instance: the engine stores it as "Gaussian Blur 2", which the catalog must keep.
s.execute("effect.apply", json!({"layers": [layer], "effect": BLUR})).unwrap();
s.state.selected_layers = vec![LayerId(layer)];
s.state.selected_props.clear();
s.prefs.general.language = language.to_string();
let mut app = EffectcraftApp::new(s);
app.show_panel(PanelKind::EffectControls);
app.show_panel(PanelKind::EffectsPresets);
app.show_panel(PanelKind::Timeline);
let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).build_eframe(|_| app);
h.run_steps(4);
(h, fx)
}
/// How many widgets draw a label containing `needle` (the Effect Controls header registers its
/// drawn name with the accessibility tree, so a translated name shows up here).
fn drawn(h: &Harness<'_, EffectcraftApp>, needle: &str) -> usize {
h.query_all_by_label_contains(needle).count()
}
/// The label the native menu gives the `effect.apply` entry for effect `id`, under top-level menu
/// `top` and category `category`. The in-window menu bar builds its labels through the same
/// `entry_label`, so this covers both menu bars.
fn menu_label(h: &Harness<'_, EffectcraftApp>, top: &str, category: &str, id: &str) -> Option<String> {
let menu = native_menu(h.state());
let effect = menu.menus.iter().find_map(|n| match n {
NativeNode::Submenu { label, children } if label == top => Some(children),
_ => None,
})?;
effect.iter().find_map(|cat| match cat {
NativeNode::Submenu { label, children } if label == category => children.iter().find_map(|item| match item {
NativeNode::Item(i) if i.command == "effect.apply" && i.params.get("effect").and_then(|v| v.as_str()) == Some(id) => Some(i.label.clone()),
_ => None,
}),
_ => None,
})
}
/// Effect Controls draws the catalog's name, including the instance index the engine appends to
/// a repeat; the English interface draws the registry's name.
#[test]
fn the_effect_controls_header_follows_the_interface_language() {
let (h, _fx) = harness("zh-hans");
assert!(drawn(&h, BLUR_ZH) > 0, "the header draws the Chinese name");
assert_eq!(drawn(&h, "高斯模糊 2"), 1, "the second instance keeps its index");
assert_eq!(drawn(&h, BLUR_EN), 0, "nothing draws the English name");
let (en, _fx) = harness("en");
assert!(drawn(&en, BLUR_EN) > 0);
assert_eq!(drawn(&en, BLUR_ZH), 0);
}
/// The generated Effect menu — its category submenus and its entries — follows the catalog.
#[test]
fn the_generated_effect_menu_is_translated() {
let (h, _fx) = harness("zh-hans");
assert_eq!(menu_label(&h, "效果", "模糊和锐化", BLUR).as_deref(), Some(BLUR_ZH), "the generated entry is translated");
let (en, _fx) = harness("en");
assert_eq!(menu_label(&en, "Effect", "Blur & Sharpen", BLUR).as_deref(), Some(BLUR_EN), "the English menu is unchanged");
}
/// Everything an agent or a script addresses stays English: the automation label, the property
/// tree's group name and `effect.list`.
#[test]
fn the_ids_automation_labels_and_effect_list_stay_english() {
let (mut h, fx) = harness("zh-hans");
assert_eq!(
h.state().auto.find(&format!("effectControls.effect.{fx}")).map(|e| e.label.clone()),
Some(BLUR_EN.to_string()),
"the automation label is the property tree's name"
);
let listed = h.state_mut().session.execute("effect.list", json!({})).unwrap();
let entry = listed.as_array().and_then(|a| a.iter().find(|e| e["id"] == BLUR)).cloned().unwrap_or_default();
assert_eq!(entry["name"], BLUR_EN, "effect.list keeps the registry's name");
assert_eq!(entry["category"], "Blur & Sharpen", "effect.list keeps the registry's category");
}
+1 -1
View File
@@ -72,7 +72,7 @@ are grouped by area; the issue tracker has the rest.
| 13 | **3D model formats and materials** | After Effects 26 imports FBX, USD/USDZ, STL besides glTF and OBJ (`usd_plugins`), and Substance 3D materials with mesh displacement (26.0–26.2); we read glTF, OBJ, STL (binary and ASCII, with facet colours) and FBX 7.x (binary and ASCII: meshes, hierarchy, Lambert/Phong materials and textures; no FBX animation or skinning yet) | Product shots and 3D scenes come as FBX/USDZ | 25–45 h | — · [file-format-parity.md](file-format-parity.md) |
| 14 | **Object Matte** (26.2, disk cache in 26.5) | Not present; MobileSAM is already in `effectcraft-segment` and could back it | The new one-click roto; Roto Brush covers part of it | 15–30 h | G7 · [target-app-parity.md](target-app-parity.md) |
| 15 | **Hardware video decode and encode** | No VideoToolbox, NVENC, Quick Sync or VA-API; #498 asks for it; FilmCraft has VideoToolbox / NVENC / VA-API paths to borrow | 4K playback and export speed | 25–45 h | G5/G6 · [hardware-parity.md](hardware-parity.md) |
| 16 | **Localization depth** | Menus in 6 languages; panels in 4; effect names and ~1,650 effect parameter names untranslated; French, German, Korean, Italian, Russian (After Effects ships them) absent; #599 Hangul boxes on macOS | After Effects is used heavily in Japan, China, Korea, Brazil, Europe | 60–110 h + native review | G9 · [localization-parity.md](localization-parity.md) |
| 16 | **Localization depth** | Menus in 6 languages; panels in 4; effect names translated in Simplified Chinese (306, plus the generated Effect menu), their ~1,650 parameter names untranslated; French, German, Korean, Italian, Russian (After Effects ships them) absent; #599 Hangul boxes on macOS | After Effects is used heavily in Japan, China, Korea, Brazil, Europe | 60–110 h + native review | G9 · [localization-parity.md](localization-parity.md) |
| 17 | **Media Browser lists files import refuses** | `media_browser.rs` lists AVI; `media::is_importable` has none of them (Targa and Radiance HDR import since #745; DPX and Cineon since #756, log shown as stored: no Cineon Settings conversion yet; MXF OP1a / OP-Atom since #767; FilmCraft has no AVI demuxer); #431 any image imports as a sequence; #324 MP3 goes silent | Confusing failures on common VFX formats | 15–30 h | G6 · [file-format-parity.md](file-format-parity.md) |
| 18 | **Relative paths and relinking** | Projects store absolute footage paths; no relink when a project moves | Moving or sharing a project breaks it | 6–12 h | G4 · [file-format-parity.md](file-format-parity.md) |
| 19 | **Plug-in ecosystem** | No After Effects SDK hosting (#600, #327 OpenFX, #334, #376 plug-in manager); our WebAssembly API has no third-party plug-ins | Many pro projects depend on Trapcode, Element, Saber… | 40–80 h for API growth; SDK hosting needs an owner decision | G8 · [scripting-parity.md](scripting-parity.md) |
+17 -7
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Ukrainian panel catalog recounted after it caught up with the Chinese ones) · **Target:** Adobe After Effects 2026
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (effect names and the generated Effect menu go through a catalog, in Simplified Chinese) · **Target:** Adobe After Effects 2026
How much of EffectCraft's interface is translated, language by language, compared with After
Effects. [gaps.md](gaps.md) gap 16 and workstream G9 hold the work;
@@ -19,13 +19,22 @@ interface; Arabic and Hebrew text *in layers* is supported through its Middle Ea
- **Menus:** `crates/ui-egui/src/i18n.rs`, one catalog per language keyed by
`(command, English label)`. A test (`every_translation_key_exists_in_the_actual_menu_tree`)
fails if any fixed menu entry is untranslated or a row is stale, so every catalog covers all
**618** fixed entries. The generated Effect menu (effect names) is deliberately left in English.
**618** fixed entries. The generated Effect menu is not part of that fixed tree: its entries
and category submenus take their labels from the effect catalog below.
- **Panels, dialogs, buttons, tooltips:** `crates/ui-egui/src/i18n/ui.rs`, keyed by the English
source string, with English fallback. Catalogs exist for Japanese, Simplified and Traditional
Chinese, Ukrainian and Czech.
- **Not translated anywhere yet:** effect names and their ≈ 1,650 parameter names
(`crates/effects`), engine-generated text (property names, errors, undo labels), the Home
screen's unwrapped literals (#574), Learn tutorials and documentation.
- **Effect names:** `crates/ui-egui/src/i18n/effects.rs`, keyed by **effect id**, in Simplified
Chinese (306 rows, one per effect in the registry, plus its 21 categories). The registry's
English name stays the effect's identity: `effect.list`, `effect.apply {"effect": …}`, scripts
and expressions match on it, and the property tree stores it as the effect group's name. Only
the drawing — the Effect menu, Effect Controls, the Timeline and Effects & Presets — goes
through the catalog, so a project file is identical whichever interface language opens it.
A repeat (`Gaussian Blur 2`) keeps the index the engine appends, and a group the user renamed
in Effect Controls stays verbatim. No other language has rows yet, so they draw English.
- **Not translated anywhere yet:** effect *parameter* names (≈ 1,650, `crates/effects`),
engine-generated text (property names, errors, undo labels), the Home screen's unwrapped
literals (#574), Learn tutorials and documentation.
- **Language choice:** Settings ▸ General ▸ Language, or Match System
(`match_system_uses_the_system_language_where_there_is_a_catalog`).
- **Fonts:** no bundled CJK font; the UI installs system fallbacks probed by script (Kana, Han,
@@ -48,7 +57,7 @@ the true percentages are slightly lower.
| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | To `full` |
|---|---|---|---|---|---|---|---|
| English | `en` | ≈ 3,900 (100%, source) | All; docs in English | — | yes | full | — |
| Simplified Chinese | `zh-hans` | 1,081 (≈ 28%): menus 618/618, panels 463 | Panels and dialogs partly (≈ 35% of panel strings); no help, effects untranslated; Home screen literals unwrapped (#574) | System CJK fallback; Han follows the UI language; CJK text in comps renders wrongly (#453); IME in text layers | no | partial | 6–9 h + review |
| Simplified Chinese | `zh-hans` | 1,081 (≈ 28%): menus 618/618, panels 463 | Panels and dialogs partly (≈ 35% of panel strings); no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574) | System CJK fallback; Han follows the UI language; CJK text in comps renders wrongly (#453); IME in text layers | no | partial | 6–9 h + review |
| Spanish | `es` | 618 (≈ 16%) | Menus only | Latin, fine | no | menus only | 7–10 h + review |
| Hindi | `hi` | 0 | — | UI: Devanagari fallback font but no shaping; text layers shape Devanagari | no | none (After Effects: none) | 9–13 h + shaping in the UI |
| Arabic | `ar` | 0 | — | UI: Arabic fallback font but no shaping and no RTL layout; text layers: bidi + shaping | no | none (After Effects: none) | 9–13 h + 15–25 h RTL UI |
@@ -75,7 +84,7 @@ After Effects also ships Italian and Russian, which EffectCraft does not (8–12
German, Korean, Italian and Russian 0%). This is the localization row in
[target-app-parity.md](target-app-parity.md#by-dimension).
**Effort.** One-time work first: route effect names, parameter names and engine messages through
**Effort.** One-time work first: route parameter names and engine messages through
a catalog, wrap the remaining panel literals, and add a Hangul probe (10–20 h). Then each language
needs ≈ 2,900 more strings (≈ 5–9 h per language at the pace of #398 and #401). Parity with After
Effects' 10 non-English languages: **≈ 60–110 h**; the four extra languages of the standard list
@@ -89,3 +98,4 @@ can give; none has had it.
|---|---|---|
| 2026-10-10 | major | First localization parity document: catalog rows counted for all six languages, the twelve-language table, After Effects' 11 shipped languages from its bundle |
| 2026-10-11 | minor | Ukrainian panel catalog brought level with the Chinese ones (495 of 496 rows, counted at this commit); its share rises to ≈ 29%. The other languages' panel counts are the 10 October measurement |
| 2026-10-13 | minor | Effect names and the generated Effect menu leave English: `i18n/effects.rs` adds 306 rows keyed by effect id (Simplified Chinese, from After Effects 2026's interface and Adobe's Chinese documentation for the `CC` series) and the 21 category names. Parameter names are still English; no other language has rows |