Browse / github.com/storytold/gridcraft / patch
i18n: Turkish formula entry (function names, separators)
i18n: Turkish formula entry (function names, separators) Adds FormulaLocale::Tr, picked by the Turkish interface as Spanish is: Excel's Turkish names for 439 functions (crates/formula/src/turkish.rs: TOPLA, EĞER, DÜŞEYARA, ETOPLA, İNDİS, KAÇINCI…), DOĞRU/YANLIŞ, `;` arguments, decimal comma, and `;` columns / `\` rows in array constants. - Names follow Turkish case rules (`indis` = İNDİS, `satır` = SATIR) in parsing, autocomplete, argument hints and Ctrl+Shift+A; the editor's word scan accepts non-ASCII letters. - English names stay accepted; functions Turkish Excel leaves in English (LET, LAMBDA, BYROW, MOD…) and those whose Turkish name could not
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- erolsavas19 (imported from pull request #2760, open)
- Base
89ac453efa7ea03a3ab0b0d24fd85ca51172f5cc- Size
- +716 −54 in 12 files
- Review
- pending
- Shared
- 2026-10-28
- Id
85d38d8f4b0670273d0b80e1
How it went
No other operator has reported using it yet.
Diff
raw · crates/formula/src/input.rs crates/formula/src/lexer.rs crates/formula/src/lib.rs crates/formula/src/parser.rs crates/formula/src/turkish.rs crates/formula/tests/it/main.rs crates/formula/tests/it/turkish_input.rs crates/ui-egui/src/editor.rs crates/ui-egui/src/i18n.rs crates/ui-egui/tests/it/formula_locale.rs docs/formula-input.md docs/localization-parity.md
@@ -9,6 +9,7 @@ pub enum FormulaLocale { #[default] En, Es,+ Tr, } const SPANISH: &[(&str, &str)] = &[@@ -32,36 +33,60 @@ const SPANISH: &[(&str, &str)] = &[ impl FormulaLocale { pub fn list_separator(self) -> char {- if self == Self::Es { ';' } else { ',' }+ if self == Self::En { ',' } else { ';' } } pub fn decimal_separator(self) -> char {- if self == Self::Es { ',' } else { '.' }+ if self == Self::En { '.' } else { ',' } } pub fn array_column_separator(self) -> char {- if self == Self::Es { '\\' } else { ',' }+ match self {+ Self::En => ',',+ Self::Es => '\\',+ Self::Tr => ';',+ } }- pub fn function_name(self, canonical: &str) -> &str {- if self == Self::Es {- SPANISH.iter().find(|(en, _)| en.eq_ignore_ascii_case(canonical)).map(|(_, es)| *es).unwrap_or(canonical)- } else {- canonical+ /// Between the rows of an array constant: `{1;2\3;4}` in Turkish Excel.+ pub fn array_row_separator(self) -> char {+ if self == Self::Tr { '\\' } else { ';' }+ }+ fn table(self) -> &'static [(&'static str, &'static str)] {+ match self {+ Self::En => &[],+ Self::Es => SPANISH,+ Self::Tr => crate::turkish::TURKISH, } }- pub fn canonical_name(self, localized: &str) -> &str {- if self == Self::Es {- SPANISH.iter().find(|(_, es)| es.eq_ignore_ascii_case(localized)).map(|(en, _)| *en).unwrap_or(localized)+ /// Upper case as the locale's users type it: Turkish `i` is `İ` and `ı` is `I` (`eğer` → `EĞER`).+ pub fn upper(self, text: &str) -> String {+ if self == Self::Tr {+ text.chars()+ .flat_map(|c| match c {+ 'i' => ['İ'].into_iter().flat_map(char::to_uppercase),+ 'ı' => ['I'].into_iter().flat_map(char::to_uppercase),+ c => [c].into_iter().flat_map(char::to_uppercase),+ })+ .collect() } else {- localized+ text.to_uppercase() } }+ fn same_alias(self, a: &str, b: &str) -> bool {+ if a.is_ascii() && b.is_ascii() { a.eq_ignore_ascii_case(b) } else { self.upper(a) == self.upper(b) }+ }+ pub fn function_name(self, canonical: &str) -> &str {+ self.table().iter().find(|(en, _)| en.eq_ignore_ascii_case(canonical)).map(|(_, local)| *local).unwrap_or(canonical)+ }+ pub fn canonical_name(self, localized: &str) -> &str {+ self.table().iter().find(|(_, local)| self.same_alias(local, localized)).map(|(en, _)| *en).unwrap_or(localized)+ } } /// Parses UI input using the selected function table and separators. Existing names and /// LET/LAMBDA bindings take precedence over aliases. File and API syntax uses [`crate::parse`]. pub fn parse_input(src: &str, locale: FormulaLocale, known_name: impl Fn(&str) -> bool) -> Result<Expr, ParseError> { let mut expr = parser::parse_syntax(src, locale)?;- if locale == FormulaLocale::Es {- normalize(&mut expr, &known_name, &mut Vec::new(), 0, false)?;+ if locale != FormulaLocale::En {+ normalize(&mut expr, locale, &known_name, &mut Vec::new(), 0, false)?; } Ok(expr) }@@ -96,7 +121,7 @@ pub fn print_input(expr: &Expr, locale: FormulaLocale, known_name: impl Fn(&str) return Ok(crate::print(expr)); } let mut expr = expr.clone();- normalize(&mut expr, &known_name, &mut Vec::new(), 0, true)?;+ normalize(&mut expr, locale, &known_name, &mut Vec::new(), 0, true)?; let canonical = crate::print(&expr); let tokens = crate::lexer::tokenize(&canonical).map_err(|e| ParseError { msg: e.msg, pos: e.pos })?; let mut out = String::new();@@ -115,36 +140,47 @@ pub fn print_input(expr: &Expr, locale: FormulaLocale, known_name: impl Fn(&str) } Tok::Number(_) => out.push_str(&text.replace('.', &locale.decimal_separator().to_string())), Tok::Comma => out.push(if array_depth > 0 { locale.array_column_separator() } else { locale.list_separator() }),+ Tok::Semicolon if array_depth > 0 => out.push(locale.array_row_separator()), _ => out.push_str(text), } } Ok(out) } -pub(crate) fn boolean_alias(name: &str) -> Option<bool> {- if name.eq_ignore_ascii_case("VERDADERO") {+/// `VERDADERO` / `DOĞRU`… typed as a value: the locale's names for TRUE and FALSE.+pub(crate) fn boolean_alias(locale: FormulaLocale, name: &str) -> Option<bool> {+ if locale == FormulaLocale::En {+ None+ } else if locale.same_alias(name, locale.function_name("TRUE")) { Some(true)- } else if name.eq_ignore_ascii_case("FALSO") {+ } else if locale.same_alias(name, locale.function_name("FALSE")) { Some(false) } else { None } } -fn normalize(expr: &mut Expr, known_name: &impl Fn(&str) -> bool, bindings: &mut Vec<String>, depth: usize, display: bool) -> Result<(), ParseError> {+fn normalize(+ expr: &mut Expr,+ locale: FormulaLocale,+ known_name: &impl Fn(&str) -> bool,+ bindings: &mut Vec<String>,+ depth: usize,+ display: bool,+) -> Result<(), ParseError> { if depth > parser::MAX_DEPTH { return Err(ParseError { msg: "formula is nested too deeply".into(), pos: 0 }); } let bound = |name: &str, bindings: &[String]| bindings.iter().rev().any(|n| crate::same_name(n, name)) || known_name(name); match expr { Expr::Bool(value) if display => {- let alias = if *value { "VERDADERO" } else { "FALSO" };+ let alias = locale.function_name(if *value { "TRUE" } else { "FALSE" }); if !bound(alias, bindings) { *expr = Expr::Name(alias.into()); } } Expr::Name(name) if !display && !bound(name, bindings) => {- if let Some(value) = boolean_alias(name) {+ if let Some(value) = boolean_alias(locale, name) { *expr = Expr::Bool(value); } }@@ -154,13 +190,13 @@ fn normalize(expr: &mut Expr, known_name: &impl Fn(&str) -> bool, bindings: &mut for pair in pairs.chunks_mut(2) { if let [declaration, value] = pair { // The new name is not in scope in its own value expression.- normalize(value, known_name, bindings, depth + 1, display)?;+ normalize(value, locale, known_name, bindings, depth + 1, display)?; if let Expr::Name(name) = declaration { bindings.push(name.clone()); } } }- normalize(body, known_name, bindings, depth + 1, display)?;+ normalize(body, locale, known_name, bindings, depth + 1, display)?; } bindings.truncate(saved); }@@ -172,40 +208,44 @@ fn normalize(expr: &mut Expr, known_name: &impl Fn(&str) -> bool, bindings: &mut bindings.push(name.clone()); } }- normalize(body, known_name, bindings, depth + 1, display)?;+ normalize(body, locale, known_name, bindings, depth + 1, display)?; } bindings.truncate(saved); } Expr::Call(name, args) => {- let alias = if display { FormulaLocale::Es.function_name(name) } else { FormulaLocale::Es.canonical_name(name) };+ let alias = if display { locale.function_name(name) } else { locale.canonical_name(name) }; if !bound(name, bindings) && (!display || !bound(alias, bindings)) { *name = alias.to_string(); }+ if !display {+ // Not a localized name (an English or user-defined one): upper case as in English.+ *name = parser::normalize_function_name(name);+ } for arg in args {- normalize(arg, known_name, bindings, depth + 1, display)?;+ normalize(arg, locale, known_name, bindings, depth + 1, display)?; } } Expr::Invoke(callee, args) => {- normalize(callee, known_name, bindings, depth + 1, display)?;+ normalize(callee, locale, known_name, bindings, depth + 1, display)?; for arg in args {- normalize(arg, known_name, bindings, depth + 1, display)?;+ normalize(arg, locale, known_name, bindings, depth + 1, display)?; } }- Expr::Unary(_, child) | Expr::Paren(child) => normalize(child, known_name, bindings, depth + 1, display)?,+ Expr::Unary(_, child) | Expr::Paren(child) => normalize(child, locale, known_name, bindings, depth + 1, display)?, Expr::Binary(..) => { // Follow the left spine of a flat chain (A1+A2+…) in a loop: it is not nesting. // Each call leaves `bindings` as it found them, so the order does not matter. let mut node = expr; while let Expr::Binary(_, left, right) = node {- normalize(right, known_name, bindings, depth + 1, display)?;+ normalize(right, locale, known_name, bindings, depth + 1, display)?; node = left; }- normalize(node, known_name, bindings, depth, display)?;+ normalize(node, locale, known_name, bindings, depth, display)?; } Expr::Array(rows) if display => { for row in rows { for child in row {- normalize(child, known_name, bindings, depth + 1, display)?;+ normalize(child, locale, known_name, bindings, depth + 1, display)?; } } }@@ -49,6 +49,12 @@ pub struct LexError { pub pos: usize, } +/// A separator inside an array constant: `\` separates columns in Spanish and rows in Turkish, and+/// would otherwise be read as part of a name.+fn is_array_separator(c: char, locale: crate::FormulaLocale) -> bool {+ c == locale.array_column_separator() || c == locale.array_row_separator()+}+ fn is_word_char(c: char) -> bool { c.is_alphanumeric() || matches!(c, '_' | '.' | '$' | '\\' | '?') }@@ -288,6 +294,10 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To push(&mut out, Tok::Comma, i + 1); i += 1; }+ c if array_depth > 0 && c == locale.array_row_separator() => {+ push(&mut out, Tok::Semicolon, i + 1);+ i += 1;+ } ';' => { push(&mut out, Tok::Semicolon, i + 1); i += 1;@@ -330,7 +340,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To } c if is_word_char(c) => { let mut j = i;- while ch(j).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {+ while ch(j).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && is_array_separator(c, locale))) { j += 1; } let w: String = chars.get(i..j).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();@@ -340,7 +350,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To } else if ch(j) == Some(':') && { // Sheet1:Sheet3!A1 — look ahead for word then '!'. let mut k = j + 1;- while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {+ while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && is_array_separator(c, locale))) { k += 1; } // `Sheet1!A1:Sheet1!B2` is a range whose second end repeats the sheet, not a span.@@ -350,7 +360,7 @@ pub fn tokenize_locale(src: &str, locale: crate::FormulaLocale) -> Result<Vec<To k > j + 1 && ch(k) == Some('!') && !repeats_sheet } { let mut k = j + 1;- while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && c == locale.array_column_separator())) {+ while ch(k).is_some_and(|c| is_word_char(c) && !(array_depth > 0 && is_array_separator(c, locale))) { k += 1; } let w2: String = chars.get(j + 1..k).map(|s| s.iter().map(|c| c.1).collect()).unwrap_or_default();@@ -9,6 +9,7 @@ mod input; pub mod lexer; pub mod parser; pub mod printer;+mod turkish; pub use ast::{Anchor, BinOp, Expr, RefKind, Reference, SheetSel, StructItem, StructRef, TrimMode, UnOp}; pub use input::{FormulaLocale, parse_input, print_input};@@ -66,7 +66,7 @@ fn parse_top(src: &str, locale: crate::FormulaLocale, allow_union: bool) -> Resu } let toks = tokenize_locale(body, locale).map_err(|e| ParseError { msg: e.msg, pos: e.pos })?; let toks = significant_spaces(toks);- let mut p = Parser { toks, i: 0, depth: 0, input: locale == crate::FormulaLocale::Es };+ let mut p = Parser { toks, i: 0, depth: 0, locale }; let e = p.expr(0, allow_union)?; match p.peek() { Tok::Eof => Ok(e),@@ -106,7 +106,8 @@ struct Parser { toks: Vec<Token>, i: usize, depth: usize,- input: bool,+ /// Entry syntax: any locale but English also accepts its TRUE/FALSE names in array constants.+ locale: crate::FormulaLocale, } fn word_is_cols(w: &str) -> Option<(u32, bool)> {@@ -293,10 +294,13 @@ impl Parser { } Tok::LBrace => self.array(), Tok::Func(name) => {- let name = normalize_function_name(&name);+ let upper = normalize_function_name(&name);+ // Turkish entry keeps the name as typed (`indis` is `İNDİS`, not `INDIS`); the+ // alias pass upper-cases it by the locale's rules.+ let name = if self.locale == crate::FormulaLocale::Tr && upper.eq_ignore_ascii_case(&name) { name } else { upper }; let mut args = self.args()?; // Files write `A1#` as `_xlfn.ANCHORARRAY(A1)`.- if name == "ANCHORARRAY"+ if name.eq_ignore_ascii_case("ANCHORARRAY") && let [Expr::Ref(Reference { kind: RefKind::Cell(_), .. })] = args.as_slice() && let Some(r) = args.pop() {@@ -434,7 +438,7 @@ impl Parser { }, Tok::Text(s) => Expr::Text(Arc::from(s)), Tok::Bool(b) => Expr::Bool(b),- Tok::Word(w) if self.input => crate::input::boolean_alias(&w)+ Tok::Word(w) if self.locale != crate::FormulaLocale::En => crate::input::boolean_alias(self.locale, &w) .map(Expr::Bool) .ok_or_else(|| self.err(format!("array constants may only contain constants, found {}", describe(&Tok::Word(w)))))?, Tok::Error(e) => Expr::Error(e),new file mode 100644@@ -0,0 +1,473 @@+//! Turkish function names for formula entry (Microsoft Excel's Turkish UI), canonical English first.+//! Sources: the Turkish names Excel ships, as published by excel-translator.de and PerfectXL (they+//! agree on 465 of GridCraft's functions; where they differ the excel-translator.de name is used) and+//! Microsoft's Turkish function reference for the dynamic-array functions added since 2019.+//! Functions Excel leaves in English in Turkish (LET, LAMBDA, BYROW, MOD, RANK…) are not listed,+//! nor those whose Turkish name could not be confirmed (MAP, UNIQUE, GROUPBY, PIVOTBY, TRIMRANGE):+//! they keep their English names, which are always accepted.+//! IMSUM is SANÇIKAR and IMSUB is SANTOPLA in Turkish Excel; the swap is Excel's and is kept so that+//! formulas typed as in Excel mean the same here.++pub(crate) const TURKISH: &[(&str, &str)] = &[+ ("ABS", "MUTLAK"),+ ("ACCRINT", "GERÇEKFAİZ"),+ ("ACCRINTM", "GERÇEKFAİZV"),+ ("ADDRESS", "ADRES"),+ ("AGGREGATE", "TOPLAMA"),+ ("AND", "VE"),+ ("ARABIC", "ARAP"),+ ("AREAS", "ALANSAY"),+ ("ARRAYTOTEXT", "DİZİMETİN"),+ ("ASIN", "ASİN"),+ ("ASINH", "ASİNH"),+ ("AVEDEV", "ORTSAP"),+ ("AVERAGE", "ORTALAMA"),+ ("AVERAGEA", "ORTALAMAA"),+ ("AVERAGEIF", "EĞERORTALAMA"),+ ("AVERAGEIFS", "ÇOKEĞERORTALAMA"),+ ("BAHTTEXT", "BAHTMETİN"),+ ("BASE", "TABAN"),+ ("BETA.DIST", "BETA.DAĞ"),+ ("BETA.INV", "BETA.TERS"),+ ("BETADIST", "BETADAĞ"),+ ("BETAINV", "BETATERS"),+ ("BINOM.DIST", "BİNOM.DAĞ"),+ ("BINOM.DIST.RANGE", "BİNOM.DAĞ.ARALIK"),+ ("BINOM.INV", "BİNOM.TERS"),+ ("BINOMDIST", "BİNOMDAĞ"),+ ("BITAND", "BİTVE"),+ ("BITLSHIFT", "BİTSOLAKAYDIR"),+ ("BITOR", "BİTVEYA"),+ ("BITRSHIFT", "BİTSAĞAKAYDIR"),+ ("BITXOR", "BİTÖZELVEYA"),+ ("CEILING", "TAVANAYUVARLA"),+ ("CEILING.MATH", "TAVANAYUVARLA.MATEMATİK"),+ ("CEILING.PRECISE", "TAVANAYUVARLA.DUYARLI"),+ ("CELL", "HÜCRE"),+ ("CHAR", "DAMGA"),+ ("CHIDIST", "KİKAREDAĞ"),+ ("CHIINV", "KİKARETERS"),+ ("CHISQ.DIST", "KİKARE.DAĞ"),+ ("CHISQ.DIST.RT", "KİKARE.DAĞ.SAĞK"),+ ("CHISQ.INV", "KİKARE.TERS"),+ ("CHISQ.INV.RT", "KİKARE.TERS.SAĞK"),+ ("CHISQ.TEST", "KİKARE.TEST"),+ ("CHITEST", "KİKARETEST"),+ ("CHOOSE", "ELEMAN"),+ ("CHOOSECOLS", "SÜTUNSEÇ"),+ ("CHOOSEROWS", "SATIRSEÇ"),+ ("CLEAN", "TEMİZ"),+ ("CODE", "KOD"),+ ("COLUMN", "SÜTUN"),+ ("COLUMNS", "SÜTUNSAY"),+ ("COMBIN", "KOMBİNASYON"),+ ("COMBINA", "KOMBİNASYONA"),+ ("COMPLEX", "KARMAŞIK"),+ ("CONCAT", "ARALIKBİRLEŞTİR"),+ ("CONCATENATE", "BİRLEŞTİR"),+ ("CONFIDENCE", "GÜVENİRLİK"),+ ("CONFIDENCE.NORM", "GÜVENİLİRLİK.NORM"),+ ("CONFIDENCE.T", "GÜVENİLİRLİK.T"),+ ("CONVERT", "ÇEVİR"),+ ("CORREL", "KORELASYON"),+ ("COUNT", "BAĞ_DEĞ_SAY"),+ ("COUNTA", "BAĞ_DEĞ_DOLU_SAY"),+ ("COUNTBLANK", "BOŞLUKSAY"),+ ("COUNTIF", "EĞERSAY"),+ ("COUNTIFS", "ÇOKEĞERSAY"),+ ("COUPDAYBS", "KUPONGÜNBD"),+ ("COUPDAYS", "KUPONGÜN"),+ ("COUPDAYSNC", "KUPONGÜNDSK"),+ ("COUPNCD", "KUPONGÜNSKT"),+ ("COUPNUM", "KUPONSAYI"),+ ("COUPPCD", "KUPONGÜNÖKT"),+ ("COVAR", "KOVARYANS"),+ ("COVARIANCE.P", "KOVARYANS.P"),+ ("COVARIANCE.S", "KOVARYANS.S"),+ ("CRITBINOM", "KRİTİKBİNOM"),+ ("CUBEKPIMEMBER", "KÜPKPIÜYESİ"),+ ("CUBEMEMBER", "KÜPÜYESİ"),+ ("CUBEMEMBERPROPERTY", "KÜPÜYEÖZELLİĞİ"),+ ("CUBERANKEDMEMBER", "DERECELİKÜPÜYESİ"),+ ("CUBESET", "KÜPKÜMESİ"),+ ("CUBESETCOUNT", "KÜPKÜMESAYISI"),+ ("CUBEVALUE", "KÜPDEĞERİ"),+ ("CUMIPMT", "TOPÖDENENFAİZ"),+ ("CUMPRINC", "TOPANAPARA"),+ ("DATE", "TARİH"),+ ("DATEDIF", "ETARİHLİ"),+ ("DATEVALUE", "TARİHSAYISI"),+ ("DAVERAGE", "VSEÇORT"),+ ("DAY", "GÜN"),+ ("DAYS", "GÜNSAY"),+ ("DAYS360", "GÜN360"),+ ("DB", "AZALANBAKİYE"),+ ("DCOUNT", "VSEÇSAY"),+ ("DCOUNTA", "VSEÇSAYDOLU"),+ ("DDB", "ÇİFTAZALANBAKİYE"),+ ("DECIMAL", "ONDALIK"),+ ("DEGREES", "DERECE"),+ ("DEVSQ", "SAPKARE"),+ ("DGET", "VAL"),+ ("DISC", "İNDİRİM"),+ ("DMAX", "VSEÇMAK"),+ ("DMIN", "VSEÇMİN"),+ ("DOLLAR", "LİRA"),+ ("DOLLARDE", "LİRAON"),+ ("DOLLARFR", "LİRAKES"),+ ("DPRODUCT", "VSEÇÇARP"),+ ("DROP", "BIRAK"),+ ("DSTDEV", "VSEÇSTDSAPMA"),+ ("DSTDEVP", "VSEÇSTDSAPMAS"),+ ("DSUM", "VSEÇTOPLA"),+ ("DURATION", "SÜRE"),+ ("DVAR", "VSEÇVAR"),+ ("DVARP", "VSEÇVARS"),+ ("EDATE", "SERİTARİH"),+ ("EFFECT", "ETKİN"),+ ("ENCODEURL", "URLKODLA"),+ ("EOMONTH", "SERİAY"),+ ("ERF", "HATAİŞLEV"),+ ("ERF.PRECISE", "HATAİŞLEV.DUYARLI"),+ ("ERFC", "TÜMHATAİŞLEV"),+ ("ERFC.PRECISE", "TÜMHATAİŞLEV.DUYARLI"),+ ("ERROR.TYPE", "HATA.TİPİ"),+ ("EVEN", "ÇİFT"),+ ("EXACT", "ÖZDEŞ"),+ ("EXP", "ÜS"),+ ("EXPAND", "GENİŞLET"),+ ("EXPON.DIST", "ÜSTEL.DAĞ"),+ ("EXPONDIST", "ÜSTELDAĞ"),+ ("F.DIST", "F.DAĞ"),+ ("F.DIST.RT", "F.DAĞ.SAĞK"),+ ("F.INV", "F.TERS"),+ ("F.INV.RT", "F.TERS.SAĞK"),+ ("FACT", "ÇARPINIM"),+ ("FACTDOUBLE", "ÇİFTFAKTÖR"),+ ("FALSE", "YANLIŞ"),+ ("FDIST", "FDAĞ"),+ ("FILTER", "FİLTRE"),+ ("FILTERXML", "XMLFİLTRELE"),+ ("FIND", "BUL"),+ ("FINDB", "BULB"),+ ("FINV", "FTERS"),+ ("FISHERINV", "FISHERTERS"),+ ("FIXED", "SAYIDÜZENLE"),+ ("FLOOR", "TABANAYUVARLA"),+ ("FLOOR.MATH", "TABANAYUVARLA.MATEMATİK"),+ ("FLOOR.PRECISE", "TABANAYUVARLA.DUYARLI"),+ ("FORECAST", "TAHMİN"),+ ("FORECAST.ETS", "TAHMİN.ETS"),+ ("FORECAST.ETS.CONFINT", "TAHMİN.ETS.GVNARAL"),+ ("FORECAST.ETS.SEASONALITY", "TAHMİN.ETS.MEVSİMSELLİK"),+ ("FORECAST.ETS.STAT", "TAHMİN.ETS.İSTAT"),+ ("FORECAST.LINEAR", "TAHMİN.DOĞRUSAL"),+ ("FORMULATEXT", "FORMÜLMETNİ"),+ ("FREQUENCY", "SIKLIK"),+ ("FV", "GD"),+ ("FVSCHEDULE", "GDPROGRAM"),+ ("GAMMA", "GAMA"),+ ("GAMMA.DIST", "GAMA.DAĞ"),+ ("GAMMA.INV", "GAMA.TERS"),+ ("GAMMADIST", "GAMADAĞ"),+ ("GAMMAINV", "GAMATERS"),+ ("GAMMALN", "GAMALN"),+ ("GAMMALN.PRECISE", "GAMALN.DUYARLI"),+ ("GCD", "OBEB"),+ ("GEOMEAN", "GEOORT"),+ ("GESTEP", "BESINIR"),+ ("GROWTH", "BÜYÜME"),+ ("HARMEAN", "HARORT"),+ ("HLOOKUP", "YATAYARA"),+ ("HOUR", "SAAT"),+ ("HSTACK", "YATAYYIĞ"),+ ("HYPERLINK", "KÖPRÜ"),+ ("HYPGEOM.DIST", "HİPERGEOM.DAĞ"),+ ("HYPGEOMDIST", "HİPERGEOMDAĞ"),+ ("IF", "EĞER"),+ ("IFERROR", "EĞERHATA"),+ ("IFNA", "EĞERYOKSA"),+ ("IFS", "ÇOKEĞER"),+ ("IMABS", "SANMUTLAK"),+ ("IMAGINARY", "SANAL"),+ ("IMARGUMENT", "SANBAĞ_DEĞİŞKEN"),+ ("IMCONJUGATE", "SANEŞLENEK"),+ ("IMCOS", "SANCOS"),+ ("IMCOSH", "SANCOSH"),+ ("IMCOT", "SANCOT"),+ ("IMCSC", "SANCSC"),+ ("IMCSCH", "SANCSCH"),+ ("IMDIV", "SANBÖL"),+ ("IMEXP", "SANÜS"),+ ("IMLN", "SANLN"),+ ("IMLOG10", "SANLOG10"),+ ("IMLOG2", "SANLOG2"),+ ("IMPOWER", "SANKUVVET"),+ ("IMPRODUCT", "SANÇARP"),+ ("IMREAL", "SANGERÇEK"),+ ("IMSEC", "SANSEC"),+ ("IMSECH", "SANSECH"),+ ("IMSIN", "SANSIN"),+ ("IMSINH", "SANSINH"),+ ("IMSQRT", "SANKAREKÖK"),+ ("IMSUB", "SANTOPLA"),+ ("IMSUM", "SANÇIKAR"),+ ("IMTAN", "SANTAN"),+ ("INDEX", "İNDİS"),+ ("INDIRECT", "DOLAYLI"),+ ("INFO", "BİLGİ"),+ ("INT", "TAMSAYI"),+ ("INTERCEPT", "KESMENOKTASI"),+ ("INTRATE", "FAİZORANI"),+ ("IPMT", "FAİZTUTARI"),+ ("IRR", "İÇ_VERİM_ORANI"),+ ("ISBLANK", "EBOŞSA"),+ ("ISERR", "EHATA"),+ ("ISERROR", "EHATALIYSA"),+ ("ISEVEN", "ÇİFTMİ"),+ ("ISFORMULA", "EFORMÜLSE"),+ ("ISLOGICAL", "EMANTIKSALSA"),+ ("ISNA", "EYOKSA"),+ ("ISNONTEXT", "EMETİNDEĞİLSE"),+ ("ISNUMBER", "ESAYIYSA"),+ ("ISO.CEILING", "ISO.TAVAN"),+ ("ISODD", "TEKMİ"),+ ("ISOMITTED", "ATLANMIŞ"),+ ("ISOWEEKNUM", "ISOHAFTASAY"),+ ("ISREF", "EREFSE"),+ ("ISTEXT", "EMETİNSE"),+ ("KURT", "BASIKLIK"),+ ("LARGE", "BÜYÜK"),+ ("LCM", "OKEK"),+ ("LEFT", "SOLDAN"),+ ("LEFTB", "SOLB"),+ ("LEN", "UZUNLUK"),+ ("LENB", "UZUNLUKB"),+ ("LINEST", "DOT"),+ ("LOGEST", "LOT"),+ ("LOGINV", "LOGTERS"),+ ("LOGNORM.DIST", "LOGNORM.DAĞ"),+ ("LOGNORM.INV", "LOGNORM.TERS"),+ ("LOGNORMDIST", "LOGNORMDAĞ"),+ ("LOOKUP", "ARA"),+ ("LOWER", "KÜÇÜKHARF"),+ ("MAKEARRAY", "DİZİOLUŞTUR"),+ ("MATCH", "KAÇINCI"),+ ("MAX", "MAK"),+ ("MAXA", "MAKA"),+ ("MAXIFS", "ÇOKEĞERMAK"),+ ("MDETERM", "DETERMİNANT"),+ ("MDURATION", "MSÜRE"),+ ("MEDIAN", "ORTANCA"),+ ("MID", "PARÇAAL"),+ ("MIDB", "ORTAB"),+ ("MIN", "MİN"),+ ("MINA", "MİNA"),+ ("MINIFS", "ÇOKEĞERMİN"),+ ("MINUTE", "DAKİKA"),+ ("MINVERSE", "DİZEY_TERS"),+ ("MIRR", "D_İÇ_VERİM_ORANI"),+ ("MMULT", "DÇARP"),+ ("MODE", "ENÇOK_OLAN"),+ ("MODE.MULT", "ENÇOK_OLAN.ÇOK"),+ ("MODE.SNGL", "ENÇOK_OLAN.TEK"),+ ("MONTH", "AY"),+ ("MROUND", "KYUVARLA"),+ ("MULTINOMIAL", "ÇOKTERİMLİ"),+ ("MUNIT", "BİRİMMATRİS"),+ ("N", "S"),+ ("NA", "YOKSAY"),+ ("NEGBINOM.DIST", "NEGBİNOM.DAĞ"),+ ("NEGBINOMDIST", "NEGBİNOMDAĞ"),+ ("NETWORKDAYS", "TAMİŞGÜNÜ"),+ ("NETWORKDAYS.INTL", "TAMİŞGÜNÜ.ULUSL"),+ ("NOMINAL", "NOMİNAL"),+ ("NORM.DIST", "NORM.DAĞ"),+ ("NORM.INV", "NORM.TERS"),+ ("NORM.S.DIST", "NORM.S.DAĞ"),+ ("NORM.S.INV", "NORM.S.TERS"),+ ("NORMDIST", "NORMDAĞ"),+ ("NORMINV", "NORMTERS"),+ ("NORMSDIST", "NORMSDAĞ"),+ ("NORMSINV", "NORMSTERS"),+ ("NOT", "DEĞİL"),+ ("NOW", "ŞİMDİ"),+ ("NPER", "TAKSİT_SAYISI"),+ ("NPV", "NBD"),+ ("NUMBERVALUE", "SAYIDEĞERİ"),+ ("ODD", "TEK"),+ ("ODDFPRICE", "TEKYDEĞER"),+ ("ODDFYIELD", "TEKYÖDEME"),+ ("ODDLPRICE", "TEKSDEĞER"),+ ("ODDLYIELD", "TEKSÖDEME"),+ ("OFFSET", "KAYDIR"),+ ("OR", "YADA"),+ ("PDURATION", "PSÜRE"),+ ("PERCENTILE", "YÜZDEBİRLİK"),+ ("PERCENTILE.EXC", "YÜZDEBİRLİK.HRC"),+ ("PERCENTILE.INC", "YÜZDEBİRLİK.DHL"),+ ("PERCENTRANK", "YÜZDERANK"),+ ("PERCENTRANK.EXC", "YÜZDERANK.HRC"),+ ("PERCENTRANK.INC", "YÜZDERANK.DHL"),+ ("PERMUT", "PERMÜTASYON"),+ ("PERMUTATIONA", "PERMÜTASYONA"),+ ("PHONETIC", "SES"),+ ("PI", "Pİ"),+ ("PMT", "DEVRESEL_ÖDEME"),+ ("POISSON.DIST", "POISSON.DAĞ"),+ ("POWER", "KUVVET"),+ ("PPMT", "ANA_PARA_ÖDEMESİ"),+ ("PRICE", "DEĞER"),+ ("PRICEDISC", "DEĞERİND"),+ ("PRICEMAT", "DEĞERVADE"),+ ("PROB", "OLASILIK"),+ ("PRODUCT", "ÇARPIM"),+ ("PROPER", "YAZIM.DÜZENİ"),+ ("PV", "BD"),+ ("QUARTILE", "DÖRTTEBİRLİK"),+ ("QUARTILE.EXC", "DÖRTTEBİRLİK.HRC"),+ ("QUARTILE.INC", "DÖRTTEBİRLİK.DHL"),+ ("QUOTIENT", "BÖLÜM"),+ ("RADIANS", "RADYAN"),+ ("RAND", "S_SAYI_ÜRET"),+ ("RANDARRAY", "RASGDİZİ"),+ ("RANDBETWEEN", "RASTGELEARADA"),+ ("RANK.AVG", "RANK.ORT"),+ ("RANK.EQ", "RANK.EŞİT"),+ ("RATE", "FAİZ_ORANI"),+ ("RECEIVED", "GETİRİ"),+ ("REGEXEXTRACT", "NORMİFADEAYIKLA"),+ ("REGEXREPLACE", "NORMİFADEDEĞİŞTİR"),+ ("REGEXTEST", "NORMİFADESINA"),+ ("REPLACE", "DEĞİŞTİR"),+ ("REPLACEB", "DEĞİŞTİRB"),+ ("REPT", "YİNELE"),+ ("RIGHT", "SAĞDAN"),+ ("RIGHTB", "SAĞB"),+ ("ROMAN", "ROMEN"),+ ("ROUND", "YUVARLA"),+ ("ROUNDDOWN", "AŞAĞIYUVARLA"),+ ("ROUNDUP", "YUKARIYUVARLA"),+ ("ROW", "SATIR"),+ ("ROWS", "SATIRSAY"),+ ("RRI", "GERÇEKLEŞENYATIRIMGETİRİSİ"),+ ("RSQ", "RKARE"),+ ("SEARCH", "MBUL"),+ ("SEARCHB", "ARAB"),+ ("SECOND", "SANİYE"),+ ("SEQUENCE", "SIRALI"),+ ("SERIESSUM", "SERİTOPLA"),+ ("SHEET", "SAYFA"),+ ("SHEETS", "SAYFALAR"),+ ("SIGN", "İŞARET"),+ ("SIN", "SİN"),+ ("SINH", "SİNH"),+ ("SKEW", "ÇARPIKLIK"),+ ("SKEW.P", "ÇARPIKLIK.P"),+ ("SLN", "DA"),+ ("SLOPE", "EĞİM"),+ ("SMALL", "KÜÇÜK"),+ ("SORT", "SIRALA"),+ ("SORTBY", "SIRALAÖLÇÜT"),+ ("SQRT", "KAREKÖK"),+ ("SQRTPI", "KAREKÖKPİ"),+ ("STANDARDIZE", "STANDARTLAŞTIRMA"),+ ("STDEV", "STDSAPMA"),+ ("STDEV.P", "STDSAPMA.P"),+ ("STDEV.S", "STDSAPMA.S"),+ ("STDEVA", "STDSAPMAA"),+ ("STDEVP", "STDSAPMAS"),+ ("STDEVPA", "STDSAPMASA"),+ ("STEYX", "STHYX"),+ ("SUBSTITUTE", "YERİNEKOY"),+ ("SUBTOTAL", "ALTTOPLAM"),+ ("SUM", "TOPLA"),+ ("SUMIF", "ETOPLA"),+ ("SUMIFS", "ÇOKETOPLA"),+ ("SUMPRODUCT", "TOPLA.ÇARPIM"),+ ("SUMSQ", "TOPKARE"),+ ("SUMX2MY2", "TOPX2EY2"),+ ("SUMX2PY2", "TOPX2AY2"),+ ("SUMXMY2", "TOPXEY2"),+ ("SWITCH", "İLKEŞLEŞEN"),+ ("SYD", "YAT"),+ ("T", "M"),+ ("T.DIST", "T.DAĞ"),+ ("T.DIST.2T", "T.DAĞ.2K"),+ ("T.DIST.RT", "T.DAĞ.SAĞK"),+ ("T.INV", "T.TERS"),+ ("T.INV.2T", "T.TERS.2K"),+ ("TAKE", "AL"),+ ("TBILLEQ", "HTAHEŞ"),+ ("TBILLPRICE", "HTAHDEĞER"),+ ("TBILLYIELD", "HTAHÖDEME"),+ ("TDIST", "TDAĞ"),+ ("TEXT", "METNEÇEVİR"),+ ("TEXTAFTER", "SONRAKİMETİN"),+ ("TEXTBEFORE", "ÖNCEKİMETİN"),+ ("TEXTJOIN", "METİNBİRLEŞTİR"),+ ("TEXTSPLIT", "METİNBÖL"),+ ("TIME", "ZAMAN"),+ ("TIMEVALUE", "ZAMANSAYISI"),+ ("TINV", "TTERS"),+ ("TOCOL", "SÜTUNA"),+ ("TODAY", "BUGÜN"),+ ("TOROW", "SATIRA"),+ ("TRANSPOSE", "DEVRİK_DÖNÜŞÜM"),+ ("TREND", "EĞİLİM"),+ ("TRIM", "KIRP"),+ ("TRIMMEAN", "KIRPORTALAMA"),+ ("TRUE", "DOĞRU"),+ ("TRUNC", "NSAT"),+ ("TYPE", "TÜR"),+ ("UNICHAR", "UNICODEKARAKTERİ"),+ ("UPPER", "BÜYÜKHARF"),+ ("USDOLLAR", "ABDDOLAR"),+ ("VALUE", "SAYIYAÇEVİR"),+ ("VALUETOTEXT", "DEĞERMETİN"),+ ("VARP", "VARS"),+ ("VARPA", "VARSA"),+ ("VDB", "DAB"),+ ("VLOOKUP", "DÜŞEYARA"),+ ("VSTACK", "DÜŞEYYIĞ"),+ ("WEBSERVICE", "WEBHİZMETİ"),+ ("WEEKDAY", "HAFTANINGÜNÜ"),+ ("WEEKNUM", "HAFTASAY"),+ ("WEIBULL.DIST", "WEIBULL.DAĞ"),+ ("WORKDAY", "İŞGÜNÜ"),+ ("WORKDAY.INTL", "İŞGÜNÜ.ULUSL"),+ ("WRAPCOLS", "SÜTUNSAR"),+ ("WRAPROWS", "SATIRSAR"),+ ("XIRR", "AİÇVERİMORANI"),+ ("XLOOKUP", "ÇAPRAZARA"),+ ("XMATCH", "ÇAPRAZEŞLEŞTİR"),+ ("XNPV", "ANBD"),+ ("XOR", "ÖZELVEYA"),+ ("YEAR", "YIL"),+ ("YEARFRAC", "YILORAN"),+ ("YIELD", "ÖDEME"),+ ("YIELDDISC", "ÖDEMEİND"),+ ("YIELDMAT", "ÖDEMEVADE"),+];++#[cfg(test)]+mod tests {+ use super::TURKISH;++ /// Each name appears once on each side, in upper case, as a valid function name; a Turkish+ /// name is never another function's English name (it would change what that name calls).+ #[test]+ fn table_is_one_to_one_and_well_formed() {+ let mut english = std::collections::HashSet::new();+ let mut turkish = std::collections::HashSet::new();+ for (en, tr) in TURKISH {+ assert!(english.insert(*en), "{en} twice");+ assert!(turkish.insert(*tr), "{tr} twice");+ assert_ne!(en, tr, "{en}: same as English, leave it out");+ assert!(tr.chars().all(|c| c.is_uppercase() || c.is_ascii_digit() || matches!(c, '.' | '_')), "{tr}");+ }+ for (en, tr) in TURKISH {+ assert!(!english.contains(tr), "{en}: {tr} is also an English name");+ }+ }+}@@ -2,3 +2,4 @@ mod printer_roundtrip; mod spanish_input;+mod turkish_input;new file mode 100644@@ -0,0 +1,108 @@+use gridcraft_formula::{FormulaLocale, parse, parse_input, print, print_input};++const TR: FormulaLocale = FormulaLocale::Tr;++fn input(src: &str) -> String {+ print(&parse_input(src, TR, |_| false).unwrap())+}++#[test]+fn turkish_function_names_and_separators_as_in_excel() {+ for (src, expected) in [+ ("=topla(A1:A10)", "SUM(A1:A10)"),+ ("EĞER(A1>0;\"Var\";\"Yok\")", "IF(A1>0,\"Var\",\"Yok\")"),+ ("eğer(A1>0;1;2)", "IF(A1>0,1,2)"),+ ("DÜŞEYARA(A2;D:E;2;YANLIŞ)", "VLOOKUP(A2,D:E,2,FALSE)"),+ ("ETOPLA(A1:A10;\">0\")", "SUMIF(A1:A10,\">0\")"),+ ("EĞERSAY(A1:A10;\">0\")", "COUNTIF(A1:A10,\">0\")"),+ ("ÇOKETOPLA(C:C;A:A;\"x\";B:B;\">5\")", "SUMIFS(C:C,A:A,\"x\",B:B,\">5\")"),+ ("BAĞ_DEĞ_SAY(A1:A9)", "COUNT(A1:A9)"),+ ("DEVRESEL_ÖDEME(0,08/12;10;10000)", "PMT(0.08/12,10,10000)"),+ ("İNDİS(A1:B5;KAÇINCI(\"x\";A1:A5;0);2)", "INDEX(A1:B5,MATCH(\"x\",A1:A5,0),2)"),+ ("indis(A1:B5;2;1)", "INDEX(A1:B5,2,1)"),+ ("ÇAPRAZARA(1;A:A;B:B)", "XLOOKUP(1,A:A,B:B)"),+ ("TOPLA.ÇARPIM(A1:A3;B1:B3)", "SUMPRODUCT(A1:A3,B1:B3)"),+ ("YUVARLA(1,2345;2)", "ROUND(1.2345,2)"),+ ("BUGÜN()", "TODAY()"),+ ("satır()", "ROW()"),+ ("MİN(1;2)+min(3;4)", "MIN(1,2)+MIN(3,4)"),+ // Excel's own Turkish names are kept, even where they look swapped.+ ("SANÇIKAR(\"1+i\";\"2\")", "IMSUM(\"1+i\",\"2\")"),+ ("SANTOPLA(\"1+i\";\"2\")", "IMSUB(\"1+i\",\"2\")"),+ ("=doğru", "TRUE"),+ ("=YANLIŞ", "FALSE"),+ ("DOĞRU()", "TRUE()"),+ // English names are always accepted, as are functions Turkish Excel leaves in English.+ ("SUM(1;2)+MOD(7;2)", "SUM(1,2)+MOD(7,2)"),+ ("LET(x;2;x*3)", "LET(x,2,x*3)"),+ ("LAMBDA(x;x+1)(2)", "LAMBDA(x,x+1)(2)"),+ ] {+ assert_eq!(input(src), expected, "{src}");+ }+}++#[test]+fn turkish_letters_follow_turkish_case_rules() {+ // `i` upper-cases to `İ` and `ı` to `I`, as Turkish Excel reads them.+ assert_eq!(TR.upper("eğer"), "EĞER");+ assert_eq!(TR.upper("indis"), "İNDİS");+ assert_eq!(TR.upper("satır"), "SATIR");+ assert_eq!(TR.canonical_name("min"), "MIN");+ assert_eq!(TR.canonical_name("MİN"), "MIN");+ assert_eq!(TR.canonical_name("Düşeyara"), "VLOOKUP");+ // Not a Turkish name: the text is left as typed (an English or user-defined name).+ assert_eq!(TR.canonical_name("MIN"), "MIN");+ assert_eq!(TR.function_name("vlookup"), "DÜŞEYARA");+ assert_eq!(TR.function_name("LAMBDA"), "LAMBDA");+}++#[test]+fn turkish_display_round_trips() {+ for english in [+ "IF(TRUE,SUM(1.5,2.25),0)",+ "VLOOKUP(A2,Sheet2!A:C,3,FALSE)",+ "IFERROR(INDEX(A1:A9,MATCH(B1,C1:C9,0)),\"\")",+ "SUM({1.5,2;3,4})+SUM((A1:A3,C1:C3))",+ "{TRUE,FALSE;FALSE,TRUE}",+ r#"IF(A1="a,b;c\d",CONCAT("1.5",",",";"),SUM(0.25,A1:A3 B2))"#,+ ] {+ let expr = parse(english).unwrap();+ let shown = print_input(&expr, TR, |_| false).unwrap();+ assert_eq!(print(&parse_input(&shown, TR, |_| false).unwrap()), print(&expr), "{english} shown as {shown}");+ }+ let expr = parse("IF(TRUE,SUM(1.5,2.25),VLOOKUP(1,A:B,2,FALSE))").unwrap();+ assert_eq!(print_input(&expr, TR, |_| false).unwrap(), "EĞER(DOĞRU;TOPLA(1,5;2,25);DÜŞEYARA(1;A:B;2;YANLIŞ))");+}++#[test]+fn turkish_array_constants_and_references() {+ // Turkish Excel: `;` between the columns of an array constant, `\` between its rows.+ assert_eq!(input("TOPLA({1;2\\3;4};5)"), "SUM({1,2;3,4},5)");+ assert_eq!(input("{DOĞRU;YANLIŞ\\TRUE;FALSE}"), "{TRUE,FALSE;TRUE,FALSE}");+ assert_eq!(print_input(&parse("{1.5,2;3,4}").unwrap(), TR, |_| false).unwrap(), "{1,5;2\\3;4}");+ for (src, expected) in [+ ("TOPLA((A1:A3;C1:C3))", "SUM((A1:A3,C1:C3))"),+ ("\"TOPLA;YANLIŞ\"&\"a\"\"b\"", "\"TOPLA;YANLIŞ\"&\"a\"\"b\""),+ ("'TOPLA;YANLIŞ'!A1+TOPLA!B2", "'TOPLA;YANLIŞ'!A1+TOPLA!B2"),+ ("Tablo1[EĞER]+YANLIŞ[TOPLA]", "Tablo1[EĞER]+YANLIŞ[TOPLA]"),+ ] {+ assert_eq!(input(src), expected, "{src}");+ }+ for bad in ["{TOPLA(A1)}", "{YANLIŞ\\}", "TOPLA(1.5;2)"] {+ assert!(parse_input(bad, TR, |_| false).is_err(), "{bad}");+ }+}++#[test]+fn turkish_input_preserves_defined_and_local_names() {+ let known = |name: &str| ["TOPLA", "YANLIŞ"].iter().any(|n| gridcraft_formula::same_name(n, name));+ let expr = parse_input("TOPLA(2;3)+YANLIŞ+EĞER(DOĞRU;4;5)", TR, known).unwrap();+ assert_eq!(print(&expr), "TOPLA(2,3)+YANLIŞ+IF(TRUE,4,5)");+ for (src, expected) in [+ ("LET(YANLIŞ;7;YANLIŞ)", "LET(YANLIŞ,7,YANLIŞ)"),+ ("LET(TOPLA;LAMBDA(x;x+1);TOPLA(2))", "LET(TOPLA,LAMBDA(x,x+1),TOPLA(2))"),+ ("LAMBDA(EĞER;EĞER+1)(2)", "LAMBDA(EĞER,EĞER+1)(2)"),+ ] {+ assert_eq!(input(src), expected, "{src}");+ }+}@@ -162,11 +162,12 @@ impl EditState { } let before = self.text.get(..self.byte_caret()).unwrap_or(""); let word: String =- before.chars().rev().take_while(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '_').collect::<String>().chars().rev().collect();+ before.chars().rev().take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_').collect::<String>().chars().rev().collect(); if word.is_empty() || word.chars().next().is_some_and(|c| c.is_ascii_digit()) { return; }- let u = word.to_ascii_uppercase();+ // Turkish upper case: `eğer` → `EĞER`, `i` → `İ`.+ let u = self.locale.upper(&word); // Not after a column letter of a reference like A1 (digits follow) – fine either way. self.autocomplete = names.iter().filter(|n| n.starts_with(&u)).take(12).cloned().collect(); if self.autocomplete.len() == 1 && self.autocomplete[0] == u {@@ -180,7 +181,7 @@ impl EditState { let Some(name) = self.autocomplete.get(self.ac_index).cloned() else { return false }; let caret_b = self.byte_caret(); let before = self.text.get(..caret_b).unwrap_or("");- let wlen: usize = before.chars().rev().take_while(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '_').map(char::len_utf8).sum();+ let wlen: usize = before.chars().rev().take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_').map(char::len_utf8).sum(); let start = caret_b - wlen; let (Some(a), Some(b)) = (self.text.get(..start), self.text.get(caret_b..)) else { return false }; self.text = format!("{a}{name}({b}");@@ -210,7 +211,7 @@ impl EditState { } let head = before.strip_suffix('(').unwrap_or(before); let name: String =- head.chars().rev().take_while(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '_').collect::<String>().chars().rev().collect();+ head.chars().rev().take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_').collect::<String>().chars().rev().collect(); if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) { return false; }@@ -258,7 +259,7 @@ impl EditState { let name: String = chars[..i] .iter() .rev()- .take_while(|c| c.is_ascii_alphanumeric() || **c == '.' || **c == '_')+ .take_while(|c| c.is_alphanumeric() || **c == '.' || **c == '_') .collect::<String>() .chars() .rev()@@ -266,7 +267,7 @@ impl EditState { if name.is_empty() { return None; }- return Some((name.to_ascii_uppercase(), arg));+ return Some((self.locale.upper(&name), arg)); } depth -= 1; }@@ -186,7 +186,12 @@ impl Language { } pub fn formula_locale(self) -> gridcraft_engine::formula::FormulaLocale {- if self == Self::Es { gridcraft_engine::formula::FormulaLocale::Es } else { gridcraft_engine::formula::FormulaLocale::En }+ use gridcraft_engine::formula::FormulaLocale;+ match self {+ Self::Es => FormulaLocale::Es,+ Self::Tr => FormulaLocale::Tr,+ _ => FormulaLocale::En,+ } } /// Translates an interface label; untranslated labels fall back to English.@@ -220,14 +220,18 @@ fn user_defined_prefix_is_hidden_while_editing_and_kept_on_commit() { } #[test]-fn turkish_interface_retains_its_canonical_formula_fallback() {+fn turkish_interface_enters_and_shows_turkish_formulas() { let mut app = app(); app.run("app.language.set", json!({"code":"tr-TR"})).unwrap(); assert_eq!(app.ui.language, Language::Tr);- assert!(enter(&mut app, "=SUM(1.5,2.25)"));+ // Typed as in Turkish Excel (Turkish case rules, `;` arguments, decimal comma), stored in English.+ assert!(enter(&mut app, "=eğer(DOĞRU;topla(1,5;2,25);0)"));+ assert_eq!(formula(&app), "IF(TRUE,SUM(1.5,2.25),0)"); app.begin_edit(None, true);- assert_eq!(app.editor.as_ref().unwrap().text, "=SUM(1.5,2.25)");- assert!(!enter(&mut app, "=TOPLA(1;2)"));+ assert_eq!(app.editor.as_ref().unwrap().text, "=EĞER(DOĞRU;TOPLA(1,5;2,25);0)");+ assert!(enter(&mut app, "=indis({10;20;30};1;2)"));+ assert_eq!(formula(&app), "INDEX({10,20,30},1,2)");+ assert!(!enter(&mut app, "=TOPLA(1.5;2)")); let restored: UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap(); assert_eq!(restored.language, Language::Tr); }@@ -1,8 +1,9 @@ # Formula language View → Interface language is the existing, persisted language preference. Choose **Español**-for Spanish formula entry and display. On first run the preference follows the system language.-English and the other interface languages currently use the English formula table and separators.+for Spanish or **Türkçe** for Turkish formula entry and display. On first run the preference+follows the system language. English and the other interface languages currently use the English+formula table and separators. | Spanish | Canonical English | | --- | --- |@@ -27,6 +28,19 @@ without a Spanish entry retain their English names/help. General interface label fall back to English where Spanish translations are not provided. An edit captures its language when it starts so switching the preference cannot reinterpret unfinished input. +## Turkish++Turkish uses Microsoft Excel's Turkish function names for 439 functions+(`crates/formula/src/turkish.rs`): `TOPLA`, `EĞER`, `DÜŞEYARA`, `ETOPLA`, `EĞERSAY`,+`BAĞ_DEĞ_SAY`, `İNDİS`, `KAÇINCI`, `DEVRESEL_ÖDEME`, `ÇAPRAZARA`… and `DOĞRU` / `YANLIŞ` for+TRUE and FALSE. Names follow Turkish case rules: `eğer` is `EĞER`, `indis` is `İNDİS` (`i` ↔ `İ`,+`ı` ↔ `I`). Arguments are separated by `;`, decimals use a comma, and array constants separate+columns with `;` and rows with `\`, as in Turkish Excel: `=TOPLA({1;2\3;4};1,5)` is 11.5.+Functions Turkish Excel leaves in English (`LET`, `LAMBDA`, `BYROW`, `MOD`, `RANK`…) and those+whose Turkish name is not confirmed yet keep their English names; English names are always+accepted. Excel's own Turkish names are kept even where they look swapped (`IMSUM` is+`SANÇIKAR`, `IMSUB` is `SANTOPLA`), so a formula typed as in Turkish Excel means the same here.+ Workbook/sheet names, table names, LET bindings and LAMBDA parameters take precedence over aliases. Text, sheet names and table column names are unchanged. When a localized spelling would collide with a name, displayed formulas retain the canonical spelling to preserve meaning.@@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (missing translations are listed by `cargo xtask i18n-report` and never fail a check) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (missing translations are listed by `cargo xtask i18n-report` and never fail a check) · **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,@@ -110,6 +110,7 @@ language (none so far) and of localized function names. | Date | Change | Summary | |---|---|---|+| 2026-10-27 | minor | Turkish formula entry (`FormulaLocale::Tr`): Excel's Turkish names for 439 functions (`crates/formula/src/turkish.rs`), `DOĞRU`/`YANLIŞ`, `;` arguments, decimal comma, `;` columns and `\` rows in array constants; Turkish case rules (`indis` = `İNDİS`) in parsing, autocomplete, argument hints and Ctrl+Shift+A. English names stay accepted | | 2026-10-23 | minor | Missing translations are reported, never a failure: `cargo xtask i18n-report` prints, per language, the strings other catalogs translate and it lacks (plus Spanish function help rows keyed by English the function catalog no longer has) and always passes; `cargo xtask ci` runs it first. Coverage and per-language parity tests and tests of one language's exact wording are gone; UI tests take the expected text from the catalog. Duplicate-key, placeholder, context-prefix and line-shape checks stay | | 2026-10-21 | minor | Romanian interface (`ro`, Română): 970 labels covering every key of the other catalogs (`crates/ui-egui/src/i18n/ro.rs`), `ro-*` system locales, the View-tab switcher and `app.language.set`; context keys show no prefix (File › Options test). English formulas and formats; AI-assisted draft pending native-speaker review | | 2026-10-20 | minor | Japanese: furigana (`rPh` phonetic runs and `<phoneticPr>` in `xl/sharedStrings.xml`) is no longer deleted by an open + save; it is kept on the workbook and written back (a `<phoneticPr>` without runs is kept too, while duplicate texts with different readings keep only the first, reported on open). Display of the readings is still missing (`crates/xlsx/src/tests/phonetic_tests.rs`) |