mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-04 09:30:22 -06:00
Merge PR #1523: UI: function help follows the interface language
ROADMAP.md: kept both progress-log rows, newest first
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-14 | Protect Sheet on a protected sheet: Home › Format, its keytips (HOP, RPS) and the tab menu now read Unprotect Sheet… and unprotect (straight away without a password); a protected sheet that forbids selecting locked (or unlocked) cells is enforced for clicks, arrows and Go To, and the Protect Sheet dialog has the select options (#1346) |
|
||||
| 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) |
|
||||
|
||||
@@ -1749,31 +1749,33 @@ 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| {
|
||||
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;
|
||||
}
|
||||
@@ -1784,9 +1786,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);
|
||||
@@ -1804,6 +1807,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"] {
|
||||
|
||||
@@ -357,6 +357,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| {
|
||||
@@ -372,11 +373,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());
|
||||
}
|
||||
@@ -386,12 +387,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() {
|
||||
@@ -406,7 +407,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,34 @@ pub(crate) fn display_template(text: &str, locale: FormulaLocale, wb: &Workbook,
|
||||
}
|
||||
display(text, locale, wb, sheet)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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};
|
||||
// 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,35 @@ 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)];
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Deserializes a saved language leniently: an unknown or malformed value (a language removed in a
|
||||
@@ -594,6 +624,27 @@ 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
|
||||
// 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"),
|
||||
];
|
||||
@@ -1751,6 +1751,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");
|
||||
@@ -1784,7 +1786,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-19 · **Change:** minor (defined-name length limit counts characters, so long Arabic names are accepted; Table names are limited to 255 characters; right-to-left sheets in `.xls` files open right-to-left; text boxes and shapes keep right-to-left paragraph direction through XLSX save; Save as Web Page and copied HTML keep right-to-left; French and Serbian `context|label` translations no longer show the context) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (function help follows the interface language; defined-name length limit counts characters, so long Arabic names are accepted; Table names are limited to 255 characters; right-to-left sheets in `.xls` files open right-to-left; text boxes and shapes keep right-to-left paragraph direction through XLSX save; Save as Web Page and copied HTML keep right-to-left; French and Serbian `context|label` translations no longer show the context) · **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).
|
||||
@@ -89,6 +95,7 @@ language (none so far) and of localized function names.
|
||||
| 2026-10-19 | trivial | Text boxes and shapes keep right-to-left paragraph direction (`a:pPr rtl`) on XLSX save, so Excel-authored Arabic and Hebrew text boxes reopen right to left |
|
||||
| 2026-10-19 | minor | Save as Web Page and copied HTML mark right-to-left sheets `dir="rtl"`, let General-aligned Arabic and Hebrew text align by its own direction (`dir="auto"`), and keep numbers on right-to-left sheets left to right and on the left |
|
||||
| 2026-10-19 | minor | French and Serbian kept the context in three `context\|label` translations ("Options\|Général", "Opcije\|Opšte" for `Options\|General`; also `Options\|Save`, `Sheet Options\|View`). `tr` shows a found translation as it is, so File › Options headings and the Sheet Options "View" checkboxes showed the prefix. Fixed; a test checks that no catalog shows a `\|` |
|
||||
| 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