From 29adb198727e5299bf2ab87f33d07fa4b7d8aa79 Mon Sep 17 00:00:00 2001 From: Viorel Toader Date: Wed, 21 Oct 2026 10:39:03 +0300 Subject: [PATCH 1/2] Translate form-dialog field labels, choices and notes Page Setup, Insert and the other form dialogs drew their field labels in English in every interface language, though their titles and OK/Cancel were translated and most catalogs already carry the labels. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk --- crates/ui-egui/src/dialogs.rs | 22 +++++----- .../ui-egui/tests/translated_form_fields.rs | 41 +++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 crates/ui-egui/tests/translated_form_fields.rs diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fb899c702..9dee2c886 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1389,12 +1389,14 @@ fn form(ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) /// Draws a dialog's `fields`, editing its values. fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) { + // Field labels, choices and notes are English source strings, translated as they are drawn. + let lang = crate::i18n::current(ui.ctx()); egui::Grid::new("form").num_columns(1).spacing(vec2(8.0, 6.0)).show(ui, |ui| { for f in d.fields.clone() { match f { Field::Text { key, label } => { ui.vertical(|ui| { - ui.label(label); + ui.label(lang.tr(label)); let mut s = d .values .get(key) @@ -1414,17 +1416,17 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) { Field::Password { key, label } => { // Masked as typed (Excel shows dots); "Show password" reveals it on purpose. ui.vertical(|ui| { - ui.label(label); + ui.label(lang.tr(label)); let mut s = d.values.get(key).and_then(Json::as_str).unwrap_or_default().to_string(); if ui.add(egui::TextEdit::singleline(&mut s).password(!d.show_password).desired_width(320.0)).changed() { d.values.insert(key.into(), json!(s)); } - ui.checkbox(&mut d.show_password, "Show password"); + ui.checkbox(&mut d.show_password, lang.tr("Show password")); }); } Field::Number { key, label } => { ui.horizontal(|ui| { - ui.label(label); + ui.label(lang.tr(label)); let mut n = d.values.get(key).and_then(Json::as_f64).unwrap_or(0.0); if ui.add(egui::DragValue::new(&mut n).speed(0.5)).changed() { d.values.insert(key.into(), json!(n)); @@ -1433,20 +1435,20 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) { } Field::Choice { key, label, options } => { ui.vertical(|ui| { - ui.label(egui::RichText::new(label).strong()); + ui.label(egui::RichText::new(lang.tr(label)).strong()); let cur = d.values.get(key).and_then(Json::as_str).unwrap_or("").to_string(); if d.name == "dataValidation" { - let selected = options.iter().find(|(val, _)| *val == cur).map(|(_, text)| *text).unwrap_or(""); + let selected = options.iter().find(|(val, _)| *val == cur).map(|(_, text)| lang.tr(text)).unwrap_or(""); egui::ComboBox::from_id_salt(("dataValidation", key)).selected_text(selected).width(300.0).show_ui(ui, |ui| { for (val, text) in options { - if ui.selectable_label(cur == val, text).clicked() { + if ui.selectable_label(cur == val, lang.tr(text)).clicked() { d.set_value(key, json!(val)); } } }); } else { for (val, text) in options { - if ui.radio(cur == val, text).clicked() { + if ui.radio(cur == val, lang.tr(text)).clicked() { d.set_value(key, json!(val)); } } @@ -1455,12 +1457,12 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) { } Field::Check { key, label } => { let mut b = d.values.get(key).and_then(Json::as_bool).unwrap_or(false); - if ui.checkbox(&mut b, label).changed() { + if ui.checkbox(&mut b, lang.tr(label)).changed() { d.values.insert(key.into(), json!(b)); } } Field::Note(t) => { - ui.label(egui::RichText::new(t).small()); + ui.label(egui::RichText::new(lang.tr(&t)).small()); } } ui.end_row(); diff --git a/crates/ui-egui/tests/translated_form_fields.rs b/crates/ui-egui/tests/translated_form_fields.rs new file mode 100644 index 000000000..6c3bc93ca --- /dev/null +++ b/crates/ui-egui/tests/translated_form_fields.rs @@ -0,0 +1,41 @@ +//! Form dialogs draw their field labels and choices in the interface language, as their titles +//! and OK/Cancel buttons already were (Page Setup showed "Left margin (in):" in every language). + +use egui_kittest::kittest::Queryable as _; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; +use serde_json::json; + +fn harness() -> egui_kittest::Harness<'static, SheetApp> { + let mut session = Session::new(); + session.new_workbook(); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +#[test] +fn form_fields_are_translated() { + for (dialog, translated, english) in [ + ("pageSetup", ["Margem esquerda (pol.):", "Rodapé (pol.):"], ["Left margin (in):", "Footer (in):"]), + ("insertPaste", ["Deslocar células para a direita", "Deslocar células para baixo"], ["Shift cells right", "Shift cells down"]), + ] { + let mut h = harness(); + h.state_mut().run("app.language.set", json!({"language": "pt"})).unwrap(); + h.state_mut().open_dialog(dialog, json!({})); + h.run_steps(3); + for label in translated { + assert!(h.query_by_label_contains(label).is_some(), "{dialog}: no {label}"); + } + for label in english { + assert!(h.query_by_label_contains(label).is_none(), "{dialog}: {label} is untranslated"); + } + } +} From c26290cd04799b884f9667b9cc21689d1a457ff3 Mon Sep 17 00:00:00 2001 From: Viorel Toader Date: Wed, 21 Oct 2026 11:13:57 +0300 Subject: [PATCH 2/2] Show form notes that contain a pipe as they are tr reads | as a context separator, so translating the Open Hyperlink note cut a URL with a pipe down to the part after it. Notes carry user data (URLs, file and sheet names); one with a pipe is now drawn verbatim. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk --- crates/ui-egui/src/dialogs.rs | 5 ++++- crates/ui-egui/tests/translated_form_fields.rs | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 9dee2c886..b290117c4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1462,7 +1462,10 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) { } } Field::Note(t) => { - ui.label(egui::RichText::new(lang.tr(&t)).small()); + // A note can carry user data (a URL, a file or sheet name); `tr` reads a `|` as a + // context separator, so such a note is shown as it is. + let text = if t.contains('|') { t.as_str() } else { lang.tr(&t) }; + ui.label(egui::RichText::new(text).small()); } } ui.end_row(); diff --git a/crates/ui-egui/tests/translated_form_fields.rs b/crates/ui-egui/tests/translated_form_fields.rs index 6c3bc93ca..35847a425 100644 --- a/crates/ui-egui/tests/translated_form_fields.rs +++ b/crates/ui-egui/tests/translated_form_fields.rs @@ -39,3 +39,17 @@ fn form_fields_are_translated() { } } } + +#[test] +fn notes_with_a_pipe_show_in_full() { + // `tr` reads `|` as a context separator; the Open Hyperlink note carries the URL, which must + // be shown whole so the user sees where the link really goes. + let url = "https://example.com/report|evil.example"; + for language in ["en", "pt"] { + let mut h = harness(); + h.state_mut().run("app.language.set", json!({"language": language})).unwrap(); + h.state_mut().open_dialog("openLink", json!({"url": url})); + h.run_steps(3); + assert!(h.query_by_label_contains(url).is_some(), "{language}: the link note lost part of {url}"); + } +}