Merge pull request #1199 from cjcwwww/i18n/effect-names
# Conflicts: # crates/ui-egui/src/panels/timeline.rs # docs/gaps.md # docs/localization-parity.md
This commit is contained in:
commit
32792ff172
|
|
@ -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" => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,652 @@
|
|||
//! 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.
|
||||
//!
|
||||
//! A language decides for itself which names stay English: After Effects' Japanese interface
|
||||
//! leaves the `CC` series, the `VR` set and the `OCIO` set in English, while its Simplified
|
||||
//! Chinese one translates them (the `CC` names come from Adobe's Chinese *documentation*, which
|
||||
//! names 68 of the 69). Repeating the English name is therefore a language-level choice, not a
|
||||
//! missing row: [`tests::only_the_brand_names_stay_in_english`] pins *Simplified Chinese's* five
|
||||
//! brand names and is deliberately not shared by the other languages.
|
||||
//!
|
||||
//! The Simplified Chinese names come from the installed After Effects 2026 (26.5.0.89)
|
||||
//! Simplified Chinese interface, its 21 category folder names included; `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);
|
||||
|
||||
/// One language's catalog: its effect rows and its category labels, the latter keyed by the
|
||||
/// registry's English category name.
|
||||
struct Catalog {
|
||||
rows: &'static [Row],
|
||||
categories: &'static [(&'static str, &'static str)],
|
||||
/// `rows` indexed by effect id, built on first use: the panels look a name up once per drawn
|
||||
/// row and per frame.
|
||||
index: OnceLock<HashMap<&'static str, (&'static str, &'static str)>>,
|
||||
}
|
||||
|
||||
impl Catalog {
|
||||
fn row(&self, id: &str) -> Option<(&'static str, &'static str)> {
|
||||
self.index.get_or_init(|| self.rows.iter().map(|(id, english, translated)| (*id, (*english, *translated))).collect()).get(id).copied()
|
||||
}
|
||||
|
||||
fn category(&self, source: &str) -> Option<&'static str> {
|
||||
self.categories.iter().find(|(english, _)| *english == source).map(|(_, translated)| *translated)
|
||||
}
|
||||
}
|
||||
|
||||
/// The catalog for `language`; `None` for English (the source of every name, needing no lookup)
|
||||
/// and for a language with no table.
|
||||
///
|
||||
/// **Adding a language** is two tables and one arm: write its rows (one per effect, ids from
|
||||
/// [`SIMPLIFIED_CHINESE`]) and its category labels, name them in the `static` below and in this
|
||||
/// `match`. Nothing else changes — not the lookups, not one panel. `zh-hant` and `ja` are listed
|
||||
/// with empty tables, so a translator fills rows in without touching this function, and until
|
||||
/// then those languages draw the registry's English name.
|
||||
fn catalog(language: &str) -> Option<&'static Catalog> {
|
||||
static SIMPLIFIED: Catalog = Catalog { rows: SIMPLIFIED_CHINESE, categories: SIMPLIFIED_CHINESE_CATEGORIES, index: OnceLock::new() };
|
||||
static TRADITIONAL: Catalog = Catalog { rows: TRADITIONAL_CHINESE, categories: TRADITIONAL_CHINESE_CATEGORIES, index: OnceLock::new() };
|
||||
static JAPANESE: Catalog = Catalog { rows: JAPANESE_EFFECTS, categories: JAPANESE_CATEGORIES, index: OnceLock::new() };
|
||||
match language {
|
||||
"zh-hans" => Some(&SIMPLIFIED),
|
||||
"zh-hant" => Some(&TRADITIONAL),
|
||||
// Japanese: the slot the Japanese data point on #821 asked for, so both languages share
|
||||
// this one catalog instead of inventing a second mechanism.
|
||||
"ja" => Some(&JAPANESE),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
catalog(current()).and_then(|c| c.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)) = catalog(current()).and_then(|c| c.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), else `source`.
|
||||
pub(crate) fn category(source: &str) -> String {
|
||||
catalog(current()).and_then(|c| c.category(source)).map_or_else(|| source.to_string(), str::to_string)
|
||||
}
|
||||
|
||||
/// Does effect `id` (`english` when the language has no row) match the Effects & Presets search
|
||||
/// `query` (lowercase)? Both names match, so a translated interface can be searched either way.
|
||||
pub(crate) fn matches(id: &str, english: &str, query: &str) -> bool {
|
||||
english.to_lowercase().contains(query)
|
||||
|| catalog(current()).and_then(|c| c.row(id)).is_some_and(|(_, translated)| translated.to_lowercase().contains(query))
|
||||
}
|
||||
|
||||
/// 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. A translator fills one row per effect, ids from
|
||||
/// [`SIMPLIFIED_CHINESE`]; a row may repeat the English name, which is what the language draws
|
||||
/// for that effect.
|
||||
const TRADITIONAL_CHINESE: &[Row] = &[];
|
||||
|
||||
/// Japanese (`ja`): reserved for the Japanese data point on #821, so both languages share this
|
||||
/// one catalog instead of inventing a second mechanism. A translator fills one row per effect,
|
||||
/// ids from [`SIMPLIFIED_CHINESE`]; the names Adobe's Japanese interface leaves in English (the
|
||||
/// `CC` series, the `VR` set, the `OCIO` set, `Cryptomatte`, `EXtractoR`, `IDentifier`,
|
||||
/// `Key Light`, `Unmult`, `Twirl (Legacy)` and the two EffectCraft-only effects) repeat that
|
||||
/// English name here rather than being left out.
|
||||
const JAPANESE_EFFECTS: &[Row] = &[];
|
||||
|
||||
/// Traditional Chinese (`zh-hant`) category labels, keyed by the registry's English category.
|
||||
const TRADITIONAL_CHINESE_CATEGORIES: &[(&str, &str)] = &[];
|
||||
|
||||
/// Japanese (`ja`) category labels, keyed by the registry's English category.
|
||||
const JAPANESE_CATEGORIES: &[(&str, &str)] = &[];
|
||||
|
||||
/// The 21 category folder names the generated Effect menu and Effects & Presets draw, in
|
||||
/// Simplified Chinese. Read from the installed After Effects 2026 (26.5.0.89) Simplified Chinese
|
||||
/// interface (the checklist's `07-类别` sheet); two of them are the interface's own wording where
|
||||
/// our first suggestion differed (`Keying` is 抠像, not 键控; `Obsolete` is 过时, not 已废弃).
|
||||
const SIMPLIFIED_CHINESE_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's Simplified Chinese interface leaves in English as brand names, and
|
||||
/// the only zh-hans rows that may repeat their English name. Deliberately not shared: another
|
||||
/// language keeps different names in English (Japanese leaves the whole `CC`, `VR` and `OCIO`
|
||||
/// sets in English), so this is zh-hans's own choice, not an invariant of the tables.
|
||||
#[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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The languages that have no rows yet, with the table a translator fills in for each. A new
|
||||
/// language is added here and to [`catalog`]; the test then guards its rows as they arrive.
|
||||
const PENDING: &[(&str, &[Row])] = &[("zh-hant", TRADITIONAL_CHINESE), ("ja", JAPANESE_EFFECTS)];
|
||||
|
||||
/// A pending language may lag Simplified Chinese but may not invent ids or disagree about
|
||||
/// which effect an id names: a translator fills these tables row by row. While they are
|
||||
/// empty the test is a no-op, which is why the slot ships before the data does.
|
||||
#[test]
|
||||
fn a_pending_language_only_uses_known_ids() {
|
||||
for (language, rows) in PENDING {
|
||||
let mut ids: Vec<&str> = vec![];
|
||||
for (id, english, translated) in *rows {
|
||||
let Some((_, hans_english, _)) = SIMPLIFIED_CHINESE.iter().find(|(hans_id, _, _)| hans_id == id) else {
|
||||
panic!("{language} row {id} is not an effect the zh-hans catalog knows");
|
||||
};
|
||||
assert_eq!(*english, *hans_english, "{id}: {language} and zh-hans name the effect differently");
|
||||
assert!(!translated.is_empty(), "{id}: empty {language} translation (repeat the English name if it stays)");
|
||||
ids.push(id);
|
||||
}
|
||||
let listed = ids.len();
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
assert_eq!(listed, ids.len(), "{language} lists an effect id twice");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every category the registry and its plug-ins report has a Simplified Chinese row, and
|
||||
/// every row of every language names a category that exists — else the generated Effect menu
|
||||
/// and Effects & Presets would draw a label no folder has.
|
||||
#[test]
|
||||
fn every_category_has_a_row() {
|
||||
let categories = effects::categories();
|
||||
for category in &categories {
|
||||
assert!(SIMPLIFIED_CHINESE_CATEGORIES.iter().any(|(english, _)| english == category), "no catalog row for category {category}");
|
||||
}
|
||||
for (language, rows) in [("zh-hans", SIMPLIFIED_CHINESE_CATEGORIES), ("zh-hant", TRADITIONAL_CHINESE_CATEGORIES), ("ja", JAPANESE_CATEGORIES)] {
|
||||
for (english, translated) in rows {
|
||||
assert!(categories.contains(english), "{language} lists {english}, which is not a category");
|
||||
assert!(!translated.is_empty(), "empty {language} 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 a language with no catalog or no row for this effect, draws the registry's
|
||||
/// name rather than an empty label.
|
||||
///
|
||||
/// The pending languages are also checked to still be row-less and to have an arm in
|
||||
/// [`catalog`]: an arm with no table would leave a translator's rows dead, and the day a
|
||||
/// pending language gets rows this fails so its slot moves out of [`PENDING`], where the
|
||||
/// coverage for it lives.
|
||||
#[test]
|
||||
fn languages_without_rows_draw_the_registry_name() {
|
||||
for language in ["en", "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}");
|
||||
}
|
||||
for (language, rows) in PENDING {
|
||||
assert!(catalog(language).is_some(), "{language} has a table but no arm in `catalog`: its rows would be dead");
|
||||
assert!(rows.is_empty(), "{language} has rows now: move it out of PENDING and cover it here");
|
||||
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}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2043,7 +2043,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));
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -1163,6 +1163,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;
|
||||
|
|
@ -1212,14 +1215,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());
|
||||
|
|
@ -1238,7 +1242,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(
|
||||
|
|
@ -1247,10 +1255,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);
|
||||
|
|
@ -1280,7 +1288,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();
|
||||
}
|
||||
|
|
@ -1622,7 +1630,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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -769,7 +769,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;
|
||||
|
|
@ -2362,6 +2366,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));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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) | First slice (#783): the Object Matte effect (Effect ▸ Matte) and `objectMatte.apply` turn a point (and optional box) on a reference frame into a matte and follow the object up to 600 frames each way, or segment every frame on its own; Roto Brush's engine, cache and model (MobileSAM from Settings ▸ Roto Brush, classical graph cut without it). Since then: the Object Matte tool in the viewer (click / drag a box, hover crosshair), the box as the model's box prompt, background tracking with progress and cancel, Freeze, and segmentations kept in the disk cache (26.5). Missing: a comparison with After Effects ([plan](fidelity/object-matte.md)) | The new one-click roto | 10–20 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 (answered: CUDA / NVENC need Nvidia's proprietary SDK through FFI, which the no-`unsafe` rule forbids); open PR #819 adds hardware decoding via `filmcraft-platform`; 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 10 languages (ja, zh-hans, zh-hant, uk, es, pt-br, ru, fr, de, cs: 624 rows each); panels in 6 (ja, zh-hans, zh-hant, uk, pt-br, cs); effect names and ~1,650 effect parameter names untranslated; Korean and Italian (After Effects ships them) absent; Hangul probe added (#599 closed) | 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 10 languages (ja, zh-hans, zh-hant, uk, es, pt-br, ru, fr, de, cs: 624 rows each); panels in 6 (ja, zh-hans, zh-hant, uk, pt-br, cs); effect names translated in Simplified Chinese (306, plus the generated Effect menu), their ~1,650 parameter names untranslated; Korean and Italian (After Effects ships them) absent; Hangul probe added (#599 closed) | 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 and #324 closed | 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) |
|
||||
|
|
|
|||
|
|
@ -19,14 +19,28 @@ 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
|
||||
fixed entries (624 catalog rows each at 470ea7a3, counted). The generated Effect menu (effect names) is deliberately left in English.
|
||||
fixed entries (624 catalog rows each at 470ea7a3, counted). 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 (410 rows), Simplified and
|
||||
Traditional Chinese (506 each), Ukrainian (505), Brazilian Portuguese (400) and Czech (501);
|
||||
counted at 470ea7a3.
|
||||
- **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. Adding a language there is two tables and one arm, and no
|
||||
panel changes: Traditional Chinese and Japanese have their tables in place and empty, so those
|
||||
languages draw English until a translator fills them (the Japanese set for #821 is what that
|
||||
slot is for). Each language decides for itself which names stay English — Adobe's Japanese
|
||||
interface leaves the `CC`, `VR` and `OCIO` sets in English while its Chinese one translates
|
||||
them — so a row repeating the English name is a choice, not a missing row.
|
||||
- **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,
|
||||
|
|
@ -50,7 +64,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,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effects untranslated; Home screen literals unwrapped (#574, closed) | 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,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | 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` | 624 (≈ 16%) | Menus only (panel catalog in open PR #996) | 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 |
|
||||
|
|
@ -77,7 +91,7 @@ Russian (`ru`) has a menu catalog (624 rows, ≈ 16%); After Effects also ships
|
|||
German and Russian 16%, Korean and Italian 0%; 175 / 10, was ≈ 12% on 10 October). 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
|
||||
|
|
@ -92,3 +106,4 @@ can give; none has had it.
|
|||
| 2026-10-12 | minor | Recounted: Russian, French, German and Czech menus (#873, #987), Brazilian Portuguese and Czech panels (#830, #987), Hangul probe (#599); overall ≈ 12% to ≈ 18%; Korean and Italian still absent |
|
||||
| 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 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue