JavaScript Intl Table

ConstructorWhat it formatsField note
Intl.NumberFormat(locale, opts)Numbers, currency, units1,234.56 in en-US is 1.234,56 in de-DE - grouping is locale law
Intl.DateTimeFormat(locale, opts)Dates and timesdateStyle/timeStyle presets; calendar choice follows locale
Intl.RelativeTimeFormat"3 days ago"numeric: auto drops the "in 0 days" awkwardness
Intl.ListFormatNatural listsA, B, and C - conjunction per language, no hand-built join
Intl.CollatorLocale-aware sortingsort() with a collator - accents and case sort correctly
Intl.PluralRulesSelect the plural formOne/few/many is locale grammar - never hardcode s
Intl.DisplayNamesRegion/language namesregion code 'DE' becomes Deutschland/Germany per locale
new Intl.Locale(s)Parse and extend tagslanguageScript-REGION-u-nu-... - BCP 47 as an object
Reference: the MDN Intl reference. Intl is the internationalization engine built into the platform: every constructor takes a locale and renders by that locale's LAW - grouping separators, calendar names, plural categories, even sort order. The rule that kills hand-rolled formatting: plural forms are grammar (Russian has one/few/many), date order is convention (MDY vs DMY), and 'and' lists differ per language - Intl.PluralRules, DateTimeFormat and ListFormat exist because these are unsolvable with string templates. Bottom line: construct formatters ONCE and reuse (they parse the locale data eagerly), pass language-region tags like de-DE, and let Intl.Collator sort - the built-in sort() is codepoint order, not any human's alphabet. Related tools: date table (the Date objects Intl formats), number table (the doubles Intl renders), and world clock (Intl time zones live).

Intl is the internationalization engine built into the platform: every constructor takes a locale (de-DE, ar-EG, zh-CN) and renders by that locale's LAW - grouping separators, calendar names, plural categories, sort order. The data ships with the browser; no library, no CDN.

Bottom line: hand-rolled formatting dies at the grammar wall - Russian plurals have one/few/many forms, German groups thousands with dots, Japanese lists join with different particles. Intl.PluralRules, NumberFormat, DateTimeFormat and ListFormat exist because string templates cannot express any of it. Construct formatters once and reuse: they parse locale data eagerly.

The honest part: Intl output varies across engines and versions by design - it renders the platform's CLDR data, which updates. Snapshot tests on exact formatted strings are brittle; test the shape (a currency symbol present, the right plural category chosen), not the exact glyphs.

How to use

  1. Format money correctly: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.5) - symbol placement and separators follow the locale, not your string concat.
  2. Sort like a human: users.sort(new Intl.Collator('sv').compare) - Swedish sorts Å after Z; the default sort() is codepoint order and gets every accented alphabet wrong.
  3. Build natural lists: new Intl.ListFormat('en', { type: 'conjunction' }).format(names) - 'A, B, and C' with the right comma-and grammar per locale.

Frequently asked questions

Why can't I just add an 's' for plurals in my UI strings?

Because plural is grammar, not suffix arithmetic. English has singular/plural; Russian has one/few/many (2 дня, 5 дней, 21 день); Arabic has six categories; Chinese does not inflect at all. The mapping from number to category is locale data - which is exactly what Intl.PluralRules.select(n) returns ('one', 'few', 'many', 'other'). The production pattern: translation files carry one string per CATEGORY (keyed by those names), the code asks PluralRules for the category, and the right string follows in every language. Any hand-rolled n === 1 check is English-only by accident.

What does locale 'law' actually decide in NumberFormat and DateTimeFormat?

Everything visual: de-DE renders 1.234,56 (dot thousands, comma decimal) while en-US renders 1,234.56 - same number, opposite conventions, both correct. Currency style moves the symbol (€ before in Germany, after in many locales, $ before with its own spacing rules in en-US); DateTimeFormat picks MDY versus DMY, 12 versus 24-hour clocks, month names and first-week rules. This is why formatted strings must never be parsed back - they are display artifacts. The rule: store data as raw values (or ISO), format at the edge, and never concatenate your own separators.

Why does array.sort() mangle accented names, and how does Collator fix it?

The default sort compares UTF-16 code units - so É (U+00C9) sorts after z (U+007A), putting every accented name at the end of the list regardless of alphabet. Real dictionaries sort by language rules: Swedish treats å/ä/ö as letters AFTER z; German phone books expand ä to ae. Intl.Collator encodes those rule tables: new Intl.Collator('sv').compare is a ready-made comparator for sort(), with options for case ordering, numeric sorting (file2 after file10) and sensitivity (ignore accents when matching). It is also fast - collators cache their rule tables, so construct once and reuse.

When do RelativeTimeFormat, ListFormat and DisplayNames earn a library replacement?

Almost never in modern engines. RelativeTimeFormat produces '3 days ago' / 'in 3 days' with correct grammar; ListFormat joins with locale-appropriate conjunctions (English's Oxford comma, others without); DisplayNames turns region and language codes into properly capitalized names in any locale (translation dropdowns without shipping name tables). These used to require date-fns, i18next helpers or hand-maintained data - the platform versions are sufficient for display purposes and maintained by the engine's CLDR updates. Reach for a library when you need message management (ICU MessageFormat bundles, extraction pipelines) - not for any single format() call.

Related tools