From fb9da639e7185277cb763fcab3e9aa841547c0f8 Mon Sep 17 00:00:00 2001 From: Losminek <301412321+Losminek@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:50:45 +0200 Subject: [PATCH 1/5] i18n (cs): translate the Activate Panel, Move Panel and Change Panel Layout commands They appear in Keyboard Shortcuts and command search but had no Czech entry. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n/cs.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index c41eb55f7..1fd44fade 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1725,6 +1725,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Working CMYK", "Pracovní CMYK"), ("Yellow", "Žlutá"), // Labels the code passes to `tr` that no other catalog has yet. + ("Activate Panel", "Aktivovat panel"), + ("Change Panel Layout", "Změnit rozvržení panelů"), ("Close Panel", "Zavřít panel"), ("Data Fields", "Datová pole"), ("Data Merge Options", "Možnosti sloučení dat"), @@ -1735,6 +1737,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("H:", "V:"), ("Insert Field", "Vložit pole"), ("List Composite Fonts", "Vypsat složená písma"), + ("Move Panel", "Přesunout panel"), ("Object & Layout", "Objekt a rozvržení"), ("Pin Bar Position", "Připnout polohu lišty"), ("Preview Record", "Náhled záznamu"), From e24c0190f8bbb55dee9200885226b6fb7615abfc Mon Sep 17 00:00:00 2001 From: Losminek <301412321+Losminek@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:25:27 +0200 Subject: [PATCH 2/5] i18n (cs): translate labels that were looked up but missing from the catalog Find/Change > Object fill labels, the QR code field labels and the Chinese/[No Language] entries of the text-language list already go through `tr`; add their Czech strings and list them in the stale-key test's allow-list so the catalog test accepts them. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n.rs | 15 +++++++++++++++ crates/ui-egui/src/i18n/cs.rs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 5c6f674d5..873988fd1 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -3063,12 +3063,27 @@ mod tests { keys.extend(crate::menus::UI_COMMANDS.iter().map(|(_, label, _, _)| *label)); // 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?", ]); diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 1fd44fade..87c8eb3fc 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1726,19 +1726,29 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Yellow", "Žlutá"), // Labels the code passes to `tr` that no other catalog has yet. ("Activate Panel", "Aktivovat panel"), + ("Cell Number:", "Číslo mobilu:"), + ("Change fill to:", "Změnit výplň na:"), ("Change Panel Layout", "Změnit rozvržení panelů"), + ("Chinese: Simplified", "Čínština: zjednodušená"), + ("Chinese: Traditional", "Čínština: tradiční"), ("Close Panel", "Zavřít panel"), ("Data Fields", "Datová pole"), ("Data Merge Options", "Možnosti sloučení dat"), ("Distribute Rows Evenly", "Rozmístit řádky rovnoměrně"), ("Don't Save", "Neukládat"), ("Drag Rows or Columns", "Přetáhnout řádky nebo sloupce"), + ("Email Address:", "E-mailová adresa:"), + ("Email:", "E-mail:"), + ("Find fill:", "Najít výplň:"), ("Group with", "Seskupit s"), ("H:", "V:"), ("Insert Field", "Vložit pole"), ("List Composite Fonts", "Vypsat složená písma"), + ("Message:", "Zpráva:"), ("Move Panel", "Přesunout panel"), ("Object & Layout", "Objekt a rozvržení"), + ("Organization:", "Organizace:"), + ("Phone:", "Telefon:"), ("Pin Bar Position", "Připnout polohu lišty"), ("Preview Record", "Náhled záznamu"), ("Remove Data Source", "Odebrat zdroj dat"), @@ -1748,10 +1758,15 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Resize panels", "Změnit velikost panelů"), ("Set Composite Font", "Nastavit složené písmo"), ("Stop Preview", "Ukončit náhled"), + ("Subject:", "Předmět:"), + ("Text:", "Text:"), ("Unsaved Changes", "Neuložené změny"), ("Update Data Source", "Aktualizovat zdroj dat"), ("Validate from Root Element", "Ověřit od kořenového prvku"), ("W:", "Š:"), + ("(any)", "(libovolná)"), + ("(unchanged)", "(beze změny)"), + ("[No Language]", "[Žádný jazyk]"), ("“{}” has changes that are not saved. Save them before closing?", "„{}“ obsahuje neuložené změny. Uložit je před zavřením?"), ]; From 63b0425464bb11475cca2546733b248650d6f196 Mon Sep 17 00:00:00 2001 From: Losminek <301412321+Losminek@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:26:40 +0200 Subject: [PATCH 3/5] i18n (cs): translate the tool names in the Tools panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toolbar tooltips and flyouts showed the English labels from the tool catalogue. Look the labels up with `tr` and add the 37 Czech tool names (InDesign-style "Nástroj ..." naming). The stale-key test now also accepts keys that are tool labels, and a new test checks every tool has a Czech name. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n.rs | 10 ++++++++++ crates/ui-egui/src/i18n/cs.rs | 37 +++++++++++++++++++++++++++++++++++ crates/ui-egui/src/toolbar.rs | 7 ++++--- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 873988fd1..8bf06015b 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -3061,6 +3061,8 @@ 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)); // UI labels passed to `tr` that no other catalog has yet. keys.extend([ "(any)", @@ -3145,6 +3147,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 87c8eb3fc..7f261f879 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1726,44 +1726,81 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Yellow", "Žlutá"), // Labels the code passes to `tr` that no other catalog has yet. ("Activate Panel", "Aktivovat panel"), + ("Add Anchor Point Tool", "Nástroj Přidat kotevní bod"), ("Cell Number:", "Číslo mobilu:"), ("Change fill to:", "Změnit výplň na:"), ("Change Panel Layout", "Změnit rozvržení panelů"), ("Chinese: Simplified", "Čínština: zjednodušená"), ("Chinese: Traditional", "Čínština: tradiční"), ("Close Panel", "Zavřít panel"), + ("Color Theme Tool", "Nástroj Motiv barev"), + ("Content Collector Tool", "Nástroj Sběrač obsahu"), + ("Content Placer Tool", "Nástroj Umísťovač obsahu"), + ("Convert Direction Point Tool", "Nástroj Převést směrový bod"), ("Data Fields", "Datová pole"), ("Data Merge Options", "Možnosti sloučení dat"), + ("Delete Anchor Point Tool", "Nástroj Odstranit kotevní bod"), + ("Direct Selection Tool", "Nástroj Přímý výběr"), ("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:"), + ("Erase Tool", "Nástroj Guma"), + ("Eyedropper Tool", "Nástroj Kapátko"), ("Find fill:", "Najít výplň:"), + ("Free Transform Tool", "Nástroj Volná transformace"), + ("Gap Tool", "Nástroj Mezera"), + ("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"), ("Insert Field", "Vložit pole"), + ("Line Tool", "Nástroj Čára"), ("List Composite Fonts", "Vypsat složená písma"), + ("Measure Tool", "Nástroj Měřidlo"), ("Message:", "Zpráva:"), ("Move Panel", "Přesunout panel"), + ("Note Tool", "Nástroj Poznámka"), ("Object & Layout", "Objekt a rozvržení"), ("Organization:", "Organizace:"), + ("Page Tool", "Nástroj Stránka"), + ("Pen Tool", "Nástroj Pero"), + ("Pencil Tool", "Nástroj Tužka"), ("Phone:", "Telefon:"), ("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"), ("Preview Record", "Náhled záznamu"), + ("Rectangle Frame Tool", "Nástroj Obdélníkový rámeček"), + ("Rectangle Tool", "Nástroj Obdélník"), ("Remove Data Source", "Odebrat zdroj dat"), ("Remove Field", "Odebrat pole"), ("Reset Bar Position", "Obnovit polohu lišty"), ("Resize panel window", "Změnit velikost okna panelu"), ("Resize panels", "Změnit velikost panelů"), + ("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"), ("Set Composite Font", "Nastavit složené písmo"), + ("Shear Tool", "Nástroj Zkosení"), + ("Smooth Tool", "Nástroj Vyhladit"), ("Stop Preview", "Ukončit náhled"), ("Subject:", "Předmět:"), ("Text:", "Text:"), + ("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"), ("Validate from Root Element", "Ověřit od kořenového prvku"), + ("Vertical Type on a Path Tool", "Nástroj Svislý text na cestě"), + ("Vertical Type Tool", "Nástroj Svislý text"), ("W:", "Š:"), + ("Zoom Tool", "Nástroj Lupa"), ("(any)", "(libovolná)"), ("(unchanged)", "(beze změny)"), ("[No Language]", "[Žádný jazyk]"), diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 5a5e1f08a..bb123d5d2 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, ); From 314f82eba02b0a58800a0a9388574b65cd9db868 Mon Sep 17 00:00:00 2001 From: Losminek <301412321+Losminek@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:29:24 +0200 Subject: [PATCH 4/5] i18n (cs): translate status-bar messages The status line showed English for the messages the UI composes itself. Literal messages and templates now go through `tr` at their call sites ("Packaged {count} files into {dir}" and similar), the ": " messages share a small `DesignApp::status_error` helper that translates the feature name, and "Home" is translated too. Engine error texts stay as the engine wrote them. Adds the Czech strings, the stale-key allow-list entries and a test for the helper. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/chrome.rs | 4 ++-- crates/ui-egui/src/dialogs.rs | 7 +++--- crates/ui-egui/src/i18n.rs | 13 ++++++++++++ crates/ui-egui/src/i18n/cs.rs | 18 ++++++++++++++++ crates/ui-egui/src/lib.rs | 27 ++++++++++++++++++++---- crates/ui-egui/src/menus.rs | 10 ++++----- crates/ui-egui/src/panel_docking.rs | 2 +- crates/ui-egui/src/panels/conditions.rs | 6 +++--- crates/ui-egui/src/panels/glyphs.rs | 2 +- crates/ui-egui/src/panels/interactive.rs | 16 +++++++------- crates/ui-egui/src/panels/library.rs | 22 +++++++++++-------- crates/ui-egui/src/panels/notes.rs | 2 +- crates/ui-egui/src/panels/properties.rs | 4 ++-- crates/ui-egui/src/panels/table.rs | 6 +++--- 14 files changed, 97 insertions(+), 42 deletions(-) diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 2896fcdc4..bfc917945 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -121,7 +121,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. @@ -157,7 +157,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/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2ab7a3bad..371b5b6ac 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -140,7 +140,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), } } } @@ -1474,7 +1474,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), } } } @@ -1931,7 +1931,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" => { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 8bf06015b..e9809e9ee 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -3088,6 +3088,19 @@ mod tests { "Text:", "Unsaved Changes", "“{}” has changes that are not saved. Save them before closing?", + // 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. diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 7f261f879..7726b99df 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1727,6 +1727,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ // Labels the code passes to `tr` that no other catalog has yet. ("Activate Panel", "Aktivovat panel"), ("Add Anchor Point Tool", "Nástroj Přidat kotevní bod"), + ("Alternate layout", "Alternativní rozvržení"), ("Cell Number:", "Číslo mobilu:"), ("Change fill to:", "Změnit výplň na:"), ("Change Panel Layout", "Změnit rozvržení panelů"), @@ -1737,6 +1738,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Content Collector Tool", "Nástroj Sběrač obsahu"), ("Content Placer Tool", "Nástroj Umísťovač obsahu"), ("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"), @@ -1749,6 +1751,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Email Address:", "E-mailová adresa:"), ("Email:", "E-mail:"), ("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ň:"), ("Free Transform Tool", "Nástroj Volná transformace"), @@ -1758,7 +1764,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Group with", "Seskupit s"), ("H:", "V:"), ("Hand Tool", "Nástroj Ruka"), + ("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í"), ("Line Tool", "Nástroj Čára"), ("List Composite Fonts", "Vypsat složená písma"), ("Measure Tool", "Nástroj Měřidlo"), @@ -1767,7 +1775,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Note Tool", "Nástroj Poznámka"), ("Object & Layout", "Objekt a rozvržení"), ("Organization:", "Organizace:"), + ("Packaged {count} files into {dir}", "Sbaleno souborů: {count} do {dir}"), ("Page Tool", "Nástroj Stránka"), + ("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:"), @@ -1777,6 +1787,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Preview Record", "Náhled záznamu"), ("Rectangle Frame Tool", "Nástroj Obdélníkový rámeček"), ("Rectangle Tool", "Nástroj Obdélník"), + ("Relinked {count}", "Znovu propojeno: {count}"), ("Remove Data Source", "Odebrat zdroj dat"), ("Remove Field", "Odebrat pole"), ("Reset Bar Position", "Obnovit polohu lišty"), @@ -1786,12 +1797,14 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("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"), ("Subject:", "Předmět:"), ("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"), @@ -1804,6 +1817,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("(any)", "(libovolná)"), ("(unchanged)", "(beze změny)"), ("[No Language]", "[Žádný jazyk]"), + ( + "{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?"), ]; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c003154ad..6bcbf8c51 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -585,6 +585,12 @@ impl DesignApp { self.ui.status = s.into(); } + /// ": " 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) { @@ -655,7 +661,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}))?; @@ -688,7 +694,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")); } } } @@ -696,7 +702,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(); @@ -775,7 +784,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; @@ -1265,6 +1274,16 @@ mod tests { } } + #[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 14a52b58f..aa91b45f3 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -848,11 +848,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 } diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index e53986c5c..aff0fb86c 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 3fb9d00eb..5ca90b5ea 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -125,7 +125,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..ec0066133 100644 --- a/crates/ui-egui/src/panels/interactive.rs +++ b/crates/ui-egui/src/panels/interactive.rs @@ -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); } } }); @@ -176,7 +176,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 +232,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 +281,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 +310,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 +398,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 +565,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 +598,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/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 49cb920f2..7478624a9 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -780,7 +780,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(); } @@ -1908,7 +1908,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/table.rs b/crates/ui-egui/src/panels/table.rs index 0238d2960..ce3080612 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, label)).small()).clicked() && let Err(e) = app.run(id, json!({"from": from, "to": to})) { - app.status(format!("Table: {e}")); + app.status_error("Table", e); } } }); @@ -145,7 +145,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { .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")); From 59f358f709db7521c2959401c3b9e469cf175158 Mon Sep 17 00:00:00 2001 From: Losminek <301412321+Losminek@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:34:17 +0200 Subject: [PATCH 5/5] i18n (cs): translate remaining dialog, panel, menu and About strings Wrap the English captions that still bypassed `tr`: print range radios, shortcut recording prompt, Preferences snapping labels, Find/Change results, import style-conflict options, Insert Cross-Reference "Link To", Layers status line (via count_label), Properties/Table/Swatches/Task Bar tooltips, Quick Apply style categories, hyperlink/bookmark page text, Story Editor title, the About window (tabs, links, app descriptions) and the credits tables. Composed strings use named-placeholder templates; Czech counts use the existing plural helper. Adds the Czech strings, a "contributors" context for the Name column, and the matching stale-key allow-list entries. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/about.rs | 13 ++--- crates/ui-egui/src/credits.rs | 40 +++++++++------- crates/ui-egui/src/dialogs.rs | 34 ++++++------- crates/ui-egui/src/i18n.rs | 45 +++++++++++++++++ crates/ui-egui/src/i18n/cs.rs | 61 +++++++++++++++++++++++- crates/ui-egui/src/menus.rs | 2 +- crates/ui-egui/src/panels/interactive.rs | 11 +++-- crates/ui-egui/src/panels/layers.rs | 6 +-- crates/ui-egui/src/panels/properties.rs | 30 ++++++++++-- crates/ui-egui/src/panels/swatches.rs | 6 ++- crates/ui-egui/src/panels/table.rs | 12 ++++- crates/ui-egui/src/story_editor.rs | 3 +- crates/ui-egui/src/taskbar.rs | 2 +- 13 files changed, 207 insertions(+), 58 deletions(-) diff --git a/crates/ui-egui/src/about.rs b/crates/ui-egui/src/about.rs index 4eae2e7c6..238d6270c 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/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 371b5b6ac..b3ef8e0d4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -216,7 +216,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!("")); @@ -295,11 +295,11 @@ fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Pages:")); ui.vertical(|ui| { let range = d.s("range"); - if ui.radio(range == "all", "All").clicked() { + if ui.radio(range == "all", crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "All"))).clicked() { d.fields.insert("range".into(), json!("all")); } ui.horizontal(|ui| { - if ui.radio(range == "range", "Range:").clicked() { + if ui.radio(range == "range", crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Range:"))).clicked() { d.fields.insert("range".into(), json!("range")); } text_field(ui, d, "pages", 120.0); @@ -1018,12 +1018,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)); @@ -1288,7 +1288,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!("")); } @@ -1302,7 +1302,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)); @@ -1360,22 +1360,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)); @@ -1499,11 +1499,11 @@ 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")] { - if ui.radio(cur == v, l).clicked() { + if ui.radio(cur == v, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, l))).clicked() { d.fields.insert("styleConflicts".into(), json!(v)); } } @@ -3564,7 +3564,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 e9809e9ee..0f27e2b11 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -3063,6 +3063,11 @@ mod tests { 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)", @@ -3088,6 +3093,41 @@ mod tests { "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}", @@ -3130,6 +3170,8 @@ mod tests { assert_eq!(tr("cs", "Edit/Interface Language"), "Úpravy/Jazyk rozhraní"); 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]"); @@ -3152,6 +3194,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 { diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 7726b99df..ade5a1d25 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1725,24 +1725,36 @@ 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"), @@ -1750,6 +1762,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("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.", @@ -1757,42 +1770,73 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ), ("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"), @@ -1802,21 +1846,34 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("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", @@ -1834,7 +1891,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/menus.rs b/crates/ui-egui/src/menus.rs index aa91b45f3..45be9b338 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -2198,7 +2198,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/panels/interactive.rs b/crates/ui-egui/src/panels/interactive.rs index ec0066133..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() } @@ -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); } diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index a9c842116..38b808168 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -231,10 +231,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/properties.rs b/crates/ui-egui/src/panels/properties.rs index 7478624a9..2b0e9be72 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -572,7 +572,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})); @@ -592,7 +596,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})); @@ -690,7 +698,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"), @@ -1796,7 +1813,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, diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 1aa11ddb3..9fabf5178 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -226,7 +226,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 ce3080612..c1e80bd44 100644 --- a/crates/ui-egui/src/panels/table.rs +++ b/crates/ui-egui/src/panels/table.rs @@ -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,7 +149,7 @@ 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!({})) { 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 cf66e9d5f..9e48d073f 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| {