diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5e859c6..ade0968 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -891,7 +891,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) { "solver" => 460.0, _ => 380.0, }; - let window = egui::Window::new(d.title.clone()) + let window = egui::Window::new(app.ui.language.tr(&d.title)) .id(egui::Id::new("dialog").with(&d.name)) .collapsible(false) .resizable(false) @@ -1223,11 +1223,15 @@ fn paste_special_shortcuts(d: &mut Dialog, ctx: &egui::Context) { fn ok_cancel(ui: &mut egui::Ui, confirm: &mut bool, open: &mut bool) { ui.add_space(8.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let ok = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(crate::theme::Tokens::get(ui.ctx()).accent)); + let lang = crate::i18n::current(ui.ctx()); + let ok = ui.add( + egui::Button::new(egui::RichText::new(format!(" {} ", lang.tr("OK"))).color(Color32::WHITE)) + .fill(crate::theme::Tokens::get(ui.ctx()).accent), + ); if ok.clicked() || ui.input(|i| i.key_pressed(Key::Enter)) { *confirm = true; } - if ui.button(" Cancel ").clicked() { + if ui.button(format!(" {} ", lang.tr("Cancel"))).clicked() { *open = false; } }); @@ -1362,9 +1366,20 @@ fn macros(ui: &mut egui::Ui, d: &mut Dialog) { } fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) { + // `d.tab` stays the English name (it is what `open_dialog` and tests pass); only the drawn + // labels are translated. The Fill tab is keyed by context: the Alignment tab's "Fill" is a + // different word in some languages. + let lang = app.ui.language; ui.horizontal(|ui| { - for tab in ["Number", "Alignment", "Font", "Border", "Fill", "Protection"] { - if ui.selectable_label(d.tab == tab, tab).clicked() { + for (tab, key) in [ + ("Number", "Number"), + ("Alignment", "Alignment"), + ("Font", "Font"), + ("Border", "Border"), + ("Fill", "Format Cells|Fill"), + ("Protection", "Protection"), + ] { + if ui.selectable_label(d.tab == tab, lang.tr(key)).clicked() { d.tab = tab.to_string(); } } @@ -1376,20 +1391,20 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: "Number" => { let mut code = st.num_fmt.as_str().to_string(); ui.columns(2, |cols| { - cols[0].label(egui::RichText::new("Category:").strong()); + cols[0].label(egui::RichText::new(lang.tr("Category:")).strong()); for name in ["General", "Number", "Currency", "Accounting", "Short Date", "Long Date", "Time", "Percentage", "Fraction", "Scientific", "Text"] { let c = gridcraft_engine::cmd::format::format_code_for(name).to_string(); - if cols[0].selectable_label(code == c, name).clicked() { + if cols[0].selectable_label(code == c, lang.tr(name)).clicked() { code = c; } } - cols[1].label(egui::RichText::new("Sample").strong()); + cols[1].label(egui::RichText::new(lang.tr("Sample")).strong()); let preview = app.session.active().map(|doc| gridcraft_engine::display::format(&sample, &code, &doc.wb).text).unwrap_or_default(); cols[1].label(egui::RichText::new(preview).font(theme::ui_font(15.0))); cols[1].add_space(8.0); - cols[1].label("Type (custom format code):"); + cols[1].label(lang.tr("Type (custom format code):")); cols[1].text_edit_singleline(&mut code); for c in [ "0", @@ -1418,7 +1433,7 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: } "Alignment" => { use gridcraft_engine::model::{HAlign, VAlign}; - ui.label(egui::RichText::new("Horizontal").strong()); + ui.label(egui::RichText::new(lang.tr("Horizontal")).strong()); ui.horizontal_wrapped(|ui| { for (h, n) in [ (HAlign::General, "General"), @@ -1430,10 +1445,10 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: (HAlign::CenterAcross, "Center Across Selection"), (HAlign::Distributed, "Distributed"), ] { - ui.radio_value(&mut st.align.h, h, n); + ui.radio_value(&mut st.align.h, h, lang.tr(n)); } }); - ui.label(egui::RichText::new("Vertical").strong()); + ui.label(egui::RichText::new(lang.tr("Vertical")).strong()); ui.horizontal_wrapped(|ui| { for (v, n) in [ (VAlign::Top, "Top"), @@ -1442,29 +1457,29 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: (VAlign::Justify, "Justify"), (VAlign::Distributed, "Distributed"), ] { - ui.radio_value(&mut st.align.v, v, n); + ui.radio_value(&mut st.align.v, v, lang.tr(n)); } }); let mut indent = st.align.indent as f32; - ui.add(egui::Slider::new(&mut indent, 0.0..=15.0).text("Indent")); + ui.add(egui::Slider::new(&mut indent, 0.0..=15.0).text(lang.tr("Indent"))); st.align.indent = indent as u8; let mut rot = if st.align.rotation == 255 { 0.0 } else { st.align.rotation as f32 }; - ui.add(egui::Slider::new(&mut rot, -90.0..=90.0).text("Orientation (degrees)")); + ui.add(egui::Slider::new(&mut rot, -90.0..=90.0).text(lang.tr("Orientation (degrees)"))); st.align.rotation = rot as i16; - ui.checkbox(&mut st.align.wrap, "Wrap text"); - ui.checkbox(&mut st.align.shrink, "Shrink to fit"); + ui.checkbox(&mut st.align.wrap, lang.tr("Wrap text")); + ui.checkbox(&mut st.align.shrink, lang.tr("Shrink to fit")); } "Font" => { - egui::ComboBox::from_label("Font").selected_text(st.font.name.clone()).show_ui(ui, |ui| { + egui::ComboBox::from_label(lang.tr("Font")).selected_text(st.font.name.clone()).show_ui(ui, |ui| { for f in crate::fonts::families() { ui.selectable_value(&mut st.font.name, f.clone(), f.clone()); } }); - ui.add(egui::Slider::new(&mut st.font.size, 6.0..=72.0).text("Size")); + ui.add(egui::Slider::new(&mut st.font.size, 6.0..=72.0).text(lang.tr("Size"))); ui.horizontal(|ui| { - ui.checkbox(&mut st.font.bold, "Bold"); - ui.checkbox(&mut st.font.italic, "Italic"); - ui.checkbox(&mut st.font.strike, "Strikethrough"); + ui.checkbox(&mut st.font.bold, lang.tr("Bold")); + ui.checkbox(&mut st.font.italic, lang.tr("Italic")); + ui.checkbox(&mut st.font.strike, lang.tr("Strikethrough")); }); use gridcraft_engine::model::{Underline, VertAlign}; ui.horizontal(|ui| { @@ -1475,17 +1490,17 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: (Underline::SingleAccounting, "Single Accounting"), (Underline::DoubleAccounting, "Double Accounting"), ] { - ui.radio_value(&mut st.font.underline, u, n); + ui.radio_value(&mut st.font.underline, u, lang.tr(n)); } }); ui.horizontal(|ui| { - ui.radio_value(&mut st.font.vert, VertAlign::Baseline, "Normal"); - ui.radio_value(&mut st.font.vert, VertAlign::Superscript, "Superscript"); - ui.radio_value(&mut st.font.vert, VertAlign::Subscript, "Subscript"); + ui.radio_value(&mut st.font.vert, VertAlign::Baseline, lang.tr("Normal")); + ui.radio_value(&mut st.font.vert, VertAlign::Superscript, lang.tr("Superscript")); + ui.radio_value(&mut st.font.vert, VertAlign::Subscript, lang.tr("Subscript")); }); - ui.label("Color:"); + ui.label(lang.tr("Color:")); let colors = app.session.active().map(|doc| doc.wb.theme.colors).unwrap_or_default(); - if let Some(c) = crate::widgets::color_palette(ui, &colors, "Automatic") { + if let Some(c) = crate::widgets::color_palette(ui, &colors, lang.tr("Automatic")) { st.font.color = if c == "none" { gridcraft_engine::model::Color::Auto } else { gridcraft_engine::model::Color::from_hex(&c).unwrap_or_default() }; } @@ -1494,10 +1509,10 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: use gridcraft_engine::model::{BorderLine, BorderStyle}; let line = BorderLine { style: BorderStyle::Thin, color: gridcraft_engine::model::Color::Auto }; ui.horizontal(|ui| { - if ui.button("None").clicked() { + if ui.button(lang.tr("None")).clicked() { st.borders = Default::default(); } - if ui.button("Outline").clicked() { + if ui.button(lang.tr("Outline")).clicked() { st.borders.top = line; st.borders.bottom = line; st.borders.left = line; @@ -1512,6 +1527,7 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: ("Diagonal ↘", &mut st.borders.diag_down), ("Diagonal ↗", &mut st.borders.diag_up), ] { + let n = lang.tr(n); ui.horizontal(|ui| { ui.label(format!("{n}:")); for (s, label) in [ @@ -1523,6 +1539,7 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: (BorderStyle::Dotted, "dotted"), (BorderStyle::Double, "double"), ] { + let label = lang.tr(label); let response = ui.selectable_label(slot.style == s, label); response .widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::SelectableLabel, ui.is_enabled(), format!("{n} {label}"))); @@ -1537,8 +1554,8 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: } "Fill" => { let colors = app.session.active().map(|doc| doc.wb.theme.colors).unwrap_or_default(); - ui.label("Background Color:"); - if let Some(c) = crate::widgets::color_palette(ui, &colors, "No Color") { + ui.label(lang.tr("Background Color:")); + if let Some(c) = crate::widgets::color_palette(ui, &colors, lang.tr("No Color")) { st.fill = if c == "none" { Default::default() } else { @@ -1547,11 +1564,13 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: } } "Protection" => { - ui.checkbox(&mut st.protection.locked, "Locked"); - ui.checkbox(&mut st.protection.hidden, "Hidden"); + ui.checkbox(&mut st.protection.locked, lang.tr("Locked")); + ui.checkbox(&mut st.protection.hidden, lang.tr("Hidden")); ui.label( - egui::RichText::new("Locking cells or hiding formulas has no effect until you protect the worksheet (Review › Protect Sheet).") - .small(), + egui::RichText::new( + lang.tr("Locking cells or hiding formulas has no effect until you protect the worksheet (Review › Protect Sheet)."), + ) + .small(), ); } _ => {} diff --git a/crates/ui-egui/tests/interaction.rs b/crates/ui-egui/tests/interaction.rs index 7093933..8e8eaaa 100644 --- a/crates/ui-egui/tests/interaction.rs +++ b/crates/ui-egui/tests/interaction.rs @@ -279,6 +279,25 @@ fn language_switch_translates_the_ribbon_and_persists() { assert_eq!(h.state().ui.language, Language::En); } +#[test] +fn format_cells_draws_its_labels_in_the_interface_language() { + let mut h = harness(blank()); + h.state_mut().run("app.language.set", json!({"language": "de"})).unwrap(); + h.state_mut().open_dialog("formatCells", json!({"tab": "Number"})); + h.run_steps(2); + assert!(h.query_by_label("Wissenschaftlich").is_some(), "the Number tab's categories are translated"); + assert!(h.query_by_label("Scientific").is_none(), "no English category is left"); + h.get_by_label("Wissenschaftlich").click(); + h.run_steps(2); + let style = &h.state().dialog.as_ref().unwrap().values["style"]; + assert_eq!(style["num_fmt"], json!("0.00E+00"), "a translated category still applies its code"); + h.state_mut().dialog = None; + h.state_mut().run("app.language.set", json!({"language": "en"})).unwrap(); + h.state_mut().open_dialog("formatCells", json!({"tab": "Number"})); + h.run_steps(2); + assert!(h.query_by_label("Scientific").is_some(), "English is unchanged"); +} + #[test] fn name_manager_new_and_edit_open_the_name_dialog_and_come_back() { // #238: New… in the Name Manager did nothing (the manager put itself back over the dialog it diff --git a/docs/localization-parity.md b/docs/localization-parity.md index e7d1de9..6f05a60 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (German ribbon catalog added) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Format Cells dialog looks its labels up in the catalog) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Per-language status against Excel, which ships its UI in dozens of languages (Windows language packs cover 100+), with localized function names, argument separators, number and date formats, @@ -14,7 +14,8 @@ right-to-left sheets and full complex-script support. Part of system language (closes #20) and a View-tab switcher, with English-keyed lookup tables: Japanese 268 labels, Simplified Chinese, Korean, Russian, Brazilian Portuguese, German and Czech 266 each. The tables are used by - the ribbon, sheet tabs and widgets (`ribbon.rs`, `tabs.rs`, `widgets.rs`); dialogs, menus, + the ribbon, sheet tabs and widgets (`ribbon.rs`, `tabs.rs`, `widgets.rs`), the Format Cells + dialog, every dialog title and the shared OK/Cancel buttons; the other dialogs, menus, context menus, task panes, status messages, command labels and function descriptions are still English literals. Untranslated labels fall back to English. - **About 2,200 user-visible strings** in all (heuristic source scan: ~1,700 UI and command @@ -77,6 +78,7 @@ language (none so far) and of localized function names. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | Format Cells looks up its tabs, labels, options and note through `Language::tr` (Fill tab keyed `Format Cells\|Fill`); dialog titles and OK/Cancel too. Labels already in a table (Number, Currency, Fill…) show translated; the rest fall back to English until the tables add them | | 2026-10-11 | minor | German ribbon catalog (`i18n/de.rs`, 266 labels, ~12%); `de` locale tags (de-DE, de-AT, de-CH…) resolve to German. PR #163 (localized German formulas/numbers) still open | | 2026-10-11 | minor | Sheet Right-to-Left mirrors the grid (#146) | | 2026-10-11 | minor | Czech (cs) ribbon table: 266 labels (~12%), menus only |