mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
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:
@@ -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);
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user