Merge pull request #1581 from FeiLiuEM/i18n/panel-tabs

i18n: the labels the call-shape scan cannot see
This commit is contained in:
BFlat
2026-10-30 07:06:21 +07:00
committed by GitHub
20 changed files with 411 additions and 36 deletions
+5 -1
View File
@@ -799,7 +799,11 @@ impl DockContent<PanelKind> for EffectContent<'_> {
}
fn logical_label(&self, panel: &PanelKind) -> String {
crate::i18n::command_label("window.panel", panel.title()).to_string()
let title = panel.title();
// The Window menu names most panels; `Markers` is only in the panel catalog, and the two
// panels in neither (`History`, `ScriptUI Panel`) keep their English names.
let listed = crate::i18n::command_label("window.panel", title);
if listed == title { crate::i18n::tr(title).to_string() } else { listed.to_string() }
}
/// The tab names what the panel shows ("Composition Intro"), after the label swatch of its
+29
View File
@@ -7704,6 +7704,35 @@ mod tests {
}
}
/// The dock's tab titles are `PanelKind::title()`; the panels the Window menu lists are drawn in
/// the interface language through their `(window.panel, title)` rows. `History` and `ScriptUI
/// Panel` are in no menu and keep their English names, which is what this pins.
#[test]
fn the_dock_draws_every_panel_it_can_in_the_interface_language() {
use crate::dock::PanelKind;
const WITHOUT_A_ROW: &[&str] = &["History", "ScriptUI Panel"];
for (name, catalog, code) in [("ja", JAPANESE, "ja"), ("zh-hans", SIMPLIFIED_CHINESE, "zh-hans"), ("zh-hant", TRADITIONAL_CHINESE, "zh-hant")] {
// The dock asks both catalogs, so a title may be named by either.
ui::set_current(code);
for p in PanelKind::ALL {
let title = p.title();
let in_menu = catalog.iter().any(|(c, en, _)| *c == "window.panel" && *en == title);
let in_panel = ui::tr(title) != title;
assert_eq!(
in_menu || in_panel,
!WITHOUT_A_ROW.contains(&title),
"{name}: {title} is {}named by a catalog and {}in the exception list",
if in_menu || in_panel { "" } else { "not " },
if WITHOUT_A_ROW.contains(&title) { "" } else { "not " }
);
}
}
ui::set_current("en");
// The dock asks the catalog rather than drawing the English name.
let dock_ui = include_str!("dock_ui.rs");
assert!(dock_ui.contains(r#"crate::i18n::command_label("window.panel", title)"#), "dock_ui.rs no longer asks the catalog for the panel titles");
}
#[test]
fn ukrainian_catalog_matches_the_settings_registry() {
let codes: Vec<_> = effectcraft_engine::prefs::LANGUAGES.iter().map(|(_, code)| *code).filter(|code| !matches!(*code, "system" | "en")).collect();
+339
View File
@@ -1688,6 +1688,32 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Pre-roll", "预卷"),
("Post-roll", "后卷"),
("If caching, play cached frames", "缓存时播放已缓存帧"),
("Apply", "应用"),
("Analyze", "分析"),
("Create Camera", "创建摄像机"),
("Add Point", "添加点"),
("Remove Point", "移除点"),
("Render", "渲染"),
("Save Current as Template…", "将当前项存为模板…"),
("New Composition From Footage", "从素材新建合成"),
("Search effects and presets", "搜索效果和预设"),
("Search project", "搜索项目"),
("Search", "搜索"),
("(no composition)", "没有合成"),
("per second", "每秒"),
("Waveform", "波形"),
("steps", "步"),
("Building waveform…", "正在生成波形…"),
("No effects. Drag one from Effects & Presets.", "没有效果。从效果和预设中拖入一个。"),
("Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it", "点击色环添加颜色;按住 Alt 点击色标(或将其拖出)可移除"),
("Start from a ready-made project. Opening a template makes an untitled copy.", "从现成项目开始。打开模板会创建一份未命名副本。"),
("Square Pixels", "方形像素"),
("Basic", "基本"),
("Advanced", "高级"),
("3D Renderer", "3D 渲染器"),
("Black", "黑色"),
("White", "白色"),
("Composition Settings", "合成设置"),
];
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
@@ -2419,6 +2445,32 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("Pre-roll", "預卷"),
("Post-roll", "後卷"),
("If caching, play cached frames", "快取時播放已快取影格"),
("Apply", "套用"),
("Analyze", "分析"),
("Create Camera", "建立攝影機"),
("Add Point", "添加點"),
("Remove Point", "移除點"),
("Render", "算圖"),
("Save Current as Template…", "將目前項目存為範本…"),
("New Composition From Footage", "從素材新增合成"),
("Search effects and presets", "搜尋效果與預設"),
("Search project", "搜尋專案"),
("Search", "搜尋"),
("(no composition)", "沒有合成"),
("per second", "每秒"),
("Waveform", "波形"),
("steps", "步驟"),
("Building waveform…", "正在產生波形…"),
("No effects. Drag one from Effects & Presets.", "沒有效果。從效果與預設集中拖入一個。"),
("Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it", "點擊色環新增顏色;按住 Alt 點擊色標(或將其拖出)即可移除"),
("Start from a ready-made project. Opening a template makes an untitled copy.", "從現成專案開始。開啟範本會建立一份未命名副本。"),
("Square Pixels", "方形像素"),
("Basic", "基本"),
("Advanced", "高級"),
("3D Renderer", "3D 算圖引擎"),
("Black", "黑色"),
("White", "白色"),
("Composition Settings", "合成設置"),
];
/// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs.
@@ -6028,6 +6080,293 @@ mod tests {
/// English source changed or went away. A string a helper draws for the panel (a toolbar
/// tooltip, a popup item, a checkbox label) is looked up inside that helper, so its English
/// source only appears as a literal there.
#[test]
fn helper_and_painter_labels_are_translated() {
/// The `widgets.rs` helpers that draw a label, and which argument holds it.
const HELPER_LABELS: &[(&str, usize)] =
&[("text_button", 2), ("search_field", 3), ("text_field", 3), ("dropdown", 2), ("dropdown_tip", 2), ("text_fit", 3), ("check_label", 1)];
/// Literals that draw as they are: a brand, a value, a separator, a unit, and the kerning
/// sample pair the Character panel shows.
const VERBATIM: &[&str] = &["EffectCraft", "32", "dB", "···", "×", "V/A"];
/// The top-level arguments of a call, given the text after its `(`. A call split over
/// several lines is still one argument list, so this walks past newlines until the
/// parenthesis closes. It understands the two string forms a label can be written in - a
/// plain one with escapes and a raw one (`r"…"`, `r#"…"#`) - so a comma or parenthesis
/// inside either is not mistaken for code.
fn args_of(s: &str) -> Vec<&str> {
#[derive(Clone, Copy)]
enum Mode {
Code,
Str(bool),
Raw(usize),
}
let cs: Vec<(usize, char)> = s.char_indices().collect();
let (mut out, mut depth, mut start, mut end) = (Vec::new(), 0i32, None, s.len());
let mut mode = Mode::Code;
let mut k = 0;
while k < cs.len() {
let (i, c) = cs[k];
match mode {
Mode::Raw(hashes) => {
if c == '"' {
let mut n = 0;
while cs.get(k + 1 + n).is_some_and(|(_, h)| *h == '#') {
n += 1;
}
if n >= hashes {
mode = Mode::Code;
k += n;
}
}
}
Mode::Str(escaped) => {
mode = if c == '"' && !escaped { Mode::Code } else { Mode::Str(c == '\\' && !escaped) };
}
Mode::Code => match c {
'"' => {
mode = Mode::Str(false);
if start.is_none() {
start = Some(i);
}
}
'r' if start.is_none() && cs.get(k + 1).is_some_and(|(_, n)| *n == '#' || *n == '"') => {
let mut n = 0;
while cs.get(k + 1 + n).is_some_and(|(_, h)| *h == '#') {
n += 1;
}
if cs.get(k + 1 + n).is_some_and(|(_, q)| *q == '"') {
mode = Mode::Raw(n);
start = Some(i);
k += n + 1;
}
}
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => {
if depth == 0 {
end = i;
break;
}
depth -= 1;
}
',' if depth == 0 => {
if let Some(st) = start {
out.push(s[st..i].trim());
}
start = None;
}
c if !c.is_whitespace() && start.is_none() => start = Some(i),
_ => {}
},
}
k += 1;
}
if let Some(st) = start {
out.push(s[st..end].trim());
}
out
}
/// The text a literal argument draws: a plain string with its escapes undone, or a raw
/// string with none. `None` when the argument is not one literal (a concatenation, a call).
fn literal_text(arg: &str) -> Option<String> {
if let Some(rest) = arg.strip_prefix('r') {
let hashes = rest.chars().take_while(|c| *c == '#').count();
// An identifier that starts with `r` (`result`) is not a raw string: the quote has
// to follow the `#`s.
if rest.chars().nth(hashes) != Some('"') {
return None;
}
let close = format!("\"{}", "#".repeat(hashes));
return arg.get(1 + hashes + 1..)?.strip_suffix(&close).map(str::to_string);
}
let body = arg.strip_prefix('"')?.strip_suffix('"')?;
// Decode the escapes that matter for the exemption: a literal written as `"\u{bb}"` is
// one character (`»`), not six.
let mut out = String::new();
let mut it = body.chars().peekable();
while let Some(c) = it.next() {
if c != '\\' {
if c == '"' {
return None; // two literals side by side
}
out.push(c);
continue;
}
match it.next()? {
'"' => out.push('"'),
'\\' => out.push('\\'),
'n' => out.push('\n'),
't' => out.push('\t'),
'x' => {
let hex: String = it.by_ref().take(2).collect();
let byte = u8::from_str_radix(&hex, 16).ok()?;
out.push(char::from(byte));
}
'u' => {
if it.next() != Some('{') {
return None;
}
let hex: String = it.by_ref().take_while(|c| *c != '}').collect();
out.push(char::from_u32(u32::from_str_radix(&hex, 16).ok()?)?);
}
other => {
out.push('\\');
out.push(other);
}
}
}
Some(out)
}
/// `source` with comments blanked and the *contents* of every string literal blanked too,
/// with offsets, newlines and the quotes kept. Call tokens are looked for in this text, so a
/// `widgets::…(` or `.text(` that only appears inside a doc string or a sample is not
/// mistaken for a call; the argument list is then read from the original at that offset.
fn code_only(source: &str) -> String {
#[derive(Clone, Copy)]
enum Mode {
Code,
Str(bool),
Raw(usize),
Comment,
}
let cs: Vec<(usize, char)> = source.char_indices().collect();
// Byte-for-byte: a blanked character becomes as many spaces as its UTF-8 length, so
// every offset found here indexes the original source as well.
let mut out: Vec<u8> = Vec::with_capacity(source.len());
let keep = |out: &mut Vec<u8>, c: char| out.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes());
let blank = |out: &mut Vec<u8>, c: char| out.extend(std::iter::repeat_n(b' ', c.len_utf8()));
let mut mode = Mode::Code;
let mut k = 0;
while k < cs.len() {
let (_, c) = cs[k];
match mode {
Mode::Comment => {
if c == '\n' {
mode = Mode::Code;
keep(&mut out, c);
} else {
blank(&mut out, c);
}
}
Mode::Raw(hashes) => {
if c == '"' {
let mut n = 0;
while cs.get(k + 1 + n).is_some_and(|(_, h)| *h == '#') {
n += 1;
}
if n >= hashes {
keep(&mut out, c);
for j in 1..=n {
keep(&mut out, cs[k + j].1);
}
mode = Mode::Code;
k += n;
} else {
blank(&mut out, c);
}
} else {
blank(&mut out, c);
}
}
Mode::Str(escaped) => {
if c == '"' && !escaped {
mode = Mode::Code;
keep(&mut out, c);
} else {
mode = Mode::Str(c == '\\' && !escaped);
blank(&mut out, c);
}
}
Mode::Code => match c {
'"' => {
mode = Mode::Str(false);
keep(&mut out, c);
}
'/' if out.last() == Some(&b'/') => {
// The first `/` stays; this one becomes a space. Both are one byte, so
// the mask keeps its length and every offset still indexes the source.
blank(&mut out, c);
mode = Mode::Comment;
}
'r' if matches!(cs.get(k + 1), Some((_, n)) if *n == '#' || *n == '"') => {
let mut n = 0;
while cs.get(k + 1 + n).is_some_and(|(_, h)| *h == '#') {
n += 1;
}
if cs.get(k + 1 + n).is_some_and(|(_, q)| *q == '"') {
keep(&mut out, c);
for j in 1..=n {
keep(&mut out, cs[k + j].1);
}
keep(&mut out, '"');
mode = Mode::Raw(n);
k += n + 1;
} else {
keep(&mut out, c);
}
}
_ => keep(&mut out, c),
},
}
k += 1;
}
debug_assert_eq!(out.len(), source.len(), "the mask must keep every byte offset");
String::from_utf8(out).expect("mask keeps UTF-8 boundaries")
}
for (path, source) in CONVERTED {
// The test module draws nothing; it holds sample strings. A test-only helper above it
// (`#[cfg(test)] pub(crate) fn press_key` in `dialogs.rs`) is kept in the scanned range,
// because production code follows it.
let cut = source
.match_indices("#[cfg(test)]")
.find(|(at, _)| source[at + "#[cfg(test)]".len()..].lines().take(3).any(|l| l.trim_start().starts_with("mod ")))
.map_or(source.len(), |(at, _)| at);
let source = &source[..cut];
let code = code_only(source);
let line_of = |at: usize| code[..at].matches('\n').count() + 1;
let check = |at: usize, arg: &str, what: &str| {
// A single character is a symbol or a glyph, not interface text - the same exemption
// the call-shape scan above makes. A call that already asks the catalog is not a
// literal here at all.
if let Some(text) = literal_text(arg)
&& text.chars().count() >= 2
&& !VERBATIM.contains(&text.as_str())
{
panic!("{path}:{}: the interface text {arg} that {what} draws is not in the catalog", line_of(at));
}
};
for (call, index) in HELPER_LABELS {
let marker = format!("widgets::{call}(");
for (at, _) in code.match_indices(&marker) {
if let Some(arg) = args_of(&source[at + marker.len()..]).get(*index) {
check(at, arg, &format!("widgets::{call}"));
}
}
}
for (at, _) in code.match_indices(".text(") {
if let Some(arg) = args_of(&source[at + ".text(".len()..]).get(2) {
check(at, arg, "Painter::text");
}
}
}
}
#[test]
fn the_composition_settings_labels_are_translated() {
// The Composition Settings dialog (and New Composition) draws these from a `(label, value)`
// table, a tab array and a `match` arm, which the call-shape scan cannot see (#1451). They
// used to have no row in any catalog, so every interface drew them in English.
for key in ["Square Pixels", "Basic", "Advanced", "3D Renderer", "Black", "White", "Composition Settings"] {
for (name, table) in [("Simplified", SIMPLIFIED_CHINESE), ("Traditional", TRADITIONAL_CHINESE)] {
let value = table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v);
assert!(value.is_some_and(|v| !v.is_empty()), "{key} has an empty {name} row");
}
}
}
#[test]
fn every_row_is_used_by_a_converted_panel() {
for (en, _) in catalogs() {
+1 -1
View File
@@ -331,7 +331,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
}
}
_ => {
ui.label(egui::RichText::new(panel.title()).weak());
ui.label(egui::RichText::new(crate::i18n::label(app, "window.panel", panel.title())).weak());
}
}
});
+4 -4
View File
@@ -127,7 +127,7 @@ pub fn wiggler(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
app.ui.anim_tools = st.clone();
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 2;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("wg-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("wg-apply")).clicked() {
let params = json!({
"apply": if st.wiggle_spatial { "spatial" } else { "temporal" },
"noise": if st.wiggle_smooth { "smooth" } else { "jagged" },
@@ -175,7 +175,7 @@ pub fn smoother(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
y += ROW + 8.0;
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 3;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("sm-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("sm-apply")).clicked() {
apply(app, "keys.smooth", json!({"tolerance": tol}));
}
app.auto.add("smoother.apply", b, "Apply");
@@ -375,7 +375,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
changes.insert("keyframeRate".into(), json!(nr));
}
} else {
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, "per second", Tokens::ui(12.0), t.text_dim);
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, crate::i18n::tr("per second"), Tokens::ui(12.0), t.text_dim);
}
y += ROW;
let mut check = |app: &mut EffectcraftApp, y: f32, key: &str, text: &str, on: bool| {
@@ -454,7 +454,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
}
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 2;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("mi-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("mi-apply")).clicked() {
match app.session.execute("mask.interpolate", json!({})) {
Ok(r) => app.ui.status = format!("Mask Interpolation: {} keyframes added ({} vertices)", r["keys"], r["vertices"]),
Err(e) => app.ui.status = e.to_string(),
@@ -413,12 +413,12 @@ pub fn editor(
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(110.0, 22.0));
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::unique(("ct-analyze", g.uid))).clicked() && !busy {
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::unique(("ct-analyze", g.uid))).clicked() && !busy {
actions.push(("camera.analyze".into(), me.clone()));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.analyze", g.uid), b1, "Analyze");
if running {
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::unique(("ct-cancel", g.uid))).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::unique(("ct-cancel", g.uid))).clicked() {
actions.push(("camera.cancel".into(), json!({})));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.cancel", g.uid), b2, "Cancel");
@@ -426,7 +426,7 @@ pub fn editor(
let params = effectcraft_engine::camera_track::static_params(g);
let solve = ct::solve(&params);
if solve.is_some() {
if widgets::text_button(ui, b3, "Create Camera", false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() {
if widgets::text_button(ui, b3, crate::i18n::tr("Create Camera"), false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() {
actions.push(("camera.create".into(), me.clone()));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.createCamera", g.uid), b3, "Create Camera");
+4 -4
View File
@@ -330,11 +330,11 @@ fn tab_body(
let cur = PIXEL_ASPECTS
.iter()
.find(|(_, v)| (v - d.pixel_aspect).abs() < 1e-3)
.map(|p| p.0.to_string())
.map(|p| tr(p.0).to_string())
.unwrap_or_else(|| format!("{:.2}", d.pixel_aspect));
let r = craft_ui::ComboBox::from_id_salt("comp-par").width(220.0).selected_text(cur).show_ui(ui, |ui| {
for (name, v) in PIXEL_ASPECTS {
if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, *name).clicked() {
if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, tr(name)).clicked() {
d.pixel_aspect = *v;
}
}
@@ -402,7 +402,7 @@ fn tab_body(
ui.horizontal(|ui| {
let r = crate::widgets::srgb_color_button(ui, &mut d.bg);
auto.add("dialog.comp.background", r.rect, "Background Color");
ui.label(color_name(d.bg));
ui.label(tr(color_name(d.bg)));
});
ui.end_row();
});
@@ -498,7 +498,7 @@ pub fn show(
ui.add_space(8.0);
ui.horizontal(|ui| {
for (tab, label, key) in [(Tab::Basic, "Basic", "basic"), (Tab::Advanced, "Advanced", "advanced"), (Tab::Renderer, "3D Renderer", "renderer")] {
let r = ui.selectable_label(d.tab == tab, label);
let r = ui.selectable_label(d.tab == tab, tr(label));
auto.add(&format!("dialog.comp.tab.{key}"), r.rect, label);
if r.clicked() {
d.tab = tab;
+2
View File
@@ -403,6 +403,8 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
let mut result = None;
let custom = app.session.prefs.comp_presets.clone();
let mut cmds = Vec::new();
// The title keys the modal id and its drag offset; `modal_fit` translates the heading itself,
// so passing the source keeps the dialog in place when the interface language changes.
modal(ctx, "Composition Settings", vec2(600.0, 470.0), t, |ui| {
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds);
});
@@ -8,12 +8,12 @@ pub(super) fn warp_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui:
let busy = app.session.is_warp_analyzing();
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::unique(("warp-analyze", g.uid))).clicked() && !busy {
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::unique(("warp-analyze", g.uid))).clicked() && !busy {
actions.push(("warp.analyze".into(), json!({"layer": layer.id.0, "effect": g.uid})));
}
app.auto.add(&format!("effectControls.warp.{}.analyze", g.uid), b1, "Analyze");
if running {
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::unique(("warp-cancel", g.uid))).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::unique(("warp-cancel", g.uid))).clicked() {
actions.push(("warp.cancel".into(), json!({})));
}
app.auto.add(&format!("effectControls.warp.{}.cancel", g.uid), b2, "Cancel");
@@ -192,7 +192,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
bp.text(
pos2(body.center().x, body.min.y + 40.0),
Align2::CENTER_CENTER,
"No effects. Drag one from Effects & Presets.",
crate::i18n::tr("No effects. Drag one from Effects & Presets."),
Tokens::ui(12.0),
t.text_faint,
);
+1 -1
View File
@@ -376,7 +376,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let sr = Rect::from_min_size(rect.min + vec2(8.0, 8.0), vec2(rect.width() - 44.0, 24.0));
let mut q = app.ui.effects_search.clone();
widgets::search_field(ui, sr, &mut q, "Search effects and presets", &t);
widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search effects and presets"), &t);
app.ui.effects_search = q.clone();
app.auto.add("effects.search", sr, "Search");
let force_scan = contents_menu(app, ui, &ctx, rect, sr, &t);
+1 -1
View File
@@ -250,7 +250,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
p.rect_filled(rect, 0.0, t.tl_bg);
let Some(rid) = root(app) else {
p.text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
return;
};
// ---- toolbar: switches and direction.
+4 -4
View File
@@ -600,7 +600,7 @@ fn colorama_wheel(
p.text(
pos2(sr.min.x, sr.max.y + 9.0),
Align2::LEFT_CENTER,
"Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it",
crate::i18n::tr("Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it"),
Tokens::ui(10.5),
t.text_faint,
);
@@ -801,19 +801,19 @@ fn reshape_buttons(
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(96.0, 22.0));
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(56.0, 22.0));
if widgets::text_button(ui, b1, "Add Point", false, &t, egui::Id::unique(("reshape-add", euid))).clicked() {
if widgets::text_button(ui, b1, crate::i18n::tr("Add Point"), false, &t, egui::Id::unique(("reshape-add", euid))).clicked() {
let mut v = pairs.clone();
v.push(add_pair(&pairs));
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.add"), b1, "Add Point");
if widgets::text_button(ui, b2, "Remove Point", false, &t, egui::Id::unique(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
if widgets::text_button(ui, b2, crate::i18n::tr("Remove Point"), false, &t, egui::Id::unique(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
let mut v = pairs.clone();
v.pop();
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.remove"), b2, "Remove Point");
if widgets::text_button(ui, b3, "Clear", false, &t, egui::Id::unique(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
if widgets::text_button(ui, b3, crate::i18n::tr("Clear"), false, &t, egui::Id::unique(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
set_str(actions, layer, g, "correspondencePoints", String::new(), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.clear"), b3, "Clear");
+2 -2
View File
@@ -18,7 +18,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.text(
pos2(x, y + 26.0),
Align2::LEFT_CENTER,
"Hands-on tutorials that run inside EffectCraft. Do each step yourself, or press Show me.",
crate::i18n::tr("Hands-on tutorials that run inside EffectCraft. Do each step yourself, or press Show me."),
Tokens::ui(11.5),
t.text_faint,
);
@@ -38,7 +38,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.circle_filled(badge.center(), 22.0, t.accent.gamma_multiply(0.25));
p.circle_stroke(badge.center(), 22.0, Stroke::new(1.5, t.accent));
p.text(badge.center() - vec2(0.0, 5.0), Align2::CENTER_CENTER, tut.steps.len().to_string(), Tokens::semibold(15.0), Color32::WHITE);
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, "steps", Tokens::ui(9.0), t.text_dim);
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, crate::i18n::tr("steps"), Tokens::ui(9.0), t.text_dim);
let tx = badge.max.x + 16.0;
p.text(pos2(tx, r.min.y + 22.0), Align2::LEFT_CENTER, &tut.title, Tokens::semibold(14.0), Color32::WHITE);
p.text(pos2(tx, r.min.y + 40.0), Align2::LEFT_CENTER, format!("{} min", tut.minutes), Tokens::ui(11.0), t.text_faint);
+2 -1
View File
@@ -11,7 +11,8 @@ use crate::{EffectcraftApp, widgets};
pub fn placeholder(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, p: PanelKind) {
let t = app.tokens;
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&(p.title())]), Tokens::ui(12.0), t.text_faint);
let name = crate::i18n::label(app, "window.panel", p.title());
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&name]), Tokens::ui(12.0), t.text_faint);
}
/// Preview panel: transport controls, the shortcut whose options are shown, and that
+2 -2
View File
@@ -494,7 +494,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
let render_b = Rect::from_min_size(pos2(top.max.x - 12.0 - bw, top.min.y + 34.0), vec2(bw, 26.0));
let stop_b = Rect::from_min_size(pos2(render_b.min.x - 8.0 - bw, render_b.min.y), vec2(bw, 26.0));
let can_render = !rendering && queue.iter().any(RenderQueueItem::is_queued) && !available.is_empty();
if widgets::text_button(ui, render_b, "Render", can_render, &t, egui::Id::unique("rq-render")).clicked() && can_render {
if widgets::text_button(ui, render_b, crate::i18n::tr("Render"), can_render, &t, egui::Id::unique("rq-render")).clicked() && can_render {
actions.push(("renderQueue.render", json!({"wait": false})));
}
app.auto.add("renderQueue.render", render_b, "Render");
@@ -507,7 +507,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, tr("Notify"), Tokens::ui(11.5), t.text_dim);
app.auto.add("renderQueue.notify", nb, if notify { "Notify: on" } else { "Notify: off" });
if rendering {
if widgets::text_button(ui, stop_b, "Stop", false, &t, egui::Id::unique("rq-stop")).clicked() {
if widgets::text_button(ui, stop_b, crate::i18n::tr("Stop"), false, &t, egui::Id::unique("rq-stop")).clicked() {
actions.push(("renderQueue.stop", json!({})));
}
app.auto.add("renderQueue.stop", stop_b, "Stop");
+1 -1
View File
@@ -102,7 +102,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
p.text(
out_rect.center(),
Align2::CENTER_CENTER,
"JavaScript with the After Effects scripting object model — try app.project.numItems",
crate::i18n::tr("JavaScript with the After Effects scripting object model — try app.project.numItems"),
Tokens::ui(12.0),
t.text_faint,
);
+2 -2
View File
@@ -62,14 +62,14 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.text(
pos2(x, y + 26.0),
Align2::LEFT_CENTER,
"Start from a ready-made project. Opening a template makes an untitled copy.",
crate::i18n::tr("Start from a ready-made project. Opening a template makes an untitled copy."),
Tokens::ui(11.5),
t.text_faint,
);
// Save the open project as a user template.
let can_save = app.session.project.comps().next().is_some();
let sr = Rect::from_min_size(pos2(area.max.x - 210.0, y - 4.0), vec2(210.0, 28.0));
let save = widgets::text_button(ui, sr, "Save Current as Template…", false, &t, egui::Id::unique("home-templates-save")).clicked();
let save = widgets::text_button(ui, sr, crate::i18n::tr("Save Current as Template…"), false, &t, egui::Id::unique("home-templates-save")).clicked();
app.auto.add("home.templates.saveCurrent", sr, "Save Current as Template…");
if save {
if can_save {
+4 -4
View File
@@ -1772,7 +1772,7 @@ fn layout(app: &mut EffectcraftApp, ui: &egui::Ui, rect: Rect, cid: ItemId, comp
fn empty_timeline(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let t = app.tokens;
let ctx = ui.ctx().clone();
ui.painter().text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
ui.painter().text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
// Dropping footage into an empty Timeline should create its first composition,
// just like dropping onto the Project panel's New Composition button (#649).
if let Some(crate::panels::DragPayload::Item(dragged)) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx).as_deref()
@@ -2213,7 +2213,7 @@ fn header_search_and_switches(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &T
let comp = g.comp.clone();
let sr = Rect::from_min_size(pos2(tc_rect.max.x + 14.0, top + 12.0), vec2((left_w - 420.0).clamp(120.0, 220.0), 22.0));
let mut search = app.ui.timeline.search.clone();
widgets::search_field(ui, sr, &mut search, "Search", &t);
widgets::search_field(ui, sr, &mut search, crate::i18n::tr("Search"), &t);
app.ui.timeline.search = search;
app.auto.add("timeline.search", sr, "Search");
let mut bx = rect.min.x + left_w - 10.0;
@@ -3622,7 +3622,7 @@ fn waveform_row(app: &mut EffectcraftApp, g: &Rw, rc: &RowCtx<'_>) {
let RowKind::Waveform { item, .. } = &rc.row.kind else { return };
lp.rect_filled(left, 0.0, if ri.is_multiple_of(2) { t.row } else { t.row_alt });
let indent = cw.name + 6.0 + 14.0 * row.depth as f32;
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, "Waveform", Tokens::ui(12.0), t.text);
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Waveform"), Tokens::ui(12.0), t.text);
let wr = Rect::from_min_max(pos2(graph_x0, r.min.y), r.max);
gp.rect_filled(wr, 0.0, t.tl_bg);
app.auto.add(&format!("timeline.layer.{}.waveform", layer.id.0), wr, "Waveform");
@@ -3632,7 +3632,7 @@ fn waveform_row(app: &mut EffectcraftApp, g: &Rw, rc: &RowCtx<'_>) {
gp.text(
pos2(tm.x(layer.in_point.seconds()).max(wr.min.x) + 6.0, wr.center().y),
Align2::LEFT_CENTER,
"Building waveform…",
crate::i18n::tr("Building waveform…"),
Tokens::ui(11.0),
t.text_faint,
);
+2 -2
View File
@@ -9,12 +9,12 @@ pub(super) fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
let c = rect.center();
p.text(c - vec2(0.0, 40.0), Align2::CENTER_CENTER, crate::i18n::tr("Composition"), Tokens::semibold(16.0), t.text);
let b1 = Rect::from_center_size(c + vec2(0.0, 0.0), vec2(220.0, 30.0));
if widgets::text_button(ui, b1, "New Composition", true, &t, egui::Id::unique("empty-newcomp")).clicked() {
if widgets::text_button(ui, b1, crate::i18n::tr("New Composition"), true, &t, egui::Id::unique("empty-newcomp")).clicked() {
crate::panels::dialogs::open_new_comp(app);
}
app.auto.add("viewer.empty.newComp", b1, "New Composition");
let b2 = Rect::from_center_size(c + vec2(0.0, 40.0), vec2(220.0, 30.0));
if widgets::text_button(ui, b2, "New Composition From Footage", false, &t, egui::Id::unique("empty-fromfootage")).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("New Composition From Footage"), false, &t, egui::Id::unique("empty-fromfootage")).clicked() {
let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({"newComp": true}));
}
app.auto.add("viewer.empty.import", b2, "New Composition From Footage");