ESC

Type to search the knowledge base.

Intl API Formatting

Format numbers, dates, lists, and relative time with Intl — locales, options, and why you should stop hand-rolling currency strings.

intermediate3 min read
  • javascript
  • intl
  • i18n
  • formatting

Hand-built date strings and $ + toFixed(2) break as soon as you gain a second locale. The Intl APIs format values with proper locale data: numerals, separators, currency, plural rules, and more. Browsers ship the data; you pass a locale and options.

NumberFormat

const usd = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
});
usd.format(1234.5); // "$1,234.50"

const dePercent = new Intl.NumberFormat('de-DE', {
  style: 'percent',
  maximumFractionDigits: 1,
});
dePercent.format(0.156); // "15,6 %"

const compact = new Intl.NumberFormat('en', {
  notation: 'compact',
  compactDisplay: 'short',
});
compact.format(12_300); // "12K"

Reuse formatters — construction is heavier than format().

const formatPrice = new Intl.NumberFormat(undefined, {
  style: 'currency',
  currency: 'INR',
});
// undefined locale → runtime default

DateTimeFormat

const d = new Date('2026-08-04T15:30:00Z');

new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: 'UTC',
}).format(d);

new Intl.DateTimeFormat('en-US', {
  weekday: 'long',
  month: 'long',
  day: 'numeric',
  timeZone: 'America/New_York',
}).format(d);

Always pass timeZone when the instant must render in a fixed zone (not the viewer’s local).

const parts = new Intl.DateTimeFormat('en', {
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
}).formatToParts(d);
// build custom layouts from parts

RelativeTimeFormat

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(3, 'hour'); // "in 3 hours"

You still compute the numeric delta; Intl only formats it.

ListFormat and plural rules

new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format([
  'React',
  'Vue',
  'Svelte',
]);
// "React, Vue, and Svelte"

const pr = new Intl.PluralRules('en');
pr.select(1); // "one"
pr.select(2); // "other"

const prRu = new Intl.PluralRules('ru');
prRu.select(3); // "few" — don't invent English-only plurals for other languages

DisplayNames

const region = new Intl.DisplayNames(['en'], { type: 'region' });
region.of('IN'); // "India"

const lang = new Intl.DisplayNames(['fr'], { type: 'language' });
lang.of('en'); // "anglais"

Locale negotiation

const locale = navigator.language; // "en-US"
Intl.NumberFormat.supportedLocalesOf(['ban', 'id-u-co-pinyin', 'de']);

Pass arrays of preferred locales; the runtime picks the best available.

Footguns

  1. Currency must be ISO code (USD), not a symbol.
  2. Don’t concatenate locale fragments with raw strings for grammar — use full formatters / ICU message libs for sentences.
  3. SSR hydration — server default locale vs browser can mismatch formatting; fix locale explicitly.
  4. Legacy toLocaleString calls the same engines but reusing Intl.* formatters is clearer and faster in loops.

Interview answer

“Intl provides locale-aware NumberFormat, DateTimeFormat, RelativeTimeFormat, ListFormat, and PluralRules. I construct formatters once with explicit locale and timeZone/currency options, and avoid hand-rolled separators. PluralRules matters for languages with more than one/other categories.”

Currency without floating lies

// store integer minor units when you can
const formatINR = new Intl.NumberFormat('en-IN', {
  style: 'currency',
  currency: 'INR',
});
formatINR.format(1999 / 100); // still binary float — OK for display of small money
// for banking math use integer cents + format the result
function formatCents(cents, currency, locale = 'en') {
  return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(
    cents / 100,
  );
}

Intl formats; it does not fix IEEE-754. Combine with integer money or decimal libraries for totals, then format once at the edge.

Locale lists and fallbacks

const fmt = new Intl.NumberFormat(['fr-CA', 'fr', 'en'], {
  style: 'currency',
  currency: 'CAD',
});
fmt.resolvedOptions().locale; // actual locale chosen

Pass a preference list; inspect resolvedOptions() when debugging “why did separators look English?” Missing ICU data in stripped embeds can fall back unexpectedly — test on target devices, not only desktop Chrome.

Further reading

Related guides