Intl API Formatting
Format numbers, dates, lists, and relative time with Intl — locales, options, and why you should stop hand-rolling currency strings.
- 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
- Currency must be ISO code (
USD), not a symbol. - Don’t concatenate locale fragments with raw strings for grammar — use full formatters / ICU message libs for sentences.
- SSR hydration — server default locale vs browser can mismatch formatting; fix locale explicitly.
- Legacy
toLocaleStringcalls the same engines but reusingIntl.*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.”
Related
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
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.