Lesson 06·Unit 3 · The Intl toolbox·Locale-aware sorting and comparison·Lv 201·~6 min
Sorting: the alphabet is locale data
Polish ł sorts between l and m. Swedish ä sorts after y. German has two collations (dictionary vs phonebook), Spanish used to treat 'ch' as a single letter. Icelandic phone books sort by first name. The alphabet is locale data; your default sort isn't.
By the end
- ·Explain why résumé files next to resume: the UCA weighs base letters before it looks at accents.
- ·Ask for a locale's tailored order, German phonebook included, in the language tag itself.
- ·Build a locale-aware match with Intl.Collator usage:'search', and choose the sensitivity it needs.
The problem
Array.sort() sorts by UTF-16 code-unit order, which is essentially random for non-ASCII. A user list in a German UI shows Ö after Z. A Polish UI shows ł with the other accented Latin letters instead of between L and M. A Hebrew UI shows whatever order the JavaScript engine felt like. Sorted lists become un-scannable. Users start scrolling instead of searching, and your "sort by name" feature becomes furniture.
How it works
Sorting is an algorithm with opinions, not a byte order
The Unicode Collation Algorithm compares strings in passes: base letters first, then accents, then case. That is why résumé files next to resume instead of after zebra: é differs from e only at the second level, and the second level only matters when the first is a tie. A code-point sort has no levels. It files every accented word by its encoding accident.
Each language then tailors the default table to its own alphabet. Swedish appends å ä ö after z as letters 27–29. Polish gives ł its own slot between l and m. German maintains two standardized orders: dictionary, and phonebook, where ä expands to ae so Müller files with Mueller. You select tailorings in the locale tag itself: de-DE-u-co-phonebk is a complete, valid answer to "which alphabetical order?".
So the rule: any list a user reads as sorted goes through Intl.Collator with the user's locale. While you are there, numeric: true fixes file10 sorting before file2 for free. Plain sort() is code-point order, which is wrong somewhere for nearly every language, including English the moment an accent or an uppercase letter appears.
How it works
Matching is a different tailoring than ordering
Sorting decides where a name files. Search decides whether two spellings count as the same name. CLDR ships separate rules for each job, and new Intl.Collator(locale, { usage: 'search' }) loads the second set. Pair it with sensitivity: base ignores accents and case, accent counts accents but not case, case counts case but not accents, variant counts both. Your match test is compare(query, candidate) === 0: the collator compares whole strings, so for a filter or find-as-you-type you run it against each candidate yourself.
The folding rules are locale opinions. English search treats ü as a decorated u, so muller finds Müller. German search treats ü as equivalent to ue: muller misses and mueller hits, because that is the equivalence German spelling uses. Spanish keeps ñ a separate letter even at base sensitivity, so nunez does not find Núñez under es. Strip accents with a regex and you have hardcoded one language's opinion into every market.
const de = new Intl.Collator("de", { usage: "search", sensitivity: "base" });
const en = new Intl.Collator("en", { usage: "search", sensitivity: "base" });
en.compare("muller", "Müller") // 0: match (ü folds to u)
de.compare("muller", "Müller") // ≠ 0: no match (ü ≡ ue in German)
de.compare("mueller", "Müller") // 0: matchSee it yourself
Try these, in order
Each step triggers a specific failure you should recognize on sight.
- 1On the default Polish list, scan the highlighted cells across the en and pl columns. Łukasiewicz and Łapińska move: Polish sorts ł as its own letter between l and m. English UCA folds it near l. Same list, different positions.
- 2Switch to “German names · dictionary vs phonebook ordering”. Müller vs Mueller swap between the de and de-DE-u-co-phonebk columns: the collation variant is selected in the BCP-47 tag itself (-u-co-phonebk).
- 3Switch to the Swedish list. Ångström, Älg, Östergren sink to the end under sv: å/ä/ö are letters 27–29, after z. The Danish column orders them differently again. Scandinavian is not one collation.
- 4Load “Case sensitivity” and compare the three English columns. Default, uppercase-first (-u-kf-upper), and sensitivity:'base' each order apple/Banana/Apricot differently: case handling is one more tailorable collation decision.
- 5Type a few entries of your own into “Your list · comma-separated”, and include at least one accented word, e.g. Zorro, Öberg, chávez, Lukas, łódź. Two or more entries replace the sample list and every column re-sorts as you type. Watch where your accented entries land per column: the highlighting marks each row where a locale disagrees with the English default.
- 6In “Sort is not search”, keep the defaults (Search locale English, Sensitivity base, Search query muller), then switch Search locale to German (de). Under English, Müller matches (ü folds to u). Under German it does not: German search folding maps ü to ue, so the six-letter query no longer lines up. Now type mueller into Search query: German matches both Müller and Mueller, while English matches only Mueller. Which spellings count as the same name is locale data.
Each column below is the same input list, sorted by Intl.Collator with the matching locale tag. Read across a row to see where the same name lands in each sort. Or type two or more entries above to sort your own strings.
| # | en English (UCA default) | pl Polish |
|---|---|---|
| 01 | Łaba | Lewandowski |
| 02 | Łapińska | Lewiński |
| 03 | Lewandowski | Lipiński |
| 04 | Lewiński | Lis |
| 05 | Lipiński | Łaba |
| 06 | Lis | Łapińska |
| 07 | Łukasiewicz | Łukasiewicz |
| 08 | Maciejewski | Maciejewski |
| 09 | Małachowski | Małachowski |
| 10 | Modliński | Modliński |
Why this happens
CLDR ships a per-locale collation table: the canonical order of letters, the rules for accents, the rules for case. The Unicode Collation Algorithm (UCA) is the default. Locales override it. Intl.Collator reads the active locale, applies the overrides, and returns a comparator function for .sort().
- Polish ł is a separate letter that sorts between l and m. ASCII-sort puts it after z.
- German phonebook (DIN 5007-2) treats ä as if it were
ae. So Müller sorts as Mueller and interleaves with names spelled Mueller. - Swedish sorts ä near y at the end of the alphabet. Apps that sort ä with a look broken to Swedish users.
- Icelandic sorts people by their given name because patronymics change per generation. The national phone book does this. CRM software that filters by "last name" breaks here.
- Case sensitivity is locale data too. For most apps the correct default is
sensitivity: "base"with a secondary tiebreaker.
The fix
Wherever your code calls .sort() on a user-visible list, replace the implicit string comparator with new Intl.Collator(locale).compare. This one-line change fixes sorting everywhere CLDR ships a tailoring. It also turns the remaining edge cases into a visible bug you can escalate to a linguist.
Ordering a list and deciding whether two strings mean the same name are different jobs. CLDR ships separate tailorings for them. usage: "search" loads the matching rules. sensitivity sets which differences count. A candidate matches when compare(query, name) === 0. The API compares whole strings, so you run it against each entry yourself. Every verdict below is that live comparison, nothing precomputed.
new Intl.Collator("en", { usage: "search", sensitivity: "base" })
- Müllermatch
- Muellerno match
- Möllerno match
- Núñezno match
- Åströmno match
- Célineno match
The folding rules are locale data too
Try it: under English with base, muller matches Müller. Switch the search locale to German and the match disappears. German search collation folds ü toward ue, not toward u, so the six-letter query no longer lines up. Type mueller and German matches Müller while English does not. Neither engine is wrong. Each locale defines which spellings its users consider the same name. If your product normalizes search with one hardcoded rule, it is wrong somewhere.
usage: "search" and an explicit sensitivity. Do not reuse your sort collator, and do not strip accents with a regex. base is the forgiving default users expect from a search box.If you remember one thing
Alphabetical order is locale data. Use Intl.Collator for anything a user reads as a sorted list. Code-point sort is wrong in most languages, sometimes in two ways in one language.
What to do about it
- Always use
Intl.Collatorto compare strings for display. It uses CLDR collation data and respects the user's locale. - For multi-language sorting (one list, many locales), pick a "neutral" collator (
"und"or the UI's locale) and applysensitivity: "base"to fold accents together, better than guessing. - For numeric strings ("file2", "file10"), pass
{ numeric: true }. Otherwise file10 sorts before file2, every time. - On the database side, set the column collation to match the display locale (e.g. PostgreSQL
COLLATE "de-DE-x-icu"). Application-layer sort is cheap; database sort needs explicit configuration.
Use this with
Stakeholders
Moments
- ·Designing any sortable user list, table, or directory
- ·Database schema review
- ·Launching into a locale with non-trivial alphabet ordering
See it in a market: Sweden: where ä sorts after z →
Field note
Germany standardized two different alphabetical orders: DIN 5007-1 (dictionary: ä sorts with a) and DIN 5007-2 (phonebook: ä sorts as ae, so Müller files with Mueller). Neither is a bug. They serve different lookup tasks. If a national standards body needed two orderings, your .sort() default has no chance.
Quick check
3 questions · pass at 2+
Question 1/3
Where does ł sort in Polish?
Question 2/3
A search box matches with new Intl.Collator(locale, { usage: 'search', sensitivity: 'base' }). The user types “muller”. In which locale does it match the directory entry Müller?
Question 3/3
Where do å, ä, ö sort in Swedish?