All lessons

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.

Intl.CollatorCLDR collationUCA

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: match
Same query, same name, two locales. Verified in ICU.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  1. 1
    On 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.
  2. 2
    Switch 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).
  3. 3
    Switch 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.
  4. 4
    Load “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.
  5. 5
    Type 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.
  6. 6
    In “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.

Sorted output, locale by locale
#
en
English (UCA default)
pl
Polish
01ŁabaLewandowski
02ŁapińskaLewiński
03LewandowskiLipiński
04LewińskiLis
05LipińskiŁaba
06LisŁapińska
07ŁukasiewiczŁukasiewicz
08MaciejewskiMaciejewski
09MałachowskiMałachowski
10ModlińskiModliń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.

Sort is not search

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.

The fix: For find-as-you-type, filters, and dedupe checks, build a second collator with 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.Collator to 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 apply sensitivity: "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

EngineeringProduct

Moments

  • ·Designing any sortable user list, table, or directory
  • ·Database schema review
  • ·Launching into a locale with non-trivial alphabet ordering

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.

Wikipedia: German orthography (alphabetization)

Quick check

3 questions · pass at 2+

  1. Question 1/3

    Where does ł sort in Polish?

  2. 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?

  3. Question 3/3

    Where do å, ä, ö sort in Swedish?

Words you'll hear

Where to read more

Related lessons