Topic 4.5
Formatting Numbers and Dates
In one line
The built-in Intl object formats numbers, money, percentages, units, dates and times the way each language and country expects: Intl.NumberFormat, Intl.DateTimeFormat and the toLocaleString shortcuts, plus helpers for relative times, lists, plurals and sorting.
Think of it like this
A translator at an airport desk. The same price, ₹1234567.5, is said differently to each traveller: an Indian traveller hears "12 lakh 34 thousand..." and sees ₹12,34,567.50, an American sees 1,234,567.50, and a German sees 1.234.567,50 (dots and commas swapped). The amount never changes; only the way it's written does. Intl is that translator: you hand it the value and say who it's for.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Format
- Turn a value into text for people to read, without changing the value itself.
- Locale
- A language and region setting, like en-IN, that decides how numbers and dates are written.
- BCP 47 tag
- The standard way to write a locale: a language code, then a region code, like
hi-IN. - Intl
- The built-in object that holds JavaScript's formatting and language tools.
- Currency code
- The three-letter ISO name of a currency, such as INR, USD or EUR.
- Time zone
- A region's clock rules, like Asia/Kolkata (UTC+5:30), including summer time changes.
- UTC
- Coordinated Universal Time: the world's reference clock, with no time zone offset.
- Collation
- The rules for putting words in alphabetical order in a given language.
- No-break space
- A space character that stops a line from breaking there. It looks like a space but isn't the normal one.
Step by step
01Make a formatter once
A formatter is an object configured for one locale and one set of options. Make it once, outside any loop, and call format as often as you like.
const rupees = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
console.log(rupees.format(1999.5)); // ₹1,999.50
console.log(rupees.format(1234567.89)); // ₹12,34,567.8902Choose the details with options
Options change the result without you touching the number. Here the same price drops its paise, and a ratio becomes a percentage.
const wholeRupees = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 });
console.log(wholeRupees.format(1999.5)); // ₹2,000
const pct = new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 });
console.log(pct.format(0.256)); // 25.6%03Format a moment in a chosen time zone
A Date is one moment in time. DateTimeFormat decides how that moment looks and on whose clock. The same moment is 10:30 in London (UTC in winter), 4:00 pm in India and 5:30 AM in New York.
const moment = new Date("2024-01-15T10:30:00Z");
const india = new Intl.DateTimeFormat("en-IN", { timeZone: "Asia/Kolkata", dateStyle: "medium", timeStyle: "short" });
console.log(india.format(moment)); // 15 Jan 2024, 4:00 pm04Say it like a person
Relative times, lists and plurals make interfaces read naturally, in every language, without hand-written rules.
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-1, "day")); // yesterday
const list = new Intl.ListFormat("en", { type: "conjunction" });
console.log(list.format(["tea", "coffee", "juice"])); // tea, coffee, and juice05Get the pieces when you need styling
formatToParts returns the formatted text as labelled pieces, so you can, for example, make the currency symbol smaller or colour the minus sign, without splitting strings yourself.
rupees.formatToParts(-1234.5);
// [{ type: "minusSign", value: "-" }, { type: "currency", value: "₹" }, { type: "integer", value: "1" }, ...]Try it yourself
- 1
Add your own currency
In the first example, add a line that shows
nin Japanese yen with theja-JPlocale. Predict: how many decimals does yen get? - 2
Change the clock
In the second example, add a row for
"America/New_York". In January New York is 5 hours behind UTC. Predict the time, then run it. - 3
Try another language
In the third example, make the relative-time formatter Hindi (
"hi") and run it. How is "yesterday" written?
Code & diagrams
`-u-nu-deva` is a Unicode extension in the locale tag: it asks for Devanagari digits. Decimal style shows up to 3 fraction digits by default; currency uses the currency's own (2 for rupees).
Preview
Console
Nothing logged yet.Expected console output
en-IN: 12,34,567.891
en-US: 1,234,567.891
de-DE: 1.234.567,891
INR: ₹12,34,567.89
USD: $1,234,567.89
compact US: 1.2M
compact IN: 12L
percent: 26%
unit: 50 km/h
always sign: +5
2 decimals: 5.00
Devanagari: १२,३४५
toLocaleString: ₹1,999.50Every formatter here names its `timeZone`, so the output is the same on every computer. Leave `timeZone` out and each visitor sees the moment on their own clock.
Preview
Console
Nothing logged yet.Expected console output
US, UTC: 1/15/2024
UK, UTC: 15/01/2024
full: Monday, January 15, 2024
medium + short: 15 Jan 2024, 10:30
India: 15 Jan 2024, 4:00 pm
Tokyo, 24h: 19:30
parts: Mon, January 15, 2024
Hindi: सोमवार, 15 जनवरी 2024
shortcut: 15.1.2024The default `sort` compares UTF-16 code units, so capitals come first and `item10` before `item2`. The collator sorts like a person would. `0` from `localeCompare` means "equal" at base sensitivity (accents ignored).
Preview
Console
Nothing logged yet.Expected console output
yesterday | tomorrow | 3 weeks ago
1 day ago
tea, coffee, and juice
tea or coffee
0 items | 1 item | 2 items
[ 'Item1', 'item10', 'item2' ]
[ 'Item1', 'item2', 'item10' ]
0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Currency style without a currency
Write new Intl.NumberFormat("en-IN", { style: "currency" }).format(10);.
Break #2
Compare formatted text in a test
Assert new Intl.NumberFormat("fr-FR").format(1234) === "1 234" (typed with a normal space).
Break #3
Forget the time zone
Show an order time with moment.toLocaleString("en-IN") on a server running in UTC, for customers in India.
Myth vs fact
Myth
toFixed(2) plus a symbol is enough for prices.
Fact
It ignores grouping, symbol position, spacing and decimal marks. Intl.NumberFormat gets all of those right for every locale.
Myth
A date's format only depends on the language.
Fact
It depends on the region too: en-US writes 1/15/2024, en-GB writes 15/01/2024, and en-IN writes 15/1/2024.
Myth
Formatted output is stable, so it's safe to snapshot in tests.
Fact
Browsers update their locale data. Fix the locale and time zone, and prefer testing values or parts rather than whole strings.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Intlis defined by a separate standard, ECMA-402, which also gets a new edition every year. Browsers implement it with ICU (International Components for Unicode) and the CLDR locale database, so new locales and fixes arrive with browser updates. Node.js ships full ICU since version 13. - ▸
Constructing a formatter is expensive (it loads locale data), while
formatis cheap.toLocaleStringon numbers and dates creates a formatter each time unless the engine caches it. Cache formatters in a module-levelMapkeyed by locale and options. - ▸
resolvedOptions()shows what the formatter really chose: the matched locale, the time zone, the numbering system.Intl.DateTimeFormat().resolvedOptions().timeZoneis the standard way to read the user's time zone.Intl.supportedValuesOf("timeZone")lists every zone the browser knows. - ▸
ECMA-402 2023 added
formatRangefor numbers (dates had it earlier):new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).formatRange(3, 5)gives$3.00 – $5.00, with locale-correct dashes and spaces.Intl.DurationFormat(2025) formats lengths of time like "1 hr, 30 min".
Remember this
- 1
Formatting means turning a value into text for people to read. It's different from storing or calculating: keep the real number or date in your code and format it only when showing it. Formatting depends on the locale: a code naming a language and usually a region, written as a BCP 47 language tag such as
"en-IN"(English, India),"en-US","de-DE","hi-IN"or"ja-JP". If you leave the locale out, the browser uses the user's own setting (navigator.language), which is usually what you want on a real site, but makes output differ from machine to machine. - 2
**
Intl.NumberFormatformats numbers. Create a formatter with a locale and options**, then callformat(value).stylechooses the kind:"decimal"(the default),"currency"(needs acurrencycode like"INR","USD","EUR"),"percent"(multiplies by 100:0.256shows as26%) or"unit"(withunit: "kilometer-per-hour","megabyte","liter"and so on). Control decimals withminimumFractionDigitsandmaximumFractionDigits; show big numbers short withnotation: "compact"(1.2M, or12Lfor lakh inen-IN); and force a plus sign withsignDisplay: "always". Indian grouping (12,34,567) comes automatically fromen-IN. - 3
**
num.toLocaleString(locale, options)** is a shortcut that takes the same options:(1999.5).toLocaleString("en-IN", { style: "currency", currency: "INR" }). It's handy once, but it builds a new formatter on every call. In a loop or a big table, create oneIntl.NumberFormatand reuse itsformatmethod: it can be many times faster. - 4
**
Intl.DateTimeFormat** formats aDate(Topic 4.6). The quick way isdateStyleandtimeStyle, each"full","long","medium"or"short". For exact control, list the parts you want instead:weekday,year,month("numeric","2-digit","short","long"),day,hour,minute,second,timeZoneName. The two ways can't be mixed in one formatter (that throws aTypeError). **timeZone** decides which clock to show:"UTC","Asia/Kolkata","America/New_York". Without it, the user's own time zone is used.hourCycle: "h23"gives a 24-hour clock.date.toLocaleDateString(),toLocaleTimeString()andtoLocaleString()are the shortcuts. - 5
**More
Intlhelpers.**Intl.RelativeTimeFormatwrites "3 days ago" or "in 2 hours", and withnumeric: "auto"says "yesterday" and "tomorrow".Intl.ListFormatjoins lists with the right word and commas ("tea, coffee, and juice").Intl.PluralRulestells you which plural form a number needs ("one"or"other"in English; some languages have six forms), so you never write "1 items".Intl.Collatorandstr.localeComparecompare strings by a language's alphabet: they puténext toe, and withnumeric: truesortitem2beforeitem10. - 6
Rules for reliable formatting. Format at the very end, and never parse formatted text back into numbers ("1.234" means one thousand in German but one point two in English). Don't compare formatted strings in tests unless you fix the locale and time zone, because the browser's locale data (called ICU and CLDR) is updated over time and has changed details like which space goes before "AM". Some locales use special spaces: French groups thousands with a narrow no-break space (
U+202F), and German puts a no-break space (U+00A0) before€. They look like spaces but aren't" ".
Explain it without notes
Why should formatting happen only at display time?
What does a locale like en-IN control in Intl.NumberFormat?
Why can two computers show different results for date.toLocaleString(), and how do you make them match?
When would you use Intl.Collator instead of the default sort?
Practice
Write formatINR(paise) that takes whole paise and returns rupees formatted for India, so formatINR(123456789) returns ₹12,34,567.89.
Write fileSize(bytes) that shows 512 B, 1.5 kB or 2.3 MB with Intl.NumberFormat and its unit style (use 1000 per step and one decimal).
Write timeAgo(minutes) that returns text like 5 minutes ago or 2 hours ago with Intl.RelativeTimeFormat, switching to hours from 60 minutes.
Trade-offs
- ↔
Relying on the user's default locale and time zone shows each person their own format with zero code, but makes output differ between machines; fixing them makes output predictable but may not match what the visitor expects.
- ↔
Intlis built in, fast once cached and covers hundreds of locales; date libraries add custom tokens like"DD/MM/YYYY"but add download size and can drift from the browser's locale data.
Done when you can
I can format numbers, currencies, percentages and units with
Intl.NumberFormat.I can format a date with
dateStyle/timeStyleor chosen parts, in a namedtimeZone.I create formatters once and reuse them.
I can write relative times, lists and plurals with
Intlhelpers.I can sort strings for people with
Intl.CollatororlocaleCompare.I keep values unformatted until display and don't compare formatted strings in tests.