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 "<feature>: <error>"
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 <noreply@anthropic.com>
This commit is contained in:
Losminek
2026-10-11 19:29:24 +02:00
co-authored by Claude Opus 5.5
parent 63b0425464
commit 314f82eba0
14 changed files with 97 additions and 42 deletions
+2 -2
View File
@@ -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();
}
}
+4 -3
View File
@@ -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<Value, String> {
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" => {
+13
View File
@@ -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.
+18
View File
@@ -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?"),
];
+23 -4
View File
@@ -585,6 +585,12 @@ impl DesignApp {
self.ui.status = s.into();
}
/// "<feature>: <error>" 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<Value, String> {
if let Some(r) = menus::run_ui(self, id, &params) {
@@ -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.
+5 -5
View File
@@ -848,11 +848,11 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
Some(dir) => {
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
}
+1 -1
View File
@@ -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();
+3 -3
View File
@@ -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),
}
}
});
+1 -1
View File
@@ -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(
+8 -8
View File
@@ -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}));
+13 -9
View File
@@ -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}")),
}
}
+1 -1
View File
@@ -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));
+2 -2
View File
@@ -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()));
}
}
});
+3 -3
View File
@@ -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"));