skip to content

Intl Formatting and Collation

The Intl namespace formats dates, numbers, currencies, plurals and relative times per locale, and Collator sorts text the way humans in that locale expect. Interviewers ask because hand-rolled formatting and naive Array.sort on strings are classic i18n defects.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

6

In JavaScript, what does calling Number.prototype.toLocaleString() or Date.prototype.toLocaleDateString() with no arguments actually do, and how do those methods relate to the Intl constructors?

level: juniorimportance: should knowfreq 45%

answer

  1. thin wrapper, not a separate engine
  2. same locales and options arguments
  3. default locale comes from the host
  4. a server default is not the user's locale
  5. reuse one formatter in a loop

basics

~10 s

Both delegate to Intl: toLocaleString builds an Intl.NumberFormat, toLocaleDateString an Intl.DateTimeFormat, taking the same locales and options arguments. With no arguments each uses the runtime's default locale, which is host-dependent.

solid answer

~40 s

The `toLocale*` methods are thin wrappers over the Intl constructors. `Number.prototype.toLocaleString(locales, options)` is specified to build an `Intl.NumberFormat` with those arguments and format the number; `Date.prototype.toLocaleString`, `toLocaleDateString` and `toLocaleTimeString` do the same with `Intl.DateTimeFormat`, and `String.prototype.localeCompare` with `Intl.Collator`. So every option you can pass to the constructor — `style`, `currency`, `dateStyle`, `timeZone`, `minimumFractionDigits` — works on the method too. The catch is the no-argument form: it uses the *runtime's* default locale and time zone, which on a server is the machine's configuration, not the end user's. For a one-off value the wrapper is fine and reads well; when you format many values, construct one `Intl.NumberFormat` or `Intl.DateTimeFormat` and reuse it, because building the formatter is the expensive part.

go deeper

for a junior

Know that these methods take a locale tag and an options object, and that passing no locale means the machine's default rather than the user's. Be able to show (1234.5).toLocaleString('de-DE') producing a comma decimal separator.

for a middle

Explain the delegation precisely: which Intl constructor each method maps to, that the options bag is identical, and what the no-option defaults are (0 to 3 fraction digits for numbers). Say why the formatter, not the format call, is the expensive part.

for a senior

Show the production instinct: locale comes from user preference, not from the process; formatted strings are presentation and never data; tests pin locale and time zone. Be ready to explain why a naive string assertion on formatted currency fails in CI.

for a principal

Own the boundary rule — raw values travel through the system, formatting happens once at the render edge — and the cost of breaking it: formatted strings in a cache or an API response are un-reformattable and lock every consumer into one locale.

## What the methods actually are ECMAScript defines `Number.prototype.toLocaleString`, `Date.prototype.toLocaleString` / `toLocaleDateString` / `toLocaleTimeString`, `String.prototype.localeCompare` and `Array.prototype.toLocaleString`. In any runtime that ships the internationalization API (ECMA-402 — every modern browser and Node build), these are specified as delegations: - `num.toLocaleString(locales, options)` ≡ `new Intl.NumberFormat(locales, options).format(num)` - `date.toLocaleDateString(locales, options)` ≡ `new Intl.DateTimeFormat(locales, dateOptions).format(date)` - `a.localeCompare(b, locales, options)` ≡ `new Intl.Collator(locales, options).compare(a, b)` That is the whole relationship: the methods are sugar, and they accept exactly the arguments the constructors accept. ```js (1234.5).toLocaleString('de-DE'); // "1.234,5" (1234.5).toLocaleString('en-US', { style: 'currency', currency: 'USD', }); // "$1,234.50" ``` ## The two arguments `locales` is a BCP 47 language tag or an ordered array of them — `'en-GB'`, `['fr-CA', 'fr', 'en']`. The runtime negotiates against the locale data it actually has and may fall back (a request for `'de-AT'` can resolve to `'de'`). `options` is the same bag the corresponding `Intl` constructor takes. Omit both and you get the *default locale*: whatever the host reports. In a browser that tracks the user's language settings; in a server process it is the machine or container configuration, which is very often `en-US` regardless of who is reading the output. This is the single most common bug with these methods — a server-rendered page shows US formatting to every user in the world because nobody passed a locale. ## Defaults you inherit With no `options`, `Number.prototype.toLocaleString` uses decimal style with `minimumFractionDigits` 0 and `maximumFractionDigits` 3, which is why `(1234.5).toLocaleString('de-DE')` is `"1.234,5"` and not `"1.234,50"`. Date methods likewise pick a locale-default set of components. If you care about the exact shape, say so explicitly with options rather than relying on the default. ## The output is presentation, not data The returned string is for humans. It contains locale-specific group and decimal separators, and in several locales spaces that are *not* U+0020 — narrow no-break and no-break spaces appear around currency symbols and day-period markers. Two consequences: 1. Never parse a formatted string back into a number or a date. Keep the raw `number` / epoch value as your data and format at the edge. 2. Assertions like `expect(out).toBe('1 234,50 €')` are brittle: the exact spacing can change between ICU data versions shipped with the runtime. Compare against `format()` output produced the same way, or assert on `formatToParts()`. ```js // fragile: the space before the symbol may be U+00A0, not ' ' const s = (1234.5).toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' }); s === '1 234,50 €'; // may be false even though it looks identical ``` ## Array.prototype.toLocaleString A rarely used member of the family: it calls `toLocaleString` on each element and joins the results with a host-appropriate separator. It is not `Intl.ListFormat` — it gives you no control over conjunctions or style. Use `Intl.ListFormat` when you want "a, b, and c". ## When to reach for the constructor instead The wrapper constructs a formatter on every call. Engines cache aggressively behind the scenes, but the reliable optimization is explicit: build one formatter and reuse it. ```js const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }); rows.forEach((r) => render(money.format(r.total))); // one formatter, N formats ``` The constructor also gives you capabilities the method has no route to: `resolvedOptions()` to see which locale, calendar, numbering system and time zone were actually chosen; `formatToParts()` to build a custom layout from typed pieces; and `supportedLocalesOf()` to negotiate a locale list before you commit. Reach for `toLocaleString` for a single value in ordinary code, and for the `Intl` object whenever the formatting is repeated, configurable, or something you need to inspect.

  • If toLocaleString already does the job, when would you construct an Intl.NumberFormat explicitly?
    When you format many values — building the formatter is the costly part, so one instance reused across rows is measurably cheaper. Also whenever you need something the wrapper cannot expose: `resolvedOptions()` to see which locale and numbering system were actually chosen, `formatToParts()` to assemble a custom layout, or `supportedLocalesOf()` to negotiate a preference list before committing.
  • What does Array.prototype.toLocaleString do?
    It calls `toLocaleString` on each element and joins the results with a separator the host picks for the locale. It is not a list formatter: you get no control over the conjunction or the style, so `['a','b','c']` never becomes "a, b, and c". For that use `Intl.ListFormat`, which takes `type` (`conjunction`/`disjunction`) and `style` options.
  • How would you keep formatted output deterministic in tests?
    Pass an explicit locale, and for dates an explicit `timeZone` — never rely on the runtime default, which differs between a developer laptop and CI. Avoid exact string equality on the result: several locales emit non-breaking or narrow no-break spaces around symbols and day periods, and that spacing can shift with the ICU data a runtime ships. Assert on `formatToParts()` values instead.

saying these in an interview costs you the question

  • Thinking toLocaleString picks up the end user's locale on a server
  • Assuming the output can be parsed back into a number or date
  • Calling toLocaleString per row instead of reusing one formatter
  • Believing 'en-US' output is byte-identical across every runtime version
  • Thinking toLocaleString and Intl.NumberFormat are unrelated APIs

context

open as a page

A list of user-visible names sorted with names.sort() puts "Zebra" before "apple" and orders accented words oddly. Why does JavaScript's default string ordering do that, and what does Intl.Collator do differently?

level: middleimportance: should knowfreq 45%

basics

~20 s

The default comparison orders strings by UTF-16 code unit, so every uppercase letter precedes every lowercase one and accented letters land after "z". Intl.Collator compares by locale collation rules instead, which is what a human reader expects.

open as a page

A checkout page renders prices with '$' + amount.toFixed(2). What breaks once the product ships in more locales and currencies, and what does Intl.NumberFormat's style: 'currency' handle instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

Hand-rolled currency hardcodes the symbol, its position, the separators and two decimal places. Intl.NumberFormat with style: 'currency' and a currency code derives all of them from locale and currency data — JPY gets zero decimals, German puts the symbol last.

open as a page

A UI builds a message as `${n} item${n === 1 ? '' : 's'}`. Explain why that pattern is an internationalization defect, and what Intl.PluralRules provides instead.

level: middleimportance: should knowfreq 28%

basics

~20 s

That pattern hardcodes English's two-form plural. Other languages have up to six categories with different rules. Intl.PluralRules.select(n) returns the category tag for a locale — 'one', 'few', 'many', 'other' — which you use to pick a message from a per-language catalog.

open as a page

A service holds timestamps as epoch milliseconds and must display each one in a chosen time zone. How do you render that with Intl.DateTimeFormat, and why is slicing or regexing the formatted string to rearrange it a bug?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Pass a timeZone option with an IANA identifier to Intl.DateTimeFormat and format the timestamp; the zone affects only rendering. Never parse the output — component order, separators and digits vary by locale. Use formatToParts to assemble a custom layout from typed pieces.

open as a page

You own the formatting layer of a product shipped in twenty locales. How do you decide which locale each Intl formatter is constructed with, how do you verify what the runtime actually chose, and how do you keep formatting cheap on hot paths?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Pass an ordered preference list, not a single tag; negotiate it with supportedLocalesOf against the runtime's data; confirm the outcome with resolvedOptions().locale, which can differ from what you asked for; and cache formatter instances keyed by locale plus options, since construction is the expensive part.

open as a page