Merge PR #2106: Translate form-dialog field labels, choices and notes

This commit is contained in:
aiongg
2026-10-22 02:39:04 +00:00
2 changed files with 70 additions and 10 deletions
+15 -10
View File
@@ -1419,12 +1419,14 @@ fn take_first_focus(d: &mut Dialog, r: &egui::Response) {
/// 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)
@@ -1445,17 +1447,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);
// Typed values count straight away: otherwise Enter or OK in the same frame
// confirms the old value, because a number field only commits when it loses focus.
@@ -1468,20 +1470,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));
}
}
@@ -1490,12 +1492,15 @@ 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());
// 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();
@@ -0,0 +1,55 @@
//! 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");
}
}
}
#[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}");
}
}