i18n: context menus, tab titles, workspaces and column headers reach the catalogs

Text drawn outside the menu bar stayed English in every language:

- i18n::command_label: the Timeline / Composition layer and property
  context menus now take the menu bar's row for the same command (… or
  ...), else the panel catalog's row; the layer menu's Effects submenu
  draws effect and category names through the effect catalog.
- The dock's tab titles (and the dragged tab), the Composition, Effect
  Controls and Properties tabs, and the workspace bar and menus use the
  window.panel and window.workspace rows every menu catalog already has.
- The Project panel's search hint, details hint and column headers, and
  the Timeline's column headers and Columns menu go through tr().

The Simplified and Traditional Chinese rows for "Type" read 文字 (text, the
Type tool); the string is the Camera Settings field and now the Project
column, so they read 类型 / 類型. dock_ui.rs joins the converted files. Languages without a row for a
string keep the English, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
(cherry picked from commit b335f1e2764929b78d7b678ec026f680b1a8129f)
This commit is contained in:
Davit Matchakhelidze
2026-10-14 22:50:03 +04:00
parent 05955c08b4
commit a1a1a36a41
7 changed files with 71 additions and 36 deletions
+6 -4
View File
@@ -41,8 +41,8 @@ impl EffectcraftApp {
}
let Some(comp) = self.session.active_comp() else { return out };
if let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) {
out.push((PanelKind::EffectControls, format!("Effect Controls {}", l.name)));
out.push((PanelKind::Properties, format!("Properties: {}", l.name)));
out.push((PanelKind::EffectControls, format!("{} {}", crate::i18n::command_label("window.panel", "Effect Controls"), l.name)));
out.push((PanelKind::Properties, format!("{}: {}", crate::i18n::command_label("window.panel", "Properties"), l.name)));
}
out
}
@@ -206,7 +206,9 @@ impl EffectcraftApp {
let titles = self.tab_titles();
let decos = self.tab_decos();
let docs = self.tab_docs();
let title = |p: PanelKind| titles.iter().find(|(k, _)| *k == p).map(|(_, s)| s.clone()).unwrap_or_else(|| p.title().to_string());
let title = |p: PanelKind| {
titles.iter().find(|(k, _)| *k == p).map(|(_, s)| s.clone()).unwrap_or_else(|| crate::i18n::command_label("window.panel", p.title()).to_string())
};
let info = dock::TabInfo { title: &title, decos: &decos, docs: &docs };
// Where each group's tabs are (tab drags show where a tab will land between them).
let mut tab_rects: Vec<(String, Vec<(PanelKind, Rect)>)> = vec![];
@@ -570,7 +572,7 @@ impl EffectcraftApp {
}
}
// Ghost of the dragged tab.
let g = painter.layout_no_wrap(p.title().to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE);
let g = painter.layout_no_wrap(crate::i18n::command_label("window.panel", p.title()).to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE);
let gr = Rect::from_min_size(pos + vec2(12.0, 10.0), g.size() + vec2(16.0, 8.0));
painter.rect_filled(gr, 4.0, t.focus.gamma_multiply(0.85));
painter.galley(gr.min + vec2(8.0, 4.0), g, Color32::WHITE);
+3 -3
View File
@@ -259,7 +259,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if mresp.clicked() {
widgets::open_popup(ui, egui::Id::new("ws-pop"));
}
let names: Vec<String> = crate::dock::WORKSPACES.iter().map(|s| s.to_string()).collect();
let names: Vec<String> = crate::dock::WORKSPACES.iter().map(|s| crate::i18n::command_label("window.workspace", s).to_string()).collect();
if let Some(i) = widgets::popup_menu(
ui,
egui::Id::new("ws-pop"),
@@ -274,7 +274,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Small Screen, Standard.
let shown = ["Standard", "Small Screen", "Learn", "Review", "Default"];
for name in shown {
let g = p.layout_no_wrap(name.to_string(), Tokens::ui(12.0), t.text);
let g = p.layout_no_wrap(crate::i18n::command_label("window.workspace", name).to_string(), Tokens::ui(12.0), t.text);
let w = g.size().x + 16.0;
let r = Rect::from_min_max(pos2(rx - w, cy - 13.0), pos2(rx, cy + 13.0));
if r.min.x < x + 120.0 {
@@ -641,7 +641,7 @@ fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter
fn ws_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
for w in app.workspace_names() {
if ui.selectable_label(app.ui.workspace == w, &w).clicked() {
if ui.selectable_label(app.ui.workspace == w, crate::i18n::command_label("window.workspace", &w)).clicked() {
app.set_workspace(&w);
ui.close();
}
+42 -16
View File
@@ -118,25 +118,37 @@ pub(crate) fn label<'a>(app: &EffectcraftApp, command: &str, source: &'a str) ->
_ => {}
}
}
match language {
"ja" => lookup(JAPANESE, command, source),
"zh-hans" => lookup(SIMPLIFIED_CHINESE, command, source),
"zh-hant" => lookup(TRADITIONAL_CHINESE, command, source),
"uk" => lookup(UKRAINIAN, command, source),
"es" => lookup(SPANISH, command, source),
"pt-br" => lookup(PORTUGUESE, command, source),
"ru" => lookup(RUSSIAN, command, source),
"fr" => lookup(FRENCH, command, source),
"de" => lookup(GERMAN, command, source),
"cs" => lookup(CZECH, command, source),
_ => source,
}
lookup(language, command, source).unwrap_or(source)
}
/// `source` looked up in `catalog` by (command, English label); the source itself when the pair is
/// A command's label drawn outside the menu bar (the Timeline's and Composition panel's layer and
/// property context menus, the dock's tab titles, the workspace bar), in the language the panels
/// draw in: the menu bar's row for (`command`, `source`), else the panel catalog's row for
/// `source`, else `source`. Those menus write `…` where the menu bar's sources have `...`, so
/// either spelling finds the menu bar's row.
pub(crate) fn command_label<'a>(command: &str, source: &'a str) -> &'a str {
let language = ui::current();
let dotted = source.strip_suffix('…').map(|stem| format!("{stem}..."));
lookup(language, command, source).or_else(|| lookup(language, command, dotted.as_deref()?)).unwrap_or_else(|| tr(source))
}
/// The row for (command, English label) in `language`'s menu catalog; `None` when the pair is
/// absent, so computed labels and user names stay verbatim.
fn lookup<'a>(catalog: &[(&str, &str, &'a str)], command: &str, source: &'a str) -> &'a str {
catalog.iter().find(|(id, en, _)| *id == command && *en == source).map(|(_, _, translated)| *translated).unwrap_or(source)
fn lookup(language: &str, command: &str, source: &str) -> Option<&'static str> {
let catalog = match language {
"ja" => JAPANESE,
"zh-hans" => SIMPLIFIED_CHINESE,
"zh-hant" => TRADITIONAL_CHINESE,
"uk" => UKRAINIAN,
"es" => SPANISH,
"pt-br" => PORTUGUESE,
"ru" => RUSSIAN,
"fr" => FRENCH,
"de" => GERMAN,
"cs" => CZECH,
_ => return None,
};
catalog.iter().find(|(id, en, _)| *id == command && *en == source).map(|(_, _, translated)| *translated)
}
pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> String {
@@ -6617,6 +6629,20 @@ mod tests {
use serde_json::json;
use std::collections::BTreeSet;
/// Labels drawn outside the menu bar take the menu bar's row for their command (with `…` for
/// `...`), else the panel catalog's row, else stay as they are.
#[test]
fn command_label_reads_the_menu_row_then_the_panel_row() {
set_current("zh-hans");
assert_eq!(command_label("window.panel", "Project"), "项目");
let (command, en, translated) = SIMPLIFIED_CHINESE.iter().find(|(c, en, _)| !c.is_empty() && en.ends_with("...")).expect("a menu row ending in ...");
assert_eq!(command_label(command, &format!("{}…", en.trim_end_matches("..."))), *translated);
assert_eq!(command_label("no.such.command", "Cancel"), "取消");
assert_eq!(command_label("window.panel", "Comp 1"), "Comp 1");
set_current("en");
assert_eq!(command_label("window.panel", "Project"), "Project");
}
#[test]
fn czech_translates_menus_without_changing_bindings_or_user_names() {
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
+3 -2
View File
@@ -1055,7 +1055,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Transparency grid", "透明网格"),
("Tutorial complete", "教程完成"),
("Two-Node Camera", "双节点摄像机"),
("Type", "文字"),
("Type", "类型"),
("Type a command or effect…", "输入命令或效果…"),
("URL:", "URL:"),
("Undock Panel", "取消停靠面板"),
@@ -1787,7 +1787,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("Transparency grid", "透明格點"),
("Tutorial complete", "教學完成"),
("Two-Node Camera", "雙節點攝影機"),
("Type", "文字"),
("Type", "類型"),
("Type a command or effect…", "輸入命令或效果…"),
("URL:", "URL:"),
("Undock Panel", "取消停靠面板"),
@@ -4209,6 +4209,7 @@ mod tests {
const CONVERTED: &[(&str, &str)] = &[
("color_picker.rs", include_str!("../color_picker.rs")),
("credits.rs", include_str!("../credits.rs")),
("dock_ui.rs", include_str!("../dock_ui.rs")),
("header.rs", include_str!("../header.rs")),
("menus.rs", include_str!("../menus.rs")),
("widgets.rs", include_str!("../widgets.rs")),
+10 -4
View File
@@ -425,13 +425,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
} else {
let hint = "Select an item to see its details";
let r = widgets::text_fit(&p, head.center(), Align2::CENTER_CENTER, hint, Tokens::ui(11.0), head.width() - 20.0, t.text_faint);
let r = widgets::text_fit(&p, head.center(), Align2::CENTER_CENTER, crate::i18n::tr(hint), Tokens::ui(11.0), head.width() - 20.0, t.text_faint);
app.auto.add("project.details.hint", r, hint);
}
// Search.
let sr = Rect::from_min_size(pos2(rect.min.x + 8.0, head.max.y + 2.0), vec2(rect.width() - 16.0, 22.0));
let mut q = app.ui.project_search.clone();
widgets::search_field(ui, sr, &mut q, "Search project", &t);
widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search project"), &t);
app.ui.project_search = q;
app.auto.add("project.search", sr, "Search");
// Column header: Name, Label, then the visible optional columns.
@@ -463,7 +463,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
continue;
}
let resp = ui.interact(hr.intersect(hdr), egui::Id::new(("proj-sort", *key)), Sense::click());
hp.with_clip_rect(hr).text(pos2(*x, hdr.center().y), Align2::LEFT_CENTER, *label, Tokens::ui(11.0), if resp.hovered() { t.text } else { t.text_dim });
hp.with_clip_rect(hr).text(
pos2(*x, hdr.center().y),
Align2::LEFT_CENTER,
crate::i18n::tr(label),
Tokens::ui(11.0),
if resp.hovered() { t.text } else { t.text_dim },
);
if *key == "label" {
icons::paint(&hp, Rect::from_center_size(pos2(label_x, hdr.center().y), vec2(10.0, 10.0)), Icon::Keyframe, t.text_dim);
}
@@ -1094,7 +1100,7 @@ fn column_menu(app: &mut EffectcraftApp, resp: &egui::Response) {
ui.label(egui::RichText::new(crate::i18n::tr("Columns")).weak());
for (key, label, _) in COLUMNS {
let mut on = app.ui.project_columns.iter().any(|c| c == key);
if ui.checkbox(&mut on, label).changed() {
if ui.checkbox(&mut on, crate::i18n::tr(label)).changed() {
set_column(&mut app.ui.project_columns, key, on);
}
}
+6 -6
View File
@@ -1786,7 +1786,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Column headers scroll with the outline and never spill into the time ruler.
let hp = p.with_clip_rect(Rect::from_min_max(ch.min, pos2(graph_x0 - 1.0, ch.max.y)));
let hic = |icon: Icon, x: f32| icons::paint(&hp, Rect::from_center_size(pos2(x, hy), vec2(12.0, 12.0)), icon, t.text_dim);
let htext = |x: f32, s: &str| hp.text(pos2(x, hy), Align2::LEFT_CENTER, s, Tokens::ui(11.0), t.text_dim);
let htext = |x: f32, s: &str| hp.text(pos2(x, hy), Align2::LEFT_CENTER, crate::i18n::tr(s), Tokens::ui(11.0), t.text_dim);
if vis.av {
hic(Icon::Eye, cw.av + 10.0);
hic(Icon::Speaker, cw.av + 28.0);
@@ -1870,7 +1870,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
for (id, label) in COLUMNS {
let label = if id == "name" { if app.ui.timeline.source_name { "Source Name" } else { "Layer Name" } } else { label };
let on = column_visible(&app.ui.timeline, id);
if ui.add_enabled(id != "name", egui::Button::new(label).selected(on)).clicked() {
if ui.add_enabled(id != "name", egui::Button::new(crate::i18n::tr(label)).selected(on)).clicked() {
actions.push(("timeline.column".into(), json!({"column": id, "visible": !on})));
ui.close();
}
@@ -3796,7 +3796,7 @@ fn prop_menu(
) {
let (l, uid) = (layer.id.0, prop.uid);
let mut item = |ui: &mut egui::Ui, key: &str, label: &str, selected: bool, cmd: &str, params: serde_json::Value| {
let resp = ui.add(egui::Button::new(label).selected(selected));
let resp = ui.add(egui::Button::new(crate::i18n::command_label(cmd, label)).selected(selected));
auto.add(&format!("timeline.prop.{uid}.menu.{key}"), resp.rect, label);
if resp.clicked() {
actions.push((cmd.into(), params));
@@ -3877,7 +3877,7 @@ fn layer_context_menu(
pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], caps: LayerMenuCaps, actions: &mut Vec<(String, serde_json::Value)>) {
let Some(&id) = layers.first() else { return };
let mut item = |ui: &mut egui::Ui, label: &str, cmd: &str, params: serde_json::Value| {
if ui.button(label).clicked() {
if ui.button(crate::i18n::command_label(cmd, label)).clicked() {
actions.push(("layer.select".into(), json!({"layers": layers})));
actions.push((cmd.into(), params));
ui.close();
@@ -3959,10 +3959,10 @@ fn effects_submenu(ui: &mut egui::Ui, layers: &[u64], item: &mut dyn FnMut(&mut
widgets::menu_scroll(ui, |ui| {
for (cat, mut fx) in cats {
fx.sort_by_key(|e| e.name.to_ascii_lowercase());
ui.menu_button(cat, |ui| {
ui.menu_button(crate::i18n::effects::category(cat), |ui| {
widgets::menu_scroll(ui, |ui| {
for e in fx {
item(ui, e.name, "effect.apply", json!({"effect": e.id, "layers": layers}));
item(ui, &crate::i18n::effects::name(e.id, e.name), "effect.apply", json!({"effect": e.id, "layers": layers}));
}
});
});
+1 -1
View File
@@ -297,5 +297,5 @@ pub fn on_close(app: &mut EffectcraftApp, p: PanelKind) {
/// The tab label of viewer `id` ("Composition Intro").
pub fn title(app: &EffectcraftApp, id: u32) -> Option<String> {
let c = comp_of(app, id)?;
Some(format!("Composition {}", app.session.project.item(c)?.name))
Some(format!("{} {}", crate::i18n::command_label("window.panel", "Composition"), app.session.project.item(c)?.name))
}