Most developers assume that changing the case of a string is a trivial mathematical operation: subtract 32 from the character code of lowercase 'a' (97) to get uppercase 'A' (65). That works for 26 ASCII letters and nothing else.
Modern applications process global UTF-8 text. In that world naive casing produces failed logins, duplicate accounts, corrupted lookups and truncated data. This article covers the traps that actually cause incidents, and how to name things so that the data layers of a system stay consistent.
01. The Infamous "Turkish i" Bug
Turkish and Azeri use two distinct letters for the i sound: dotted i / İ and dotless ı / I. Under Turkish rules:
- lowercase dotted
iuppercases to dotted capitalİ(U+0130), notI; - lowercase dotless
ı(U+0131) uppercases toI.
The bug appears when a library silently picks the machine's locale. In Java, "admin".toUpperCase() without an explicit locale gives "ADMİN" on a Turkish system, and C# ToUpper() behaves the same way with the current culture. A check like role.toUpperCase().equals("ADMIN") then fails only for Turkish users, and only in production.
JavaScript is safer here: toUpperCase() and toLowerCase() ignore the host locale, and only toLocaleUpperCase("tr") applies Turkish rules. The rule that follows is simple: use locale-independent conversions for identifiers, keys and protocol strings, and locale-aware conversions only for text shown to people.
"title".toUpperCase() // "TITLE" (always)
"title".toLocaleUpperCase("tr") // "TİTLE" (explicit Turkish rules)
"I".toLocaleLowerCase("tr") // "ı" (dotless)
02. When Uppercase Changes the Length
Unicode case mappings are not always one character to one character. Three examples show up regularly:
- German sharp s:
"straße".toUpperCase()returns"STRASSE". One character became two. - Ligatures: the single character
fi(U+FB01) uppercases toFI. - Greek final sigma:
Σlowercases toσin the middle of a word but toςat the end, so lowercasing depends on context.
If you store an uppercased value in a fixed-width column or a fixed-size buffer, a string that fit before conversion may be truncated or rejected after it. Always measure the converted string, not the original.
03. Case Folding: Comparing Strings Without Case
Lowercasing and case folding are different jobs. Lowercasing prepares text for display. Case folding, defined in the Unicode CaseFolding.txt data file, prepares text for caseless comparison. The difference is visible with the German example:
"STRASSE".toLowerCase() === "straße".toLowerCase() // false: "strasse" vs "straße" // Full case folding maps both to "strasse", so they compare equal.
JavaScript has no built-in caseFold(). Practical options are to compare with Intl.Collator(undefined, { sensitivity: "base" }) for human-facing matching, or to normalize and lowercase consistently on both sides of every comparison for identifiers. The important habit is applying one canonical transformation everywhere a value is stored and looked up.
04. Normalization: One Character, Two Representations
The letter é can be a single code point (U+00E9) or an e followed by a combining acute accent (U+0065 U+0301). They look identical and compare as different strings:
"\u00e9" === "e\u0301" // false
"\u00e9" === "e\u0301".normalize("NFC") // true
"\uFB01".normalize("NFKD") // "fi"
"\uFF21".normalize("NFKD") // "A" (full-width A)
| Form | What it does | Typical use |
|---|---|---|
| NFC | Composes sequences into single code points | Storing and comparing user text |
| NFD | Decomposes into base letter plus combining marks | Stripping accents, e.g. building URL slugs |
| NFKC / NFKD | Also folds compatibility characters (ligatures, full-width forms, circled digits) | Search indexes and username uniqueness checks |
For usernames and similar identifiers, a common safe pipeline is normalize (NFKC), then lowercase or case-fold, then compare. Skipping the first step lets two visually identical names coexist as different accounts.
05. Naming Conventions Across the Stack
Modern projects pass data across several environments, each with its own convention. Mixing them inside one layer is what causes mapping bugs:
| Stack Layer | Standard Convention | Why It Was Chosen |
|---|---|---|
| SQL & Relational Databases | snake_case | Many databases fold unquoted identifiers to a single case, so underscores keep word boundaries safe |
| REST & GraphQL APIs | camelCase | JavaScript object keys deserialize directly into frontend state |
| CSS & Web URLs | kebab-case | Hyphens are legal in selectors and URL paths and are read as word separators by search engines |
| Classes & Components | PascalCase | Marks types and UI components in JavaScript, TypeScript, C# and React |
Bridging these conventions needs a deterministic splitter that breaks words on uppercase transitions, digits and punctuation, then re-joins them in the target style. That is what the ToolsGrid Case Converter does, using locale-independent JavaScript conversions so the output is identical on every device. Its slug mode applies NFD and strips combining accents, so Café Menu becomes cafe-menu.
Convert Case Locally With ToolsGrid
Switch between Title Case, camelCase, PascalCase, snake_case, kebab-case and URL slugs in one click. Text is processed in your browser and never uploaded.
Frequently Asked Unicode Casing Questions
What is the Turkish 'i' problem in Unicode case conversion?
Turkish and Azeri have two distinct letters: dotted i/İ and dotless ı/I. In Turkish locale rules, lowercase i uppercases to İ (U+0130), not I. APIs that use the machine's default locale, such as Java's String.toUpperCase() or C#'s ToUpper(), can therefore turn "admin" into "ADMİN" and break identifier comparisons. JavaScript's toUpperCase() is locale-independent; only toLocaleUpperCase('tr') applies the Turkish mapping.
Why can lowercase to uppercase change the length of a string?
Case conversion is not a 1:1 character mapping in Unicode. For instance, the German lowercase sharp s ('ß') uppercases to two capital letters ('SS'), and the ligature 'fi' uppercases to 'FI'. The string grows, which can break fixed-width buffers, database column limits and index arithmetic.
What is the difference between lowercasing and case folding?
Lowercasing is a presentation operation. Case folding, defined by Unicode in CaseFolding.txt, is designed for caseless comparison: it maps 'ß' and 'SS' to the same form ('ss') where simple lowercasing does not. JavaScript has no built-in caseFold(), so comparisons that must be robust usually combine normalize() with toLowerCase() or use Intl.Collator with base sensitivity.
Does ToolsGrid's case converter use locale-specific rules?
No. It uses JavaScript's locale-independent toUpperCase() and toLowerCase(), so the result is the same on every device. Slug generation first decomposes text with NFD and removes combining accents, which turns 'café' into 'cafe'.