From 47268a42af5a62c06a83dfdd7ba0a366bb7e9f63 Mon Sep 17 00:00:00 2001 From: quantmind-br Date: Thu, 15 Oct 2026 10:49:13 -0300 Subject: [PATCH 1/2] UI: function help follows the interface language The argument hint, autocomplete, Insert Function and the Formulas-tab menus read per-language tables of function descriptions and argument names (i18n/_functions.rs), keyed by the catalog's English text and binary-searched. The hint is assembled from the formula syntax's function name and separator plus the language's argument names, all of them or none, so it never mixes two languages. Insert Function searches the translated descriptions and shows readable category names. SPANISH_HELP moves to es_functions.rs with its 10 functions. --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 37 ++++++--- crates/ui-egui/src/formula_bar.rs | 11 +-- crates/ui-egui/src/formula_locale.rs | 105 +++++++++++++++++------- crates/ui-egui/src/i18n.rs | 49 +++++++++++ crates/ui-egui/src/i18n/es_functions.rs | 35 ++++++++ crates/ui-egui/src/ribbon.rs | 2 +- docs/localization-parity.md | 13 ++- 8 files changed, 202 insertions(+), 51 deletions(-) create mode 100644 crates/ui-egui/src/i18n/es_functions.rs diff --git a/ROADMAP.md b/ROADMAP.md index 070552d07..426cae12f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-15 | Function help follows the interface language: argument hints, autocomplete, Insert Function (search and readable categories) and the Formulas-tab menus read per-language tables of descriptions and argument names; Spanish keeps its 10 functions in the new format | | 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) | | 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) | | 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) | diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fb899c702..093e88cd8 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1746,30 +1746,30 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: } fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) { + let language = app.ui.language; ui.horizontal(|ui| { - ui.label("Search:"); - let r = ui.add(egui::TextEdit::singleline(&mut d.search).desired_width(300.0).hint_text("e.g. lookup, average, date")); + ui.label(language.tr("Search:")); + let r = ui.add(egui::TextEdit::singleline(&mut d.search).desired_width(300.0).hint_text(language.tr("e.g. lookup, average, date"))); r.request_focus(); }); - let locale = app.ui.language.formula_locale(); + let locale = language.formula_locale(); let list = gridcraft_engine::cmd::formulas::function_list(); - let q = d.search.to_ascii_lowercase(); + // The names and descriptions searched are the ones shown, so in the interface language. + let q = d.search.to_lowercase(); let hits: Vec<&Json> = list .iter() .filter(|f| { q.is_empty() - || f["name"].as_str().is_some_and(|n| locale.function_name(n).to_ascii_lowercase().contains(&q)) - || f["description"].as_str().is_some_and(|n| n.to_ascii_lowercase().contains(&q)) + || f["name"].as_str().is_some_and(|n| locale.function_name(n).to_lowercase().contains(&q)) + || f["description"].as_str().is_some_and(|n| language.function_description(n).unwrap_or(n).to_lowercase().contains(&q)) }) .take(300) .collect(); egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| { for (i, f) in hits.iter().enumerate() { let name = locale.function_name(f["name"].as_str().unwrap_or("")); - let r = ui.add( - egui::Button::selectable(i == d.list_index, format!("{name:<18} {}", f["category"].as_str().unwrap_or(""))) - .min_size(vec2(480.0, 20.0)), - ); + let category = language.tr(function_category_label(f["category"].as_str().unwrap_or(""))); + let r = ui.add(egui::Button::selectable(i == d.list_index, format!("{name:<18} {category}")).min_size(vec2(480.0, 20.0))); if r.clicked() { d.list_index = i; } @@ -1780,9 +1780,10 @@ fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confir } }); if let Some(f) = hits.get(d.list_index) { + let name = f["name"].as_str().unwrap_or(""); ui.separator(); - ui.label(egui::RichText::new(crate::formula_locale::signature(f["name"].as_str().unwrap_or(""), locale).unwrap_or_default()).strong()); - ui.label(crate::formula_locale::description(f["name"].as_str().unwrap_or(""), locale).unwrap_or_default()); + ui.label(egui::RichText::new(crate::formula_locale::signature(name, locale, language).unwrap_or_default()).strong()); + ui.label(crate::formula_locale::description(name, locale, language).unwrap_or_default()); } let mut open = true; ok_cancel(ui, confirm, &mut open); @@ -1800,6 +1801,18 @@ fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confir } } +/// Excel's name for a function category, given the catalog's id (`MathTrig`). +fn function_category_label(id: &str) -> &str { + match id { + "DateTime" => "Date & Time", + "Lookup" => "Lookup & Reference", + "MathTrig" => "Math & Trig", + "AddIn" => "Add-in and Automation", + // Financial, Logical, Text, Statistical, Engineering, Database, Information, … + other => other, + } +} + fn find(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.horizontal(|ui| { for tab in ["Find", "Replace"] { diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 7f1ad87fc..bf2c873a5 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -351,6 +351,7 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: // Autocomplete list and argument hint under the editor. if mine && ui.is_rect_visible(resp.rect) && (!ed.autocomplete.is_empty() || ed.current_function().is_some()) { let below = resp.rect.left_bottom() + vec2(0.0, 4.0); + let language = app.ui.language; // Areas have their own clip rect: keep cell helpers inside the worksheet. let bounds = if ed.from_formula_bar { ui.ctx().content_rect() } else { ui.clip_rect() }; egui::Area::new(id.with("ac")).fixed_pos(below).constrain_to(bounds).order(egui::Order::Tooltip).show(ui.ctx(), |ui| { @@ -366,11 +367,11 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: if r.clicked() { pick = Some(i); } - if sel && let Some(desc) = crate::formula_locale::description(n, ed.locale) { + if sel && let Some(desc) = crate::formula_locale::description(n, ed.locale, language) { r.on_hover_text(desc); } } - if let Some(desc) = ed.autocomplete.get(ed.ac_index).and_then(|n| crate::formula_locale::description(n, ed.locale)) { + if let Some(desc) = ed.autocomplete.get(ed.ac_index).and_then(|n| crate::formula_locale::description(n, ed.locale, language)) { ui.separator(); ui.add(egui::Label::new(egui::RichText::new(desc).small()).wrap()); } @@ -380,12 +381,12 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: ed.request_focus = true; } } else if let Some((name, arg)) = ed.current_function() - && let Some(sig) = crate::formula_locale::signature(&name, ed.locale) + && let Some(sig) = crate::formula_locale::signature(&name, ed.locale, language) { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; let (head, rest) = sig.split_once('(').unwrap_or((&sig, "")); - ui.label(egui::RichText::new(format!("{}(", ed.locale.function_name(head))).font(theme::ui_font(12.0))); + ui.label(egui::RichText::new(format!("{head}(")).font(theme::ui_font(12.0))); let inner = rest.trim_end_matches(')'); let parts: Vec<&str> = inner.split(&format!("{} ", ed.locale.list_separator())).collect(); for (i, part) in parts.iter().enumerate() { @@ -400,7 +401,7 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font: }); // The description too, so values an argument takes (DATEDIF's units, #363) show // while it's typed; wrapped to the hint's width. - if let Some(desc) = crate::formula_locale::description(&name, ed.locale) { + if let Some(desc) = crate::formula_locale::description(&name, ed.locale, language) { ui.set_max_width(ui.min_rect().width().max(220.0)); ui.separator(); ui.add(egui::Label::new(egui::RichText::new(desc).small()).wrap()); diff --git a/crates/ui-egui/src/formula_locale.rs b/crates/ui-egui/src/formula_locale.rs index 6e833b95d..11b551b10 100644 --- a/crates/ui-egui/src/formula_locale.rs +++ b/crates/ui-egui/src/formula_locale.rs @@ -1,4 +1,5 @@ //! UI formula syntax boundary. Engine commands, control/API callers and XLSX keep English. +use crate::i18n::Language; use gridcraft_engine::formula::{self, FormulaLocale, ParseError}; use gridcraft_engine::model::Workbook; @@ -86,39 +87,55 @@ fn hide_udf_prefix(text: &str, locale: FormulaLocale) -> String { out } -// Descriptions/signatures for the supported Spanish function table; untranslated functions -// retain their English help. Parameter punctuation follows the same locale as entry. -const SPANISH_HELP: &[(&str, &str, &str)] = &[ - ("SUM", "SUMA(número1; [número2]; ...)", "Suma los números."), - ("AVERAGE", "PROMEDIO(número1; [número2]; ...)", "Devuelve el promedio de los números."), - ("IF", "SI(prueba_lógica; valor_si_verdadero; [valor_si_falso])", "Devuelve un valor según una condición."), - ("COUNT", "CONTAR(valor1; [valor2]; ...)", "Cuenta las celdas que contienen números."), - ("SUMIF", "SUMAR.SI(rango; criterio; [rango_suma])", "Suma las celdas que cumplen un criterio."), - ("COUNTIF", "CONTAR.SI(rango; criterio)", "Cuenta las celdas que cumplen un criterio."), - ("VLOOKUP", "BUSCARV(valor_buscado; matriz; indicador_columnas; [ordenado])", "Busca un valor en la primera columna de una tabla."), - ("TODAY", "HOY()", "Devuelve la fecha actual."), - ("TRUE", "VERDADERO()", "Devuelve el valor lógico VERDADERO."), - ("FALSE", "FALSO()", "Devuelve el valor lógico FALSO."), -]; - -pub(crate) fn signature(name: &str, locale: FormulaLocale) -> Option { - let canonical = locale.canonical_name(name); - if locale == FormulaLocale::Es - && let Some((_, signature, _)) = SPANISH_HELP.iter().find(|(name, ..)| *name == canonical) - { - return Some((*signature).into()); - } - crate::formula_bar::function_signature(canonical).map(|s| s.replace(',', &locale.list_separator().to_string())) +/// A function's argument hint. The name and separators follow the formula syntax; the argument +/// names follow the interface language, all of them or none, so a hint never mixes two languages. +pub(crate) fn signature(name: &str, locale: FormulaLocale, language: Language) -> Option { + let english = crate::formula_bar::function_signature(locale.canonical_name(name))?; + let Some((head, args)) = split_signature(&english) else { return Some(english) }; + let separator = format!("{} ", locale.list_separator()); + let args = match args.iter().map(|arg| translate_argument(arg, language)).collect::>>() { + Some(translated) => translated.join(&separator), + None => args.join(&separator), + }; + Some(format!("{}({args})", locale.function_name(head))) } -pub(crate) fn description(name: &str, locale: FormulaLocale) -> Option { - let canonical = locale.canonical_name(name); - if locale == FormulaLocale::Es - && let Some((_, _, description)) = SPANISH_HELP.iter().find(|(name, ..)| *name == canonical) - { - return Some((*description).into()); +/// A function's description in the interface language (English while untranslated). +pub(crate) fn description(name: &str, locale: FormulaLocale, language: Language) -> Option { + let english = crate::formula_bar::function_description(locale.canonical_name(name))?; + Some(language.function_description(&english).map_or(english, str::to_string)) +} + +/// `SUM(number1, [number2], ...)` -> `SUM` and its arguments as written. +fn split_signature(signature: &str) -> Option<(&str, Vec<&str>)> { + let (head, rest) = signature.split_once('(')?; + let inner = rest.strip_suffix(')')?; + Some((head, if inner.is_empty() { Vec::new() } else { inner.split(", ").collect() })) +} + +/// The catalog names of a signature's arguments (`number` for `[number2]`), without `...`; the +/// function help tables are checked against them. +#[cfg(test)] +pub(crate) fn argument_names(signature: &str) -> Vec<&str> { + split_signature(signature).map(|(_, args)| args.into_iter().map(argument_name).filter(|name| *name != "...").collect()).unwrap_or_default() +} + +/// `[number2]` -> `number`. A bracket can open or close on its own (SWITCH's +/// `[default_or_value2, result2]`, LAMBDA's `[parameter1, ...]`). +fn argument_name(arg: &str) -> &str { + let arg = arg.strip_prefix('[').unwrap_or(arg); + let arg = arg.strip_suffix(']').unwrap_or(arg); + arg.trim_end_matches(|c: char| c.is_ascii_digit()) +} + +/// `[number2]` -> `[número2]`: brackets, index and `...` stay. +fn translate_argument(arg: &str, language: Language) -> Option { + let name = argument_name(arg); + if name == "..." { + return Some(arg.to_string()); } - crate::formula_bar::function_description(canonical) + // `arg` is `[`? + `name` + index + `]`?, so the first match is the name itself. + Some(arg.replacen(name, language.function_argument(name)?, 1)) } /// Avoid suggesting an alias that would call a workbook/table name instead of the function. @@ -138,3 +155,31 @@ pub(crate) fn display_template(text: &str, locale: FormulaLocale, wb: &Workbook, } display(text, locale, wb, sheet) } + +#[cfg(test)] +mod tests { + use super::*; + + fn sig(name: &str, locale: FormulaLocale, language: Language) -> String { + signature(name, locale, language).unwrap_or_default() + } + + #[test] + fn function_help_follows_the_interface_language_and_the_formula_syntax() { + use FormulaLocale::{En, Es}; + // Spanish help over English formulas: the name and `,` stay as typed; brackets, indexes + // and `...` survive. + assert_eq!(sig("IF", En, Language::Es), "IF(prueba_lógica, [valor_si_verdadero], [valor_si_falso])"); + assert_eq!(sig("SUM", En, Language::Es), "SUM(número1, [número2], ...)"); + assert_eq!(description("IF", En, Language::Es).as_deref(), Some("Devuelve un valor según una condición.")); + // Spanish names and separators, from a localized name. + assert_eq!(sig("suma", Es, Language::Es), "SUMA(número1; [número2]; ...)"); + assert_eq!(description("SUMA", Es, Language::Es).as_deref(), Some("Suma los números.")); + // An argument list Spanish covers only in part stays English instead of mixing (the name + // is whatever the Spanish formula table calls SUMIFS). + let sumifs = sig("SUMIFS", Es, Language::Es); + assert!(sumifs.ends_with("(sum_range; criteria_range1; criteria1; ...)"), "{sumifs}"); + assert_eq!(description("SUMIFS", Es, Language::Es).as_deref(), Some("Adds the cells that meet every condition.")); + assert_eq!(sig("IF", En, Language::En), "IF(logical_test, [value_if_true], [value_if_false])"); + } +} diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 061698df9..76a98a209 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; mod cs; mod de; mod es; +mod es_functions; mod fr; mod ko; mod pt; @@ -132,6 +133,34 @@ impl Language { }; translated.unwrap_or_else(|| text.rsplit_once('|').map_or(text, |(_, label)| label)) } + + /// A worksheet function's description in this language, looked up by the English text of the + /// function catalog; `None` when untranslated, and the help shows the English. + pub fn function_description(self, english: &str) -> Option<&'static str> { + lookup(self.function_help()?.0, english) + } + + /// A function argument name in this language, given as the catalog spells it without + /// brackets or index (`number`, `logical_test`). + pub fn function_argument(self, english: &str) -> Option<&'static str> { + lookup(self.function_help()?.1, english) + } + + /// The function help tables (descriptions, argument names) of the languages that have them. + fn function_help(self) -> Option<(HelpTable, HelpTable)> { + match self { + Self::Es => Some((es_functions::DESCRIPTIONS, es_functions::ARGUMENTS)), + _ => None, + } + } +} + +/// English text and its translation, sorted by the English. +type HelpTable = &'static [(&'static str, &'static str)]; + +fn lookup(table: HelpTable, english: &str) -> Option<&'static str> { + let i = table.binary_search_by(|(key, _)| (*key).cmp(english)).ok()?; + table.get(i).map(|(_, translated)| *translated) } /// Deserializes a saved language leniently: an unknown or malformed value (a language removed in a @@ -594,6 +623,26 @@ const JAPANESE: &[(&str, &str)] = &[ mod tests { use super::*; + #[test] + fn function_help_is_sorted_and_keyed_by_current_catalog_text() { + // Lookups binary-search the tables. A key that no longer matches the catalog (an English + // description reworded, an argument renamed) would quietly show English again. + let catalog = gridcraft_engine::cmd::formulas::function_list(); + let descriptions: std::collections::HashSet<&str> = catalog.iter().filter_map(|f| f["description"].as_str()).collect(); + let arguments: std::collections::HashSet<&str> = + catalog.iter().filter_map(|f| f["signature"].as_str()).flat_map(crate::formula_locale::argument_names).collect(); + for language in Language::ALL { + let Some((description_table, argument_table)) = language.function_help() else { continue }; + for (table, catalog_text) in [(description_table, &descriptions), (argument_table, &arguments)] { + assert!(table.windows(2).all(|pair| pair[0].0 < pair[1].0), "{language:?} function help is not sorted and unique"); + for (english, translated) in table { + assert!(catalog_text.contains(english), "{language:?} translates {english:?}, which the function catalog doesn't use"); + assert!(!translated.is_empty(), "{language:?}: empty translation of {english:?}"); + } + } + } + } + #[test] fn translations_are_unique_and_preserve_unknown_text() { for (i, (en, ja)) in JAPANESE.iter().enumerate() { diff --git a/crates/ui-egui/src/i18n/es_functions.rs b/crates/ui-egui/src/i18n/es_functions.rs new file mode 100644 index 000000000..da607ed5b --- /dev/null +++ b/crates/ui-egui/src/i18n/es_functions.rs @@ -0,0 +1,35 @@ +//! Spanish help for the worksheet functions. It covers the 10 common functions Spanish had help +//! for (SUM, AVERAGE, IF…); the others keep their English help. + +/// Function descriptions. +pub(super) const DESCRIPTIONS: &[(&str, &str)] = &[ + ("Adds all the numbers in the arguments.", "Suma los números."), + ("Adds the cells that meet a condition.", "Suma las celdas que cumplen un criterio."), + ("Arithmetic mean of the numbers.", "Devuelve el promedio de los números."), + ("Counts the cells that meet a condition.", "Cuenta las celdas que cumplen un criterio."), + ("Counts the numbers.", "Cuenta las celdas que contienen números."), + ( + "Finds a value in the first column of a table and returns the value in the same row of another column.", + "Busca un valor en la primera columna de una tabla.", + ), + ("Returns one value when a condition is TRUE and another when it is FALSE.", "Devuelve un valor según una condición."), + ("Returns the current date.", "Devuelve la fecha actual."), + ("Returns the logical value FALSE.", "Devuelve el valor lógico FALSO."), + ("Returns the logical value TRUE.", "Devuelve el valor lógico VERDADERO."), +]; + +/// Argument names without brackets or index (`number`, not `[number2]`). +pub(super) const ARGUMENTS: &[(&str, &str)] = &[ + ("col_index_num", "indicador_columnas"), + ("criteria", "criterio"), + ("logical_test", "prueba_lógica"), + ("lookup_value", "valor_buscado"), + ("number", "número"), + ("range", "rango"), + ("range_lookup", "ordenado"), + ("sum_range", "rango_suma"), + ("table_array", "matriz"), + ("value", "valor"), + ("value_if_false", "valor_si_falso"), + ("value_if_true", "valor_si_verdadero"), +]; diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 9c6d89eeb..07d307611 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1780,7 +1780,7 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { let locale = app.ui.language.formula_locale(); let name = app.session.active().map(|d| crate::formula_locale::completion_name(n, locale, &d.wb, d.wb.active_sheet)).unwrap_or(n); - let desc = crate::formula_locale::description(n, locale).unwrap_or_default(); + let desc = crate::formula_locale::description(n, locale, app.ui.language).unwrap_or_default(); if ui.button(name).on_hover_text(desc).clicked() { app.run_or_alert("formulas.insertFunction", json!({"name": n})); ui.close(); diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 09197ff6c..699ee1547 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-12 · **Change:** minor (Format Cells dialog looks its labels up in the catalog) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (function help follows the interface language) · **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, @@ -16,8 +16,14 @@ right-to-left sheets and full complex-script support. Part of each. The tables are used by 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. + context menus, task panes, status messages and command labels are still English literals. + Untranslated labels fall back to English. +- **Function help** (the argument hint, autocomplete, Insert Function and the Formulas-tab + menus) follows the interface language through per-language tables keyed by the catalog's + English text (`i18n/_functions.rs`): descriptions, and argument names from which the + hint is assembled with the formula syntax's function name and separator. Spanish translates + the 10 common functions it had help for (SUM, AVERAGE, IF…); other languages show the English + help. - **About 2,200 user-visible strings** in all (heuristic source scan: ~1,700 UI and command strings plus ~500 function descriptions), so each shipped language covers **~12%** (measured count / estimated total). @@ -78,6 +84,7 @@ language (none so far) and of localized function names. | Date | Change | Summary | |---|---|---| +| 2026-10-15 | minor | Function help in the interface language: argument hints, autocomplete, Insert Function (search, categories) and the Formulas-tab menus read per-language tables of descriptions and argument names keyed by the catalog's English text; Spanish keeps its 10 functions in the new format | | 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-12 | minor | Brazilian Portuguese catalog (`i18n/pt.rs`) grows from 377 to 923 entries: every key the other catalogs translate, including the dialog strings #669 starts looking up (~42%) | | 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 | From bc58b2773a2f50cc3b954e36b3146755b28a3698 Mon Sep 17 00:00:00 2001 From: quantmind-br Date: Tue, 20 Oct 2026 08:25:18 -0300 Subject: [PATCH 2/2] Document the functions this PR touches --- crates/ui-egui/src/dialogs.rs | 2 ++ crates/ui-egui/src/formula_locale.rs | 3 +++ crates/ui-egui/src/i18n.rs | 2 ++ crates/ui-egui/src/ribbon.rs | 2 ++ 4 files changed, 9 insertions(+) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 093e88cd8..d027df9fd 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1745,6 +1745,8 @@ fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: } } +/// Insert Function: searches names (as the formula language spells them) and descriptions, shows +/// each hit's category and description, and starts editing `=NAME(` (or appends to the formula). fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) { let language = app.ui.language; ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/formula_locale.rs b/crates/ui-egui/src/formula_locale.rs index 11b551b10..c34e3cf86 100644 --- a/crates/ui-egui/src/formula_locale.rs +++ b/crates/ui-egui/src/formula_locale.rs @@ -160,10 +160,13 @@ pub(crate) fn display_template(text: &str, locale: FormulaLocale, wb: &Workbook, mod tests { use super::*; + /// The hint for `name`, empty when there is none. fn sig(name: &str, locale: FormulaLocale, language: Language) -> String { signature(name, locale, language).unwrap_or_default() } + /// Hints keep the formula syntax's name and separators and translate the argument names + /// only when the interface language has them all. #[test] fn function_help_follows_the_interface_language_and_the_formula_syntax() { use FormulaLocale::{En, Es}; diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 016441ced..fcc59bf3d 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -158,6 +158,7 @@ impl Language { /// English text and its translation, sorted by the English. type HelpTable = &'static [(&'static str, &'static str)]; +/// The translation of `english` in a help table (binary search, so the table must stay sorted). fn lookup(table: HelpTable, english: &str) -> Option<&'static str> { let i = table.binary_search_by(|(key, _)| (*key).cmp(english)).ok()?; table.get(i).map(|(_, translated)| *translated) @@ -623,6 +624,7 @@ const JAPANESE: &[(&str, &str)] = &[ mod tests { use super::*; + /// Help tables are sorted, unique, and keyed by text the function catalog still has. #[test] fn function_help_is_sorted_and_keyed_by_current_catalog_text() { // Lookups binary-search the tables. A key that no longer matches the catalog (an English diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index b25ce4b2e..f0b74089a 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1749,6 +1749,8 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { } } +/// The Formulas tab: Insert Function, AutoSum, the function-category menus (names in the formula +/// language, descriptions in the interface language), defined names, auditing and calculation. fn formulas(app: &mut SheetApp, ui: &mut Ui) { let __k = big_button(ui, Icon::Function, "Insert\nFunction", "Insert Function (⇧F3)", false); control_keytip(app, ui, &__k, "dialog:insertFunction");