mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
i18n: the panel tabs, and the labels the call-shape scan cannot see
Two English surfaces the translation work so far could not reach. The dock's tab titles: `PanelKind::title()` is drawn by `dock_ui.rs`'s `tab_titles`, which listed only the tabs that name a comp, a layer or a script and let every other one fall back to `title()`'s English - so every panel tab (`Timeline`, `Preview`, `Properties`, `Effects & Presets`, ...) stayed English. The Window menu already carries a (`window.panel`, title) row in every catalog, so the dock reads that pair and falls back to the panel catalog for `Markers` (the one panel only it names); `History` and `ScriptUI Panel` have no row anywhere and keep their English names. `app.auto`'s labels and the dock's serialized state keep the English title, which the control channel needs. The scan's blind shapes: `converted_panels_have_no_unwrapped_literals` only sees a literal that follows a call token, so a label passed to a helper, the third argument of `Painter::text`, a `(key, label, ...)` table, a `let` binding and a `match` arm were all invisible. The two that can be checked without guessing are added (`helper_and_painter_labels_are_translated`), with the same single-character exemption the call scan makes, and the 25 sites they find now ask the catalog (`Apply`, `Analyze`, `Cancel`, `Create Camera`, `Add Point`, `Remove Point`, `Clear`, `Render`, `Stop`, `Save Current as Template...`, `New Composition From Footage`, the three search fields, `(no composition)`, `per second`, `Waveform`, `steps`), with 15 rows added to the Simplified and Traditional catalogs in the conventions those tables already use (Render 算圖, Apply 套用, Add Point 添加點). The `uic!` labels and the Keyboard Shortcuts dialog are a different shape and are still to decide (#1451). Verified on Linux: cargo fmt --all --check, cargo test -p effectcraft-ui-egui (every suite passes), cargo clippy -p effectcraft-ui-egui --all-targets -- -D warnings clean. Reverting the dock's catalog lookup fails `the_dock_draws_every_panel_it_can_in_the_interface_language`; unwrapping a helper or a painter label fails `helper_and_painter_labels_are_translated` ("panels/project.rs:434: the interface text \"Search project\" that widgets::search_field draws is not in the catalog"). Also answers #1451's question about languages whose rows have no Simplified row to be a subset of: six labels the Composition Settings dialog draws from a `(label, value)` table, a tab array and a `match` arm (`Square Pixels`, `Basic`, `Advanced`, `3D Renderer`, `Black`, `Composition Settings`, plus `White` from the same `color_name`) had no row anywhere, so every interface drew them in English; they have Simplified and Traditional rows now, in the existing menu wording. The seventh, `Lock Aspect Ratio`, is only an automation name - the drawn label is `Lock Aspect Ratio to {}`, which already had rows, and `comp_settings.rs` built it with `format!` where `dialogs.rs` already used `tr_args`, so that one is wired now too. Review fixes (CodeRabbit): the scan now runs over the whole file with line comments blanked, so a `.text(` call split across lines is still one argument list - which found five more labels the per-line loop could not see (`No effects. Drag one from Effects & Presets.`, the colour-ring hint in `fx_editors.rs`, the Learn blurb, the Script Console hint, the Templates blurb, plus `Building waveform…` from the review), all wired with rows. And the New Composition title is back to the English source: `modal_fit` keys the modal id and the drag offset on the title (its own comment says the id stays on the untranslated source) and translates the heading itself, so `tr(..)` there would have moved the dialog when the interface language changed. And the argument list is bounded by the call's closing parenthesis: `args_of` used to flush a trailing argument at end-of-string, so a last argument without a trailing comma carried the `)` and the source after it, which made the check skip it as "not a literal". Reverting that one line is a two-line test (a literal last argument in `header.rs` is caught now and was silent before). The label scan also understands the two string forms a label can be written in: a plain string ends at the first unescaped quote, a raw string (`r"…"`, `r#"…"#`) at a quote followed by as many `#` as opened it, and `literal_text` decodes the argument before the exemptions run - so an escaped or raw label is checked like any other instead of being skipped. Two more scan fixes from review: `without_comments` tracks the escape state rather than the previous character, so a string ending in a backslash (`"a\\"`) closes where it should; and the scanned range ends at the test *module* - the first `#[cfg(test)]` followed by a `mod` - rather than at the first `#[cfg(test)]`, because `dialogs.rs` keeps a test-only helper above its test module and production code between them was not scanned. And call tokens are looked for in a code-only mask: comments blanked, the contents of plain and raw strings blanked too, with the quotes kept and every byte offset preserved (a blanked character becomes as many spaces as its UTF-8 length, and a debug assertion pins the mask length). The argument list is read from the original source at that offset, so a sample string that quotes a call cannot be mistaken for one. Building the mask from chars instead of bytes shifted every offset after the first non-ASCII character, which the debug assertion now catches. The dock's display path has its own test now: `tab_titles` is called with `general.language` set to Chinese and the labels a window shows are asserted (`Timeline` 时间轴 ...), plus `History` keeping its English tab and the English interface keeping all of them. Reverting the dock to `PanelKind::title()` fails it with "Timeline is not drawn in Chinese", which the catalog-only test could not see.
This commit is contained in:
20 files changed
+412
-37
No files matched your search
@@ -791,7 +791,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
|
||||
|
||||
@@ -7620,6 +7620,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();
|
||||
|
||||
@@ -1641,6 +1641,32 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Total Offset", "总偏移"),
|
||||
("Per Layer", "每个图层"),
|
||||
("Track", "跟踪"),
|
||||
("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)] = &[
|
||||
@@ -2327,6 +2353,32 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Total Offset", "總偏移"),
|
||||
("Per Layer", "每個圖層"),
|
||||
("Track", "追蹤"),
|
||||
("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.
|
||||
@@ -5579,6 +5631,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() {
|
||||
|
||||
@@ -281,7 +281,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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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(¶ms);
|
||||
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");
|
||||
|
||||
@@ -266,7 +266,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;
|
||||
@@ -327,7 +327,7 @@ pub fn show(
|
||||
let (ow, oh) = (d.width, d.height);
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px")));
|
||||
auto.add("dialog.comp.width", r.rect, "Width");
|
||||
let lbl = format!("Lock Aspect Ratio to {}", aspect_label(ow as f64, oh as f64));
|
||||
let lbl = tr_args("Lock Aspect Ratio to {}", &[&aspect_label(ow as f64, oh as f64)]);
|
||||
let r = ui.checkbox(&mut d.lock_aspect, lbl);
|
||||
auto.add("dialog.comp.lockAspect", r.rect, "Lock Aspect Ratio");
|
||||
if d.lock_aspect && ow != d.width && ow > 0 {
|
||||
@@ -348,11 +348,11 @@ pub fn show(
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -420,7 +420,7 @@ pub fn show(
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -183,7 +183,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,
|
||||
);
|
||||
|
||||
@@ -357,7 +357,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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -474,7 +474,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");
|
||||
@@ -487,7 +487,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");
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1721,7 +1721,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()
|
||||
@@ -2152,7 +2152,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;
|
||||
@@ -3536,7 +3536,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");
|
||||
@@ -3546,7 +3546,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,
|
||||
);
|
||||
|
||||
@@ -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!({}));
|
||||
}
|
||||
app.auto.add("viewer.empty.import", b2, "New Composition From Footage");
|
||||
|
||||
Reference in new issue
Block a user