From 31abf075b74b542aca0a668993a502b98a982c3c Mon Sep 17 00:00:00 2001 From: Lord-Taw <149683190+Lord-Taw@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:33:07 -0300 Subject: [PATCH] Translate and standardizes UI error feedback by locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This change centralizes the translation of user-facing error messages in the UI layer while preserving the original error payloads for machine consumption and AI-agent integrations. ### What changed - Added a centralized error-translation path in `i18n.rs`. - The UI now translates only the final display text, instead of altering the underlying engine or protocol error payload. - If a translated string is not available, the original message is kept as a fallback, avoiding broken or overly-simplified output. - The translation logic is applied at the presentation layer, not at the source of the error. ### Updated call sites - Error and alert rendering in `widgets.rs` - Dialog error presentation in `dialogs.rs` - Print and feedback failures in `printing.rs` - Top-level UI error handling in `lib.rs` ### Design intent - Keep the raw engine errors intact for scripts, control channels, and AI/agent integrations. - Present a clearer, localized message only to end users. - Avoid modifying dictionary files or adding new translation entries to the global catalog unnecessarily. - Keep the behavior consistent with the project’s accessibility and internationalization goals. ### Validation - The focused i18n test passed successfully. - The relevant UI test suite passed successfully. - The repository-required CI gate was run and completed without failures according to the latest verification output. --- crates/ui-egui/src/dialogs.rs | 2 +- crates/ui-egui/src/i18n.rs | 10 ++++++++++ crates/ui-egui/src/lib.rs | 2 +- crates/ui-egui/src/printing.rs | 6 +++--- crates/ui-egui/src/widgets.rs | 3 +++ 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fb899c702..a1ecdcb09 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2612,7 +2612,7 @@ fn solver_results(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, open: & fn spelling(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, open: &mut bool) { let r = d.result.clone().unwrap_or(Json::Null); if let Some(e) = r.get("error").and_then(Json::as_str) { - ui.label(crate::clean_error(e)); + ui.label(app.ui.language.tr_error(e)); return; } let issues = r["issues"].as_array().cloned().unwrap_or_default(); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 061698df9..67f673dc1 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -132,6 +132,13 @@ impl Language { }; translated.unwrap_or_else(|| text.rsplit_once('|').map_or(text, |(_, label)| label)) } + + pub(crate) fn tr_error(self, message: &str) -> String { + let message = crate::clean_error(message); + let translated = self.tr(&message); + let fallback = message.rsplit_once('|').map_or(message.as_str(), |(_, label)| label); + if translated == fallback { message } else { translated.to_owned() } + } } /// Deserializes a saved language leniently: an unknown or malformed value (a language removed in a @@ -617,6 +624,9 @@ mod tests { assert_eq!(Language::Ko.tr("Data"), "데이터"); assert_eq!(Language::Ru.tr("Data"), "Данные"); assert_eq!(Language::PtBr.tr("Data"), "Dados"); + assert_eq!(Language::PtBr.tr_error("Select a chart first."), "Selecione um gráfico primeiro."); + assert_eq!(Language::PtBr.tr_error("Sheet|Unknown failure"), "Sheet|Unknown failure"); + assert_eq!(Language::PtBr.tr_error("command `chart`: Select a chart first."), "Selecione um gráfico primeiro."); assert_eq!(Language::Es.tr("Data"), "Datos"); assert_eq!(Language::Es.tr("Freeze Panes"), "Inmovilizar paneles"); assert_eq!(Language::Es.tr("Sheet1!A1"), "Sheet1!A1"); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f96cdac3c..f0f720442 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -980,7 +980,7 @@ impl SheetApp { else { return; }; - let text = printing::feedback(&message, |label| self.ui.language.tr(label).to_owned()); + let text = printing::feedback(&message, self.ui.language, |label| self.ui.language.tr(label).to_owned()); if matches!(message, printing::Feedback::Submitted(_)) { self.toast = Some((text, now_ms())); } else { diff --git a/crates/ui-egui/src/printing.rs b/crates/ui-egui/src/printing.rs index 7c34120bb..7193678c2 100644 --- a/crates/ui-egui/src/printing.rs +++ b/crates/ui-egui/src/printing.rs @@ -17,7 +17,7 @@ pub fn show(state: &mut State, ui: &mut egui::Ui, discover: &Discover, translate }; state.show(ui, egui::Id::new("gridcraft_print_settings"), discover, &labels); if let Some(message) = &state.message { - ui.label(feedback(message, translate)); + ui.label(feedback(message, crate::i18n::current(ui.ctx()), translate)); } } @@ -84,7 +84,7 @@ pub fn settings(state: &mut State, params: &serde_json::Value, discover: Option< })) } -pub fn feedback(message: &Feedback, translate: impl Fn(&str) -> String) -> String { +pub fn feedback(message: &Feedback, language: crate::i18n::Language, translate: impl Fn(&str) -> String) -> String { match message { Feedback::FindingPrinters => translate("Finding printers…"), Feedback::NoPrinters => translate("No printers found. Configure a system printer or export a PDF."), @@ -92,7 +92,7 @@ pub fn feedback(message: &Feedback, translate: impl Fn(&str) -> String) -> Strin Feedback::Submitting => translate("Submitting print job…"), Feedback::Submitted(receipt) if receipt.trim().is_empty() => translate("Print job submitted."), Feedback::Submitted(receipt) => translate("Print job submitted: {receipt}").replace("{receipt}", receipt), - Feedback::Failed(error) => translate("Print failed: {error}").replace("{error}", error), + Feedback::Failed(error) => translate("Print failed: {error}").replace("{error}", &language.tr_error(error)), Feedback::WorkerStopped => translate("Print failed: the printing worker stopped."), } } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index e24105b49..6de95d1af 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -272,6 +272,8 @@ pub fn color_palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str) -> pub fn message_box(app: &mut SheetApp, ctx: &egui::Context) { let Some((title, msg)) = app.message.clone() else { return }; + let title = app.ui.language.tr(&title).to_owned(); + let msg = app.ui.language.tr_error(&msg); let mut close = false; egui::Modal::new(egui::Id::new("message_box")).show(ctx, |ui| { ui.set_width(380.0); @@ -302,6 +304,7 @@ pub fn toast(app: &mut SheetApp, ctx: &egui::Context) { app.toast = None; return; } + let msg = app.ui.language.tr_error(&msg); let Some(rect) = app.grid.rect else { return }; egui::Area::new(egui::Id::new("toast")).fixed_pos(pos2(rect.center().x - 160.0, rect.bottom() - 48.0)).order(egui::Order::Tooltip).show( ctx,