diff --git a/crates/formula/src/input.rs b/crates/formula/src/input.rs
--- a/crates/formula/src/input.rs
+++ b/crates/formula/src/input.rs
@@ -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)?;
                 }
             }
         }
diff --git a/crates/formula/src/lexer.rs b/crates/formula/src/lexer.rs
--- a/crates/formula/src/lexer.rs
+++ b/crates/formula/src/lexer.rs
@@ -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();
diff --git a/crates/formula/src/lib.rs b/crates/formula/src/lib.rs
--- a/crates/formula/src/lib.rs
+++ b/crates/formula/src/lib.rs
@@ -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};
diff --git a/crates/formula/src/parser.rs b/crates/formula/src/parser.rs
--- a/crates/formula/src/parser.rs
+++ b/crates/formula/src/parser.rs
@@ -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),
diff --git a/crates/formula/src/turkish.rs b/crates/formula/src/turkish.rs
new file mode 100644
--- /dev/null
+++ b/crates/formula/src/turkish.rs
@@ -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");
+        }
+    }
+}
diff --git a/crates/formula/tests/it/main.rs b/crates/formula/tests/it/main.rs
--- a/crates/formula/tests/it/main.rs
+++ b/crates/formula/tests/it/main.rs
@@ -2,3 +2,4 @@
 
 mod printer_roundtrip;
 mod spanish_input;
+mod turkish_input;
diff --git a/crates/formula/tests/it/turkish_input.rs b/crates/formula/tests/it/turkish_input.rs
new file mode 100644
--- /dev/null
+++ b/crates/formula/tests/it/turkish_input.rs
@@ -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}");
+    }
+}
diff --git a/crates/ui-egui/src/editor.rs b/crates/ui-egui/src/editor.rs
--- a/crates/ui-egui/src/editor.rs
+++ b/crates/ui-egui/src/editor.rs
@@ -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;
                 }
diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs
--- a/crates/ui-egui/src/i18n.rs
+++ b/crates/ui-egui/src/i18n.rs
@@ -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.
diff --git a/crates/ui-egui/tests/it/formula_locale.rs b/crates/ui-egui/tests/it/formula_locale.rs
--- a/crates/ui-egui/tests/it/formula_locale.rs
+++ b/crates/ui-egui/tests/it/formula_locale.rs
@@ -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);
 }
diff --git a/docs/formula-input.md b/docs/formula-input.md
--- a/docs/formula-input.md
+++ b/docs/formula-input.md
@@ -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.
diff --git a/docs/localization-parity.md b/docs/localization-parity.md
--- 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-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`) |
