mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-28 20:56:54 -06:00
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/<lang>_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.
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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::<Option<Vec<_>>>() {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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])");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"),
|
||||
];
|
||||
@@ -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();
|
||||
|
||||
@@ -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/<lang>_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 |
|
||||
|
||||
Reference in New Issue
Block a user