i18n: look up the Format Cells dialog labels in the catalog

The Format Cells dialog drew English literals in every language. Its
tabs, labels, options and protection note now go through Language::tr,
as do dialog titles and the shared OK/Cancel buttons. The Fill tab is
keyed Format Cells|Fill because the Alignment tab's Fill is a different
word in some languages. d.tab keeps the English name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michał Marcin Brzuchalski 2026-10-12 20:35:10 +02:00
parent fc45b7f6d8
commit 26ed66cfe1
No known key found for this signature in database
GPG Key ID: 1AFC0C9426BA6045
3 changed files with 78 additions and 38 deletions

View File

@ -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(),
);
}
_ => {}

View File

@ -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

View File

@ -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 |