diff --git a/crates/ui-egui/src/about.rs b/crates/ui-egui/src/about.rs index badcb0a04..e91899e93 100644 --- a/crates/ui-egui/src/about.rs +++ b/crates/ui-egui/src/about.rs @@ -122,9 +122,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { ui.allocate_ui_with_layout(vec2(ui.available_width(), h), egui::Layout::top_down(egui::Align::Min), |ui| { ui.set_min_height(h); if app.ui.about_tab == 1 { - crate::credits::contributors_ui(ui); + crate::credits::contributors_ui(ui, &app.ui.language); } else { - crate::credits::models_ui(ui); + crate::credits::models_ui(ui, &app.ui.language); } }); ui.separator(); @@ -164,7 +164,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { .color(t.text_dim), )); ui.add_space(16.0); - if discord_button(ui, "Join the ArtCraft Discord", vec2(300.0, 40.0)) { + if discord_button(ui, crate::i18n::tr(&app.ui.language, "Join the ArtCraft Discord"), vec2(300.0, 40.0)) { open = Some("discord"); } ui.add_space(4.0); @@ -182,7 +182,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { for (text, key) in [("DesignCraft page", "appPage"), ("GitHub", "github"), ("getartcraft.com", "website"), ("Report an issue", "issues")] { - if link(ui, text, links::get(key).unwrap_or_default(), t.accent) { + if link(ui, crate::i18n::tr(&app.ui.language, text), links::get(key).unwrap_or_default(), t.accent) { open = Some(key); } ui.add_space(6.0); @@ -206,8 +206,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { ui.add_space(((ui.available_width() - w) / 2.0).max(0.0)); for (slug, name, what) in SIBLINGS { let url = format!("{}/apps/{slug}", links::WEBSITE); - let b = - ui.add(egui::Button::new(egui::RichText::new(*name).size(12.0)).corner_radius(10.0)).on_hover_text(format!("{what} — {url}")); + let b = ui + .add(egui::Button::new(egui::RichText::new(*name).size(12.0)).corner_radius(10.0)) + .on_hover_text(format!("{} — {url}", crate::i18n::tr(&app.ui.language, what))); if b.clicked() { app.ui.pending_urls.push(url); } diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 3a1d282fd..a12adeff1 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -285,7 +285,7 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) { ); ui.add_space(6.0); if icons::button(ui, "share", 22.0, false, crate::i18n::tr(&app.ui.language, "Share")).clicked() { - app.status("Export a PDF, IDML or package to share — no cloud account needed."); + app.status(crate::i18n::tr(&app.ui.language, "Export a PDF, IDML or package to share — no cloud account needed.")); } ui.add_space(8.0); // Always one click away: the ArtCraft community Discord. @@ -321,7 +321,7 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) { impl DesignApp { fn session_home(&mut self) { - self.ui.status = "Home".into(); + self.ui.status = crate::i18n::tr(&self.ui.language, "Home").into(); } } diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index 765dec19d..b9efa035a 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -7,6 +7,8 @@ use std::cmp::Ordering; use egui::RichText; +use crate::i18n::{tr, tr_context}; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Contributor { pub login: &'static str, @@ -211,45 +213,48 @@ impl Default for View { } /// About ▸ Contributors: a name toggle, a sort, and the list as a grab bag or a table. -pub fn contributors_ui(ui: &mut egui::Ui) { +pub fn contributors_ui(ui: &mut egui::Ui, lang: &str) { let id = egui::Id::new("credits_view"); let mut v = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_default(); ui.horizontal_wrapped(|ui| { - ui.label("Show"); + ui.label(tr(lang, "Show")); for m in NameMode::ALL { - if ui.selectable_label(v.names == m, m.label()).clicked() { + if ui.selectable_label(v.names == m, tr(lang, m.label())).clicked() { v.names = m; } } ui.separator(); - ui.label("Sort"); - egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { + ui.label(tr(lang, "Sort")); + egui::ComboBox::from_id_salt("credits_sort").selected_text(tr(lang, v.key.label().0)).show_ui(ui, |ui| { for k in SortKey::ALL { - if ui.selectable_label(v.key == k, k.label().0).clicked() { + if ui.selectable_label(v.key == k, tr(lang, k.label().0)).clicked() { v.key = k; v.ascending = k.default_ascending(); } } }); - if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text("Reverse the order").clicked() { + if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(tr(lang, "Reverse the order")).clicked() { v.ascending = !v.ascending; } ui.separator(); - if ui.selectable_label(!v.table, "Grab bag").clicked() { + if ui.selectable_label(!v.table, tr(lang, "Grab bag")).clicked() { v.table = false; } - if ui.selectable_label(v.table, "Table").clicked() { + if ui.selectable_label(v.table, tr(lang, "Table")).clicked() { v.table = true; } }); let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending); - ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak()); + let totals = tr(lang, "{contributors} contributors · {commits} commits") + .replace("{contributors}", &list.len().to_string()) + .replace("{commits}", &group(TOTAL_COMMITS)); + ui.label(RichText::new(totals).small().weak()); ui.separator(); egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { if list.is_empty() { - ui.label("No contributor data was built into this copy."); + ui.label(tr(lang, "No contributor data was built into this copy.")); } else if v.table { - table(ui, &list, &mut v); + table(ui, &list, &mut v, lang); } else { ui.horizontal_wrapped(|ui| { for (i, c) in list.iter().enumerate() { @@ -264,11 +269,12 @@ pub fn contributors_ui(ui: &mut egui::Ui) { ui.data_mut(|d| d.insert_temp(id, v)); } -fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) { +fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, lang: &str) { egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| { for k in SortKey::ALL { let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" }; - if ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0).clicked() { + let header = format!("{}{arrow}", tr_context(lang, k.label().1, "contributors")); + if ui.button(RichText::new(header).strong()).on_hover_text(tr(lang, k.label().0)).clicked() { if v.key == k { v.ascending = !v.ascending; } else { @@ -295,16 +301,16 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) { } /// About ▸ Models: AI models credited in Co-Authored-By trailers. -pub fn models_ui(ui: &mut egui::Ui) { +pub fn models_ui(ui: &mut egui::Ui, lang: &str) { if MODELS.is_empty() { - ui.label("No model credits were built into this copy."); + ui.label(tr(lang, "No model credits were built into this copy.")); return; } let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1); egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| { for h in ["Company", "Model", "Version", "Commits", "% of all commits", "Lines +/−"] { - ui.label(RichText::new(h).strong()); + ui.label(RichText::new(tr(lang, h)).strong()); } ui.end_row(); for m in MODELS { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 0dc637912..50bbc262d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -144,7 +144,7 @@ fn user_dictionary(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { Ok(_) => { d.fields.insert("word".into(), json!("")); } - Err(e) => app.status(format!("User Dictionary: {e}")), + Err(e) => app.status_error("User Dictionary", e), } } } @@ -220,7 +220,7 @@ fn keyboard_shortcuts(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label.trim_end_matches('…'))) .on_hover_text(format!("{id}{}", if menu.is_empty() { String::new() } else { format!(" ({menu})") })); let cur = crate::menus::shortcut_of(app, id).map(|s| crate::menus::shortcut_text(&s)).unwrap_or_else(|| "—".into()); - let text = if recording == *id { "Press keys…".to_string() } else { cur }; + let text = if recording == *id { crate::i18n::tr(&app.ui.language, "Press keys…").to_string() } else { cur }; if ui.add(egui::Button::new(text).min_size(egui::vec2(110.0, 0.0))).clicked() { d.fields.insert("recording".into(), json!(id)); d.fields.insert("message".into(), json!("")); @@ -1023,12 +1023,12 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he ui.end_row(); }); ui.add_space(6.0); - check(ui, d, "snap.alignEdges", "Align to Object Edges"); - check(ui, d, "snap.alignCenters", "Align to Object Centers"); - check(ui, d, "snap.dimensions", "Smart Dimensions"); - check(ui, d, "snap.spacing", "Smart Spacing"); + check(ui, d, "snap.alignEdges", crate::i18n::tr(&app.ui.language, "Align to Object Edges")); + check(ui, d, "snap.alignCenters", crate::i18n::tr(&app.ui.language, "Align to Object Centers")); + check(ui, d, "snap.dimensions", crate::i18n::tr(&app.ui.language, "Smart Dimensions")); + check(ui, d, "snap.spacing", crate::i18n::tr(&app.ui.language, "Smart Spacing")); ui.horizontal(|ui| { - ui.label("Snap to Zone"); + crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Snap to Zone")); let mut zone = d.n("snap.zone").filter(|z| z.is_finite()).unwrap_or(4.0); if ui.add(egui::DragValue::new(&mut zone).speed(0.1)).changed() { d.fields.insert("snap.zone".into(), json!(zone)); @@ -1302,7 +1302,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { for (key, label, any) in [("objFill", "Find fill:", "(any)"), ("objChangeFill", "Change fill to:", "(unchanged)")] { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label)); let cur = d.s(key); - egui::ComboBox::from_id_salt(key).selected_text(if cur.is_empty() { any } else { cur.as_str() }).width(180.0).show_ui(ui, |ui| { + egui::ComboBox::from_id_salt(key).selected_text(if cur.is_empty() { crate::i18n::tr(&app.ui.language, any) } else { cur.as_str() }).width(180.0).show_ui(ui, |ui| { if ui.selectable_label(cur.is_empty(), crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, any))).clicked() { d.fields.insert(key.into(), json!("")); } @@ -1316,7 +1316,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { } crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Kind:")); let cur = d.s("objKind"); - egui::ComboBox::from_id_salt("objKind").selected_text(if cur.is_empty() { "(any)" } else { cur.as_str() }).show_ui(ui, |ui| { + egui::ComboBox::from_id_salt("objKind").selected_text(if cur.is_empty() { crate::i18n::tr(&app.ui.language, "(any)") } else { cur.as_str() }).show_ui(ui, |ui| { for v in ["", "text", "graphic", "shape", "line", "group"] { if ui.selectable_label(cur == v, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, if v.is_empty() { "(any)" } else { v }))).clicked() { d.fields.insert("objKind".into(), json!(v)); @@ -1374,22 +1374,22 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Find Next"))).clicked() { let r = app.run("find.next", params()); let msg = match r { - Ok(Value::Null) => "No matches".to_string(), - Ok(_) => "Found".to_string(), + Ok(Value::Null) => crate::i18n::tr(&app.ui.language, "No matches").to_string(), + Ok(_) => crate::i18n::tr(&app.ui.language, "Found").to_string(), Err(e) => e, }; d.fields.insert("status".into(), json!(msg)); } if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Change All"))).clicked() { let msg = match app.run("find.change", params()) { - Ok(v) => format!("{} replacement(s) made", v["count"]), + Ok(v) => crate::i18n::tr(&app.ui.language, "{count} replacement(s) made").replace("{count}", &v["count"].to_string()), Err(e) => e, }; d.fields.insert("status".into(), json!(msg)); } if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Count"))).clicked() { let msg = match app.run("find.find", params()) { - Ok(v) => format!("{} match(es)", v.as_array().map(|a| a.len()).unwrap_or(0)), + Ok(v) => crate::i18n::tr(&app.ui.language, "{count} match(es)").replace("{count}", &v.as_array().map_or(0, |a| a.len()).to_string()), Err(e) => e, }; d.fields.insert("status".into(), json!(msg)); @@ -1498,7 +1498,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { let f = app.session.execute("color.settings", &json!({})).unwrap_or_default(); d.fields.insert("profiles".into(), f["profiles"].clone()); } - Err(e) => app.status(format!("Color Settings: {e}")), + Err(e) => app.status_error("Color Settings", e), } } } @@ -1523,7 +1523,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { if !d.b("removeStyles") { crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Style Name Conflicts")).font(semibold(12.0))); let conflicts: Vec = d.fields.get("conflicts").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default(); - ui.label(if conflicts.is_empty() { crate::i18n::tr(&app.ui.language, "No conflicts").to_string() } else { format!("{} conflict(s): {}", conflicts.len(), conflicts.join(", ")) }); + ui.label(if conflicts.is_empty() { crate::i18n::tr(&app.ui.language, "No conflicts").to_string() } else { crate::i18n::tr(&app.ui.language, "{count} conflict(s): {names}").replace("{count}", &conflicts.len().to_string()).replace("{names}", &conflicts.join(", ")) }); let cur = d.s("styleConflicts"); ui.horizontal(|ui| { for (v, l) in [("useExisting", "Use Document Style Definition"), ("redefine", "Redefine Document Style"), ("autoRename", "Auto Rename")] { @@ -2103,7 +2103,8 @@ pub fn confirm(app: &mut DesignApp) -> Result { json!({"printer": if printer.is_empty() { Value::Null } else { json!(printer) }, "copies": d.n("copies").unwrap_or(1.0).max(1.0) as u64, "pages": pages, "spreads": d.b("spreads"), "marks": d.b("marks"), "bleed": d.b("bleed")}), )?; - app.status(format!("Sent {} page(s) to {}", r["pages"], r["printer"].as_str().unwrap_or("the default printer"))); + let printer = r["printer"].as_str().unwrap_or_else(|| crate::i18n::tr(&app.ui.language, "the default printer")); + app.status(crate::i18n::tr(&app.ui.language, "Sent {count} page(s) to {printer}").replace("{count}", &r["pages"].to_string()).replace("{printer}", printer)); Ok(r) } "pdfExport" => { @@ -4742,7 +4743,7 @@ fn insert_xref(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let all = "[All Paragraphs]"; ui.horizontal(|ui| { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Link To:")); - combo(ui, d, "linkTo", &[("paragraph", "Paragraph"), ("anchor", "Text Anchor")]); + translated_combo(&app.ui.language, ui, d, "linkTo", &[("paragraph", "Paragraph"), ("anchor", "Text Anchor")]); }); ui.add_space(6.0); let dim = crate::theme::Tokens::get(ui.ctx()).text_dim; diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index f4326c661..5f99765a4 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -3882,16 +3882,86 @@ mod tests { keys.extend(spec.menu.iter().copied()); } keys.extend(crate::menus::UI_COMMANDS.iter().map(|(_, label, _, _)| *label)); + // Tool names reach `tr` from the Tools panel. + keys.extend(designcraft_tools::TOOL_GROUPS.iter().flat_map(|group| group.iter()).map(|tool| tool.label)); + // So do the About window's tab names, app descriptions and the credits' column and sort labels. + keys.extend(crate::about::ABOUT_TABS); + keys.extend(crate::about::SIBLINGS.iter().map(|(_, _, what)| *what)); + keys.extend(crate::credits::NameMode::ALL.iter().map(|mode| mode.label())); + keys.extend(crate::credits::SortKey::ALL.iter().flat_map(|key| [key.label().0, key.label().1])); // UI labels passed to `tr` that no other catalog has yet. keys.extend([ + "(any)", + "(unchanged)", + "[No Language]", + "Cell Number:", + "Change fill to:", + "Chinese: Simplified", + "Chinese: Traditional", "Don't Save", + "Email Address:", + "Email:", + "Find fill:", "Group with", "H:", + "Message:", + "Organization:", + "Phone:", "W:", "Resize panel window", "Resize panels", + "Subject:", + "Text:", "Unsaved Changes", "“{}” has changes that are not saved. Save them before closing?", + "% of all commits", + "{count} conflict(s): {names}", + "{count} match(es)", + "{count} replacement(s) made", + "{contributors} contributors · {commits} commits", + "{label}: {name} (double-click: Color Picker)", + "All", + "Align To", + "Auto Rename", + "Character Style", + "Company", + "DesignCraft page", + "Equal fixed heights for the selected rows; cell text may become overset.", + "Found", + "Grab bag", + "Key Object", + "layer", + "layers", + "Lines +/−", + "New Cell Style", + "New Table Style", + "No contributor data was built into this copy.", + "No matches", + "No model credits were built into this copy.", + "Object Style", + "Paragraph Style", + "Press keys…", + "Range:", + "Redefine Document Style", + "Report an issue", + "Reverse the order", + "Story {story} ← story {parent}", + "Story Editor — {count} words", + "Text Anchor", + "Use Document Style Definition", + // Status-bar messages. + "Alternate layout", + "Couldn't write recovery data: {error}", + "Export a PDF, IDML or package to share — no cloud account needed.", + "Import cancelled: the originating document has closed", + "Invalid panel layout; restored the default dock", + "PDF import cancelled: the originating document has closed", + "Packaged {count} files into {dir}", + "Relinked {count}", + "Sent {count} page(s) to {printer}", + "the default printer", + "{name}: import cancelled while a dialog is open; choose the file again", + "{name}: {count} step(s) done", ]); // A new English string falls back to English until it is translated, so only stale // entries (keys nothing passes to `tr` any more) fail. @@ -3920,6 +3990,8 @@ mod tests { assert_eq!(tr("cs", "Leading"), "Proklad"); assert_eq!(tr("cs", "Unknown label"), "Unknown label"); assert_eq!(tr_context("cs", "Group", "selection"), "Skupina"); + assert_eq!(tr_context("cs", "Name", "contributors"), "Jméno"); + assert_eq!(tr_context("cs", "Name", "unknown"), "Název"); assert_eq!(tr("cs", "Group"), "Seskupit"); assert_eq!(tr_context("cs", "Columns", "table"), tr("cs", "Columns")); assert_eq!(style_name("cs", "[Basic Paragraph]"), "[Základní odstavec]"); @@ -3942,6 +4014,9 @@ mod tests { assert_eq!(count_label("cs", "items", 1), "1 položka"); assert_eq!(count_label("cs", "objects", 7), "7 objektů"); assert_eq!(count_label("cs", "warnings", 3), "3 varování"); + assert_eq!(count_label("cs", "layer", 1), "1 vrstva"); + assert_eq!(count_label("cs", "layers", 3), "3 vrstvy"); + assert_eq!(count_label("cs", "layers", 8), "8 vrstev"); // A noun without plural forms keeps the plain translation. assert_eq!(count_label("cs", "a new noun", 3), "3 a new noun"); for (noun, forms) in cs::PLURALS { @@ -3950,6 +4025,14 @@ mod tests { } } + #[test] + fn czech_translates_every_tool_name() { + for tool in designcraft_tools::TOOL_GROUPS.iter().flat_map(|group| group.iter()) { + assert_ne!(tr("cs", tool.label), tool.label, "{}", tool.id); + assert_eq!(tr("", tool.label), tool.label); + } + } + #[test] fn japanese_vertical_type_commands_are_translated() { for (en, ja) in [ diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e07f5f67a..7e726b170 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1724,30 +1724,160 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Working CMYK", "Pracovní CMYK"), ("Yellow", "Žlutá"), // Labels the code passes to `tr` that no other catalog has yet. + ("About", "O aplikaci"), + ("Activate Panel", "Aktivovat panel"), + ("Add Anchor Point Tool", "Nástroj Přidat kotevní bod"), + ("Align To", "Zarovnat k"), + ("All", "Vše"), + ("Alternate layout", "Alternativní rozvržení"), + ("Auto Rename", "Přejmenovat automaticky"), + ("Binary assets added", "Přidané binární soubory"), + ("Binary assets removed", "Odebrané binární soubory"), + ("Cell Number:", "Číslo mobilu:"), + ("Change fill to:", "Změnit výplň na:"), + ("Change Panel Layout", "Změnit rozvržení panelů"), + ("Character Style", "Styl znaku"), + ("Chinese: Simplified", "Čínština: zjednodušená"), + ("Chinese: Traditional", "Čínština: tradiční"), ("Close Panel", "Zavřít panel"), + ("Color Theme Tool", "Nástroj Motiv barev"), + ("Commits", "Commity"), + ("Company", "Společnost"), + ("Content Collector Tool", "Nástroj Sběrač obsahu"), + ("Content Placer Tool", "Nástroj Umísťovač obsahu"), + ("Contributors", "Přispěvatelé"), + ("Convert Direction Point Tool", "Nástroj Převést směrový bod"), + ("Couldn't write recovery data: {error}", "Nelze zapsat data pro obnovu: {error}"), ("Data Fields", "Datová pole"), ("Data Merge Options", "Možnosti sloučení dat"), + ("Delete Anchor Point Tool", "Nástroj Odstranit kotevní bod"), + ("DesignCraft page", "Stránka DesignCraftu"), + ("Direct Selection Tool", "Nástroj Přímý výběr"), + ("Display name", "Zobrazované jméno"), ("Distribute Rows Evenly", "Rozmístit řádky rovnoměrně"), ("Don't Save", "Neukládat"), ("Drag Rows or Columns", "Přetáhnout řádky nebo sloupce"), + ("Ellipse Frame Tool", "Nástroj Eliptický rámeček"), + ("Ellipse Tool", "Nástroj Elipsa"), + ("Email Address:", "E-mailová adresa:"), + ("Email:", "E-mail:"), + ("Equal fixed heights for the selected rows; cell text may become overset.", "Stejné pevné výšky vybraných řádků; text buněk může přesáhnout."), + ("Erase Tool", "Nástroj Guma"), + ( + "Export a PDF, IDML or package to share — no cloud account needed.", + "Ke sdílení exportujte PDF, IDML nebo balíček – cloudový účet není potřeba.", + ), + ("Eyedropper Tool", "Nástroj Kapátko"), + ("Find fill:", "Najít výplň:"), + ("First commit", "První commit"), + ("Found", "Nalezeno"), + ("Free Transform Tool", "Nástroj Volná transformace"), + ("Gap Tool", "Nástroj Mezera"), + ("Grab bag", "Seznam"), + ("Gradient Feather Tool", "Nástroj Přechodové zjemnění okraje"), + ("Gradient Swatch Tool", "Nástroj Vzorek přechodu"), ("Group with", "Seskupit s"), ("H:", "V:"), + ("Hand Tool", "Nástroj Ruka"), + ("image editing", "úpravy obrázků"), + ("Import cancelled: the originating document has closed", "Import zrušen: původní dokument byl zavřen"), ("Insert Field", "Vložit pole"), + ("Invalid panel layout; restored the default dock", "Neplatné rozvržení panelů; obnoveno výchozí ukotvení"), + ("Key Object", "Klíčový objekt"), + ("Last commit", "Poslední commit"), + ("layer", "vrstva"), + ("layers", "vrstev"), + ("Line delta", "Rozdíl řádků"), + ("Line Tool", "Nástroj Čára"), + ("Lines +/−", "Řádky +/−"), + ("Lines added", "Přidané řádky"), + ("Lines deleted", "Odstraněné řádky"), ("List Composite Fonts", "Vypsat složená písma"), + ("Measure Tool", "Nástroj Měřidlo"), + ("Merged PRs", "Sloučené PR"), + ("Message:", "Zpráva:"), + ("Models", "Modely"), + ("motion graphics and visual effects", "pohyblivá grafika a vizuální efekty"), + ("Move Panel", "Přesunout panel"), + ("Name (A–Z)", "Jméno (A–Z)"), + ("New Cell Style", "Nový styl buňky"), + ("New Table Style", "Nový styl tabulky"), + ("No contributor data was built into this copy.", "Do této kopie nebyla zabudována data o přispěvatelích."), + ("No matches", "Žádné shody"), + ("No model credits were built into this copy.", "Do této kopie nebyly zabudovány údaje o modelech."), + ("Note Tool", "Nástroj Poznámka"), ("Object & Layout", "Objekt a rozvržení"), + ("Object Style", "Styl objektu"), + ("Organization:", "Organizace:"), + ("Packaged {count} files into {dir}", "Sbaleno souborů: {count} do {dir}"), + ("Page Tool", "Nástroj Stránka"), + ("Paragraph Style", "Styl odstavce"), + ("PDF import cancelled: the originating document has closed", "Import PDF zrušen: původní dokument byl zavřen"), + ("Pen Tool", "Nástroj Pero"), + ("Pencil Tool", "Nástroj Tužka"), + ("Phone:", "Telefon:"), + ("photo library and raw development", "knihovna fotografií a vyvolávání RAW"), ("Pin Bar Position", "Připnout polohu lišty"), + ("Polygon Frame Tool", "Nástroj Mnohoúhelníkový rámeček"), + ("Polygon Tool", "Nástroj Mnohoúhelník"), + ("Press keys…", "Stiskněte klávesy…"), ("Preview Record", "Náhled záznamu"), + ("Range:", "Rozsah:"), + ("reading, organizing and protecting PDFs", "čtení, správa a ochrana PDF"), + ("Real name", "Skutečné jméno"), + ("Rectangle Frame Tool", "Nástroj Obdélníkový rámeček"), + ("Rectangle Tool", "Nástroj Obdélník"), + ("Redefine Document Style", "Předefinovat styl dokumentu"), + ("Relinked {count}", "Znovu propojeno: {count}"), ("Remove Data Source", "Odebrat zdroj dat"), ("Remove Field", "Odebrat pole"), + ("Report an issue", "Nahlásit problém"), ("Reset Bar Position", "Obnovit polohu lišty"), ("Resize panel window", "Změnit velikost okna panelu"), ("Resize panels", "Změnit velikost panelů"), + ("Reverse the order", "Obrátit pořadí"), + ("Rotate Tool", "Nástroj Otočení"), + ("Scale Tool", "Nástroj Změna měřítka"), + ("Scissors Tool", "Nástroj Nůžky"), + ("Selection Tool", "Nástroj Výběr"), + ("Sent {count} page(s) to {printer}", "Odesláno stránek: {count} (tiskárna: {printer})"), ("Set Composite Font", "Nastavit složené písmo"), + ("Shear Tool", "Nástroj Zkosení"), + ("Smooth Tool", "Nástroj Vyhladit"), ("Stop Preview", "Ukončit náhled"), + ("Story Editor — {count} words", "Editor textu – počet slov: {count}"), + ("Story {story} ← story {parent}", "Text {story} ← text {parent}"), + ("Subject:", "Předmět:"), + ("Text Anchor", "Textová kotva"), + ("Text:", "Text:"), + ("the default printer", "výchozí"), + ("Type on a Path Tool", "Nástroj Text na cestě"), + ("Type Tool", "Nástroj Text"), ("Unsaved Changes", "Neuložené změny"), ("Update Data Source", "Aktualizovat zdroj dat"), + ("Use Document Style Definition", "Použít definici stylu v dokumentu"), + ("Username", "Uživatelské jméno"), ("Validate from Root Element", "Ověřit od kořenového prvku"), + ("vector illustration", "vektorová ilustrace"), + ("Vertical Type on a Path Tool", "Nástroj Svislý text na cestě"), + ("Vertical Type Tool", "Nástroj Svislý text"), + ("video editing, color and sound", "střih videa, barvy a zvuk"), ("W:", "Š:"), + ("Zoom Tool", "Nástroj Lupa"), + ("% of all commits", "% všech commitů"), + ("(any)", "(libovolná)"), + ("(unchanged)", "(beze změny)"), + ("[No Language]", "[Žádný jazyk]"), + ("{contributors} contributors · {commits} commits", "{contributors} přispěvatelů · {commits} commitů"), + ("{count} conflict(s): {names}", "Konflikty ({count}): {names}"), + ("{count} match(es)", "Shod: {count}"), + ("{count} replacement(s) made", "Provedeno nahrazení: {count}"), + ("{label}: {name} (double-click: Color Picker)", "{label}: {name} (dvojklik: Výběr barvy)"), + ( + "{name}: import cancelled while a dialog is open; choose the file again", + "{name}: import byl zrušen, protože je otevřený dialog; vyberte soubor znovu", + ), + ("{name}: {count} step(s) done", "{name}: hotovo kroků: {count}"), ("“{}” has changes that are not saved. Save them before closing?", "„{}“ obsahuje neuložené změny. Uložit je před zavřením?"), ]; @@ -1760,7 +1890,9 @@ pub(super) const PLURALS: &[(&str, [&str; 3])] = &[ ("error", ["chyba", "chyby", "chyb"]), ("errors", ["chyba", "chyby", "chyb"]), ("warnings", ["varování", "varování", "varování"]), + ("layer", ["vrstva", "vrstvy", "vrstev"]), + ("layers", ["vrstva", "vrstvy", "vrstev"]), ]; /// Contexts whose Czech noun differs from the shared action label. -pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Group", "selection", "Skupina")]; +pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Group", "selection", "Skupina"), ("Name", "contributors", "Jméno")]; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 00f9b396e..c5144f8e0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -656,6 +656,12 @@ impl DesignApp { self.canvas_ime } + /// ": " in the status bar: the feature name is translated, the engine's own + /// error text stays as the engine wrote it. + pub fn status_error(&mut self, feature: &str, error: impl std::fmt::Display) { + self.ui.status = format!("{}: {error}", crate::i18n::tr(&self.ui.language, feature)); + } + /// THE entry point for every action (menus, shortcuts, palette, panels, control channel). pub fn run(&mut self, id: &str, params: Value) -> Result { if let Some(r) = menus::run_ui(self, id, ¶ms) { @@ -825,7 +831,7 @@ impl DesignApp { pub(crate) fn activate_import_target(&mut self, request: &ImportRequest) -> Result<(), String> { let Some(index) = request.target.and_then(|uid| self.session.documents().iter().position(|d| d.uid == uid)) else { let message = "Import cancelled: the originating document has closed"; - self.status(message); + self.status(crate::i18n::tr(&self.ui.language, message)); return Err(message.into()); }; self.run("file.activate", json!({"index": index}))?; @@ -858,7 +864,7 @@ impl DesignApp { if owns_dialog { self.ui.dialog = None; } - self.status("PDF import cancelled: the originating document has closed"); + self.status(crate::i18n::tr(&self.ui.language, "PDF import cancelled: the originating document has closed")); } } } @@ -866,7 +872,10 @@ impl DesignApp { let files = std::mem::take(&mut *inbox.lock().unwrap_or_else(|e| e.into_inner())); for file in files { if self.ui.dialog.is_some() { - self.status(format!("{}: import cancelled while a dialog is open; choose the file again", file.name)); + self.status( + crate::i18n::tr(&self.ui.language, "{name}: import cancelled while a dialog is open; choose the file again") + .replace("{name}", &file.name), + ); continue; } let name = file.name.clone(); @@ -953,7 +962,7 @@ impl DesignApp { if self.session.documents().iter().any(|d| d.is_dirty()) && let Err(e) = self.session.execute("file.recovery.save", &json!({})) { - self.status(format!("Couldn't write recovery data: {e}")); + self.status(crate::i18n::tr(&self.ui.language, "Couldn't write recovery data: {error}").replace("{error}", &e.to_string())); } } let dt = now - self.last_time; @@ -1571,6 +1580,16 @@ mod tests { assert_eq!(alert(&app), ("Can't Place the File".to_string(), e)); } + #[test] + fn status_messages_follow_the_interface_language_but_engine_errors_stay_english() { + let mut app = DesignApp::new(Session::new(), Services::default()); + app.status_error("Table", "No table selected"); + assert_eq!(app.ui.status, "Table: No table selected"); + app.ui.language = "cs".into(); + app.status_error("Table", "No table selected"); + assert_eq!(app.ui.status, "Tabulka: No table selected"); + } + #[test] fn dropping_an_unreadable_file_alerts() { // A document opens, so it says it can't be opened. diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9de2f04f5..40e9c2a43 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -901,11 +901,11 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option { let r = app.run("file.package", json!({"dir": dir})); if let Ok(v) = &r { - app.status(format!( - "Packaged {} files into {}", - v["files"].as_array().map_or(0, |f| f.len()), - v["dir"].as_str().unwrap_or("") - )); + app.status( + crate::i18n::tr(&app.ui.language, "Packaged {count} files into {dir}") + .replace("{count}", &v["files"].as_array().map_or(0, |f| f.len()).to_string()) + .replace("{dir}", v["dir"].as_str().unwrap_or("")), + ); } r } @@ -2587,7 +2587,7 @@ pub fn palette(app: &mut DesignApp, ctx: &egui::Context) { if let Some(sc) = &it.shortcut { b = b.shortcut_text(shortcut_text(sc)); } else if !it.kind.is_empty() { - b = b.shortcut_text(it.kind); + b = b.shortcut_text(crate::i18n::tr(&app.ui.language, it.kind)); } // The button aligns its label by the layout it's in: left, like the rows with a shortcut. let size = egui::vec2(460.0, 22.0); diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 3df34d2ca..90a52df78 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -300,7 +300,7 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) -> bool { let Some(layout) = app.ui.docking.as_ref() else { return false }; if !valid(layout) { app.ui.docking = None; - app.status("Invalid panel layout; restored the default dock"); + app.status(crate::i18n::tr(&app.ui.language, "Invalid panel layout; restored the default dock")); return false; } let mut layout = layout.clone(); diff --git a/crates/ui-egui/src/panels/conditions.rs b/crates/ui-egui/src/panels/conditions.rs index 6cb3cd3e5..45eddec20 100644 --- a/crates/ui-egui/src/panels/conditions.rs +++ b/crates/ui-egui/src/panels/conditions.rs @@ -58,10 +58,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if has_text { let on = !applied.contains(&c.name); if let Err(e) = app.run("condition.apply", json!({"name": c.name, "on": on})) { - app.status(format!("Conditional Text: {e}")); + app.status_error("Conditional Text", e); } } else { - app.status("Select text to apply a condition".to_string()); + app.status(crate::i18n::tr(&app.ui.language, "Select text to apply a condition")); } } nr.context_menu(|ui| { @@ -83,7 +83,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() && !name.trim().is_empty() { match app.run("condition.new", json!({"name": name.trim()})) { Ok(_) => name.clear(), - Err(e) => app.status(format!("Conditional Text: {e}")), + Err(e) => app.status_error("Conditional Text", e), } } }); diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index de9d52d12..430268bad 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -72,7 +72,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { st.recent.insert(0, c); st.recent.truncate(12); } - Err(e) => app.status(format!("Glyphs: {e}")), + Err(e) => app.status_error("Glyphs", e), } } crate::rtl::label( diff --git a/crates/ui-egui/src/panels/interactive.rs b/crates/ui-egui/src/panels/interactive.rs index 5b770aec8..ce795857b 100644 --- a/crates/ui-egui/src/panels/interactive.rs +++ b/crates/ui-egui/src/panels/interactive.rs @@ -5,7 +5,7 @@ use serde_json::{Value, json}; use crate::DesignApp; use crate::theme::Tokens; -fn dest_text(d: &Value) -> String { +fn dest_text(lang: &str, d: &Value) -> String { if let Some(u) = d.get("url").and_then(Value::as_str) { return u.to_string(); } @@ -13,7 +13,7 @@ fn dest_text(d: &Value) -> String { return format!("mailto:{e}"); } if let Some(p) = d.get("page").and_then(Value::as_u64) { - return format!("Page {}", p + 1); + return format!("{} {}", crate::i18n::tr(lang, "Page"), p + 1); } d.to_string() } @@ -34,7 +34,7 @@ pub fn hyperlinks(app: &mut DesignApp, ui: &mut egui::Ui) { { let p = if let Some(m) = url.strip_prefix("mailto:") { json!({"email": m}) } else { json!({"url": url}) }; if let Err(e) = app.run("hyperlink.create", p) { - app.status(format!("Hyperlinks: {e}")); + app.status_error("Hyperlinks", e); } } }); @@ -55,7 +55,7 @@ pub fn hyperlinks(app: &mut DesignApp, ui: &mut egui::Ui) { for h in list { let hid = h["id"].clone(); ui.horizontal(|ui| { - let r = ui.selectable_label(false, h["name"].as_str().unwrap_or("")).on_hover_text(dest_text(&h["dest"])); + let r = ui.selectable_label(false, h["name"].as_str().unwrap_or("")).on_hover_text(dest_text(&app.ui.language, &h["dest"])); if r.clicked() { let _ = app.run("hyperlink.goToSource", json!({"id": hid})); } @@ -88,7 +88,10 @@ pub fn bookmarks(app: &mut DesignApp, ui: &mut egui::Ui) { for (i, b) in list.iter().enumerate() { let page = b["page"].as_u64().unwrap_or(0) as usize; ui.horizontal(|ui| { - if ui.selectable_label(false, b["name"].as_str().unwrap_or("")).on_hover_text(format!("Page {}", app.session.page_label(page))).clicked() + if ui + .selectable_label(false, b["name"].as_str().unwrap_or("")) + .on_hover_text(format!("{} {}", crate::i18n::tr(&app.ui.language, "Page"), app.session.page_label(page))) + .clicked() { crate::canvas::go_to_page(app, page); } @@ -176,7 +179,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() && !name.trim().is_empty() { match app.run("xml.newTag", json!({"name": name.trim()})) { Ok(_) => name.clear(), - Err(e) => app.status(format!("Tags: {e}")), + Err(e) => app.status_error("Tags", e), } } }); @@ -232,7 +235,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) { { match app.run("xml.loadDtd", json!({"path": path})) { Ok(_) => problems = None, - Err(e) => app.status(format!("Load DTD: {e}")), + Err(e) => app.status_error("Load DTD", e), } } let has_dtd = info["dtd"].as_bool().unwrap_or(false); @@ -281,7 +284,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("xml")) && let Err(e) = app.run("file.importXml", json!({"path": path})) { - app.status(format!("Import XML: {e}")); + app.status_error("Import XML", e); } }); } @@ -310,7 +313,7 @@ pub fn states(app: &mut DesignApp, ui: &mut egui::Ui) { .clicked() && let Err(e) = app.run("states.create", json!({})) { - app.status(format!("Object States: {e}")); + app.status_error("Object States", e); } return; } @@ -398,7 +401,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) { if let Some(p) = params && let Err(e) = app.run("button.set", p) { - app.status(format!("Buttons: {e}")); + app.status_error("Buttons", e); } crate::rtl::label( ui, @@ -565,7 +568,7 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Create"))).clicked() && let Err(e) = app.run("layout.createAlternate", json!({"name": name, "width": w, "height": h})) { - app.status(format!("Alternate layout: {e}")); + app.status_error("Alternate layout", e); } ui.data_mut(|x| x.insert_temp(key, (name, w, h))); } @@ -598,7 +601,7 @@ pub fn media(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("place")) && let Err(e) = app.run("media.options", json!({"poster": {"path": path}})) { - app.status(format!("Media: {e}")); + app.status_error("Media", e); } if !m["poster"].is_null() && ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "None"))).clicked() { let _ = app.run("media.options", json!({"poster": null})); diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 7f4793115..d391e7a59 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -479,10 +479,10 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.label( egui::RichText::new(format!( - "Page: {}, {} Layer{}", + "{}: {}, {}", + crate::i18n::tr(&app.ui.language, "Page"), app.session.page_label(cur), - doc.layers.len(), - if doc.layers.len() == 1 { "" } else { "s" } + crate::i18n::count_label(&app.ui.language, if doc.layers.len() == 1 { "layer" } else { "layers" }, doc.layers.len()) )) .size(11.0) .color(t.text_dim), diff --git a/crates/ui-egui/src/panels/library.rs b/crates/ui-egui/src/panels/library.rs index ccaa595dd..f659c7c1d 100644 --- a/crates/ui-egui/src/panels/library.rs +++ b/crates/ui-egui/src/panels/library.rs @@ -40,7 +40,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("library")) && let Err(e) = app.run("library.open", json!({"path": path})) { - app.status(format!("Library: {e}")); + app.status_error("Library", e); } }); let Ok(items) = app.session.execute("library.list", &json!({})) else { @@ -51,7 +51,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.add_enabled(has_sel, egui::Button::new(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Add Selection")))).clicked() && let Err(e) = app.run("library.add", json!({})) { - app.status(format!("Library: {e}")); + app.status_error("Library", e); } ui.separator(); let items = items.as_array().cloned().unwrap_or_default(); @@ -77,7 +77,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Place"))).clicked() && let Err(e) = app.run("library.place", json!({"index": i})) { - app.status(format!("Library: {e}")); + app.status_error("Library", e); } }); }); @@ -93,13 +93,13 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some(path) = app.services.pick_save.as_mut().and_then(|f| f("Book.dcbook")) && let Err(e) = app.run("book.new", json!({"path": path})) { - app.status(format!("Book: {e}")); + app.status_error("Book", e); } if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Open…"))).clicked() && let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("book")) && let Err(e) = app.run("book.open", json!({"path": path})) { - app.status(format!("Book: {e}")); + app.status_error("Book", e); } }); let Ok(info) = app.session.execute("book.list", &json!({})) else { @@ -145,7 +145,7 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("open")) && let Err(e) = app.run("book.add", json!({"path": path})) { - app.status(format!("Book: {e}")); + app.status_error("Book", e); } if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Update Numbering"))).clicked() { let _ = app.run("book.paginate", json!({})); @@ -157,7 +157,7 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some(path) = app.services.pick_save.as_mut().and_then(|f| f("Book.pdf")) && let Err(e) = app.run("book.exportPdf", json!({"path": path})) { - app.status(format!("Book: {e}")); + app.status_error("Book", e); } }); } @@ -202,7 +202,7 @@ pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) { app.ui.scripts.push((name, text)); sel = app.ui.scripts.len() - 1; } - Err(e) => app.status(format!("Scripts: {e}")), + Err(e) => app.status_error("Scripts", e), } } if ui @@ -222,7 +222,11 @@ pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) { && let Some((name, text)) = app.ui.scripts.get(i).cloned() { match app.run("script.run", json!({"text": text})) { - Ok(r) => app.status(format!("{name}: {} step(s) done", r["steps"])), + Ok(r) => app.status( + crate::i18n::tr(&app.ui.language, "{name}: {count} step(s) done") + .replace("{name}", &name) + .replace("{count}", &r["steps"].to_string()), + ), Err(e) => app.status(format!("{name}: {e}")), } } diff --git a/crates/ui-egui/src/panels/notes.rs b/crates/ui-egui/src/panels/notes.rs index 5139a090d..6eebb7411 100644 --- a/crates/ui-egui/src/panels/notes.rs +++ b/crates/ui-egui/src/panels/notes.rs @@ -28,7 +28,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { { match app.run("note.new", json!({"text": draft.trim()})) { Ok(_) => draft.clear(), - Err(e) => app.status(format!("Notes: {e}")), + Err(e) => app.status_error("Notes", e), } } ui.data_mut(|d| d.insert_temp(id, draft)); diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 05e348b8a..8a58eab47 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -595,7 +595,11 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) { let chip = sub(b, 4.0, 2.0, 17.0, 17.0); widgets::paint_chip(ui.painter(), chip, fc, fg); ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); - let resp = ui.interact(chip, ui.id().with("fillchip"), Sense::click()).on_hover_text(format!("Fill: {}", i.fill)); + let resp = ui.interact(chip, ui.id().with("fillchip"), Sense::click()).on_hover_text(format!( + "{}: {}", + crate::i18n::tr(&app.ui.language, "Fill"), + i.fill + )); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Fill swatch")); swatch_menu(app, &resp, &doc, &i.fill, |app, n| { let _ = app.run("object.fill", json!({"swatch": n})); @@ -615,7 +619,11 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) { let chip = sub(b, 4.0, 2.0, 17.0, 17.0); widgets::paint_stroke_chip(ui.painter(), chip, sc, egui::Color32::from_gray(20)); ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); - let resp = ui.interact(chip, ui.id().with("strokechip"), Sense::click()).on_hover_text(format!("Stroke: {}", i.stroke)); + let resp = ui.interact(chip, ui.id().with("strokechip"), Sense::click()).on_hover_text(format!( + "{}: {}", + crate::i18n::tr(&app.ui.language, "Stroke"), + i.stroke + )); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Stroke swatch")); swatch_menu(app, &resp, &doc, &i.stroke, |app, n| { let _ = app.run("object.stroke", json!({"swatch": n})); @@ -695,7 +703,16 @@ fn align_row(app: &mut DesignApp, ui: &mut Ui) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 8.0; let to = app.ui.align_to.clone(); - let resp = icon_toggle(ui, "align-to", false, &format!("Align To: {to}")); + let to_label = match to.as_str() { + "selection" => "Selection", + "keyObject" => "Key Object", + "margins" => "Margins", + "page" => "Page", + "spread" => "Spread", + other => other, + }; + let tip = format!("{}: {}", crate::i18n::tr(&app.ui.language, "Align To"), crate::i18n::tr(&app.ui.language, to_label)); + let resp = icon_toggle(ui, "align-to", false, &tip); egui::Popup::menu(&resp).show(|ui| { for (label, id) in [ ("Align to Selection", "selection"), @@ -785,7 +802,7 @@ fn convert_shape_menu(app: &mut DesignApp, resp: &egui::Response) { ] { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() { if let Err(e) = app.run("object.convertShape", json!({"to": to})) { - app.status(format!("Convert Shape: {e}")); + app.status_error("Convert Shape", e); } ui.close(); } @@ -1896,7 +1913,12 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) { for l in &stories { let out = l["outOfDate"].as_bool().unwrap_or(false); ui.horizontal(|ui| { - crate::rtl::label(ui, format!("Story {} ← story {}", l["story"], l["parent"])); + crate::rtl::label( + ui, + crate::i18n::tr(&app.ui.language, "Story {story} ← story {parent}") + .replace("{story}", &l["story"].to_string()) + .replace("{parent}", &l["parent"].to_string()), + ); if out { crate::rtl::label( ui, @@ -2008,7 +2030,7 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) { { let r = app.run("links.relinkFolder", json!({"dir": dir.to_string_lossy()})); if let Ok(v) = r { - app.status(format!("Relinked {}", v["relinked"])); + app.status(crate::i18n::tr(&app.ui.language, "Relinked {count}").replace("{count}", &v["relinked"].to_string())); } } }); diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 5e71f71e8..d5432035a 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -251,7 +251,11 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) { if k == stroke { ui.painter().rect_stroke(r, 2.0, egui::Stroke::new(1.5, t.accent), egui::StrokeKind::Inside); } - let resp = resp.on_hover_text(format!("{label}: {n} (double-click: Color Picker)")); + let resp = resp.on_hover_text( + crate::i18n::tr(&app.ui.language, "{label}: {name} (double-click: Color Picker)") + .replace("{label}", crate::i18n::tr(&app.ui.language, label)) + .replace("{name}", n), + ); if resp.clicked() { stroke = k; } diff --git a/crates/ui-egui/src/panels/table.rs b/crates/ui-egui/src/panels/table.rs index 79b434a76..ba653364e 100644 --- a/crates/ui-egui/src/panels/table.rs +++ b/crates/ui-egui/src/panels/table.rs @@ -36,7 +36,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Graphic…"))).clicked() && let Err(e) = app.run("app.graphicCell", json!({})) { - app.status(format!("Table: {e}")); + app.status_error("Table", e); } if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Text"))).clicked() { let _ = app.run("table.textCell", json!({})); @@ -55,7 +55,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if ui.add_enabled(ok, egui::Button::new(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).small()).clicked() && let Err(e) = app.run(id, json!({"from": from, "to": to})) { - app.status(format!("Table: {e}")); + app.status_error("Table", e); } } }); @@ -76,7 +76,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { } } }); - if crate::icons::button(ui, "plus", 20.0, false, &format!("New {label}")).clicked() { + if crate::icons::button( + ui, + "plus", + 20.0, + false, + crate::i18n::tr(&app.ui.language, if kind == "cell" { "New Cell Style" } else { "New Table Style" }), + ) + .clicked() + { if kind == "cell" { run(app, "style.cell.create", json!({"name": "Cell Style 1", "fromSelection": true})); } else { @@ -141,11 +149,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { }); if ui .button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Distribute Rows Evenly"))) - .on_hover_text("Equal fixed heights for the selected rows; cell text may become overset.") + .on_hover_text(crate::i18n::tr(&app.ui.language, "Equal fixed heights for the selected rows; cell text may become overset.")) .clicked() && let Err(e) = app.run("table.distributeRows", json!({})) { - app.status(format!("Table: {e}")); + app.status_error("Table", e); } divider(ui); caption(ui, crate::i18n::tr(&app.ui.language, "Cell Text")); diff --git a/crates/ui-egui/src/story_editor.rs b/crates/ui-egui/src/story_editor.rs index 5b093ab41..19beb1384 100644 --- a/crates/ui-egui/src/story_editor.rs +++ b/crates/ui-egui/src/story_editor.rs @@ -23,7 +23,8 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { let overset = cs.overset_at; let mut open = true; let mut edit: Option<(usize, usize, String)> = None; - egui::Window::new(format!("Story Editor — {} words", story.text.split_whitespace().count())) + let words = story.text.split_whitespace().count().to_string(); + egui::Window::new(crate::i18n::tr(&app.ui.language, "Story Editor — {count} words").replace("{count}", &words)) .id(egui::Id::new("story_editor")) .open(&mut open) .default_size(vec2(560.0, 520.0)) diff --git a/crates/ui-egui/src/taskbar.rs b/crates/ui-egui/src/taskbar.rs index eaf5e7cce..7332064ba 100644 --- a/crates/ui-egui/src/taskbar.rs +++ b/crates/ui-egui/src/taskbar.rs @@ -164,7 +164,7 @@ fn text_controls(app: &mut DesignApp, ui: &mut Ui, in_text: bool) { if let Some(doc) = app.session.active().map(|d| d.doc.clone()) { let sw = c["fill"].as_str().unwrap_or("[Black]").to_string(); let (col, g) = widgets::swatch_colors(&doc, &sw, 1.0); - let resp = widgets::swatch_chip(ui, 17.0, col, g).on_hover_text(format!("Fill: {sw}")); + let resp = widgets::swatch_chip(ui, 17.0, col, g).on_hover_text(format!("{}: {sw}", crate::i18n::tr(&app.ui.language, "Fill"))); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Task Bar fill")); let mut picked = None; egui::Popup::menu(&resp).show(|ui| { diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 03b297dd4..f404ae648 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -74,9 +74,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { let g = TOOL_GROUPS[gi]; let shown = g.iter().find(|tl| tl.id == current).unwrap_or(&g[0]); let active = g.iter().any(|tl| tl.id == current); + let name = crate::i18n::tr(&app.ui.language, shown.label); let tip = match shown.shortcut { - Some(s) => format!("{} ({s})", shown.label), - None => shown.label.to_string(), + Some(s) => format!("{name} ({s})"), + None => name.to_string(), }; let resp = tool_button(ui, shown.icon, active, &tip); if g.len() > 1 { @@ -135,7 +136,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { ui.painter().text( r.min + vec2(28.0, 12.0), egui::Align2::LEFT_CENTER, - tl.label, + crate::i18n::tr(&app.ui.language, tl.label), egui::FontId::proportional(12.5), t.text, );