Command Palette

Search for a command to run...

Hectal
PHASE 4Beginner ~28 min· topic 5 of 6

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.

money.jswhole filejavascript
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.89

02Choose 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.

money.jswhole filejavascript
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.

time.jswhole filejavascript
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 pm
Format a moment in a chosen time zonediagram
Rendering diagram…

04Say it like a person

Relative times, lists and plurals make interfaces read naturally, in every language, without hand-written rules.

words.jswhole filejavascript
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 juice

05Get 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.

money.jswhole filejavascript
rupees.formatToParts(-1234.5);
// [{ type: "minusSign", value: "-" }, { type: "currency", value: "₹" }, { type: "integer", value: "1" }, ...]

Try it yourself

  1. 1

    Add your own currency

    In the first example, add a line that shows n in Japanese yen with the ja-JP locale. Predict: how many decimals does yen get?

  2. 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. 3

    Try another language

    In the third example, make the relative-time formatter Hindi ("hi") and run it. How is "yesterday" written?

Code & diagrams

One number, many locales and styles New tab

`-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

Press Run to see the page.

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.50
Dates and times on different clocks New tab

Every 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

Press Run to see the page.

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.2024
Relative times, lists, plurals and sorting New tab

The 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

Press Run to see the page.

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' ]
0

Break 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);.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Currency code is required with currency style.

Break #2

Compare formatted text in a test

Assert new Intl.NumberFormat("fr-FR").format(1234) === "1 234" (typed with a normal space).

terminal
$ Console
── what you'll see ──
false

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.

terminal
$ the page shows
── what you'll see ──
15/1/2024, 10:30:00 am (the customer placed the order at 4:00 pm their time)

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.

  • ▸

    Intl is 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 format is cheap. toLocaleString on numbers and dates creates a formatter each time unless the engine caches it. Cache formatters in a module-level Map keyed by locale and options.

  • ▸

    resolvedOptions() shows what the formatter really chose: the matched locale, the time zone, the numbering system. Intl.DateTimeFormat().resolvedOptions().timeZone is the standard way to read the user's time zone. Intl.supportedValuesOf("timeZone") lists every zone the browser knows.

  • ▸

    ECMA-402 2023 added formatRange for 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. 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. 2

    **Intl.NumberFormat formats numbers. Create a formatter with a locale and options**, then call format(value). style chooses the kind: "decimal" (the default), "currency" (needs a currency code like "INR", "USD", "EUR"), "percent" (multiplies by 100: 0.256 shows as 26%) or "unit" (with unit: "kilometer-per-hour", "megabyte", "liter" and so on). Control decimals with minimumFractionDigits and maximumFractionDigits; show big numbers short with notation: "compact" (1.2M, or 12L for lakh in en-IN); and force a plus sign with signDisplay: "always". Indian grouping (12,34,567) comes automatically from en-IN.

  3. 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 one Intl.NumberFormat and reuse its format method: it can be many times faster.

  4. 4

    **Intl.DateTimeFormat** formats a Date (Topic 4.6). The quick way is dateStyle and timeStyle, 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 a TypeError). **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() and toLocaleString() are the shortcuts.

  5. 5

    **More Intl helpers.** Intl.RelativeTimeFormat writes "3 days ago" or "in 2 hours", and with numeric: "auto" says "yesterday" and "tomorrow". Intl.ListFormat joins lists with the right word and commas ("tea, coffee, and juice"). Intl.PluralRules tells you which plural form a number needs ("one" or "other" in English; some languages have six forms), so you never write "1 items". Intl.Collator and str.localeCompare compare strings by a language's alphabet: they put é next to e, and with numeric: true sort item2 before item10.

  6. 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

01

Why should formatting happen only at display time?

02

What does a locale like en-IN control in Intl.NumberFormat?

03

Why can two computers show different results for date.toLocaleString(), and how do you make them match?

04

When would you use Intl.Collator instead of the default sort?

Practice

01

Write formatINR(paise) that takes whole paise and returns rupees formatted for India, so formatINR(123456789) returns ₹12,34,567.89.

02

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).

03

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.

  • ↔

    Intl is 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/timeStyle or chosen parts, in a named timeZone.

  • I create formatters once and reuse them.

  • I can write relative times, lists and plurals with Intl helpers.

  • I can sort strings for people with Intl.Collator or localeCompare.

  • I keep values unformatted until display and don't compare formatted strings in tests.