All lessons

Lesson 03·Unit 2 · Three failures you can see·Intl.* APIs and CLDR data·Lv 101·~8 min

Dates, times, numbers & currency

1,234.56 in the US. 1.234,56 in Germany. 1'234.56 in Switzerland. 1,23,456 in India. ¥1,235 in Japan (no fractional digits). One number, twenty locales; Intl.* and CLDR already know the rules.

Intl.NumberFormatIntl.DateTimeFormatIntl.RelativeTimeFormat

By the end

  • ·Format money with each currency's own minor units, so JPY rounds to whole yen without you hardcoding two decimals.
  • ·Explain why one options object renders differently everywhere: field order, digits, calendars, and grouping are CLDR data, not styling.
  • ·Predict the characters Intl emits, including fr-FR's narrow no-break space, and stop hand-typing expected bytes into tests.

The problem

A signup page that says $1,234.56 on 12/31/2024 reads as a typo in Berlin, as an error in Tokyo, and is illegible in Riyadh. Currency precision varies (JPY = 0 decimal places, BHD = 3), thousand separators differ (, vs . vs '), digits themselves can be Latin or Arabic-Indic. Get this wrong and you lose users before they reach the conversion funnel, and you'll never see the bounce in your funnel attribution.

How it works

You request fields, and the locale decides everything else

Intl.DateTimeFormat has no format string. That is a design decision. You declare which fields you need (a date, a time, at what verbosity), and CLDR supplies the locale's own pattern: the field order, the separators, the digits, even the calendar. The API's contract is "tell me what to say, not how to say it".

That is why one options object renders as 4/21/26, 6:30 PM in en-US and 21.04.26, 18:30 in de-DE: month-first versus day-first, 12-hour versus 24-hour, slash versus dot, all locale data. The differences go past the cosmetic: ar-EG writes the same date in Arabic-Indic digits by default, and th-TH renders 2026 as 2569 because Thailand's default calendar is Buddhist. A hand-rolled MM/DD/YY template gets every one of those wrong, silently.

Numbers follow the same contract. Grouping (1,234,567 versus India's lakh-crore 12,34,567), decimal separators, currency symbol position, and compact notation (1.2M in English, 123.5万 in Japanese, 12.3 लाख in Hindi): CLDR ships every one of these per locale. Keep amounts as numbers until the last possible moment, then let the formatter place every character, including the ones you did not know existed.

How it works

The week is locale data too

Formatting a date is only half the calendar. The other half is the grid it sits in, and that grid is locale data. Intl.Locale(...).weekInfo reports the first day of the week and which days are the weekend. Both vary widely: the United States starts the week on Sunday, most of Europe on Monday, and much of the Middle East on Saturday with a Friday-Saturday weekend.

This is the field a hand-built date picker or a "this weekend" reminder gets wrong. Hardcode a Sunday-first grid with a Saturday-Sunday weekend and it is incorrect for a Cairo or Riyadh user. The week strip in the demo re-lays-out per locale so you can watch the start column and the shaded weekend move. Engines still disagree on the API shape, some exposing weekInfo as a getter and newer ones a getWeekInfo() method, so feature-detect both.

How it works

Intl formats units, but will not convert or choose them

Intl.NumberFormat with a unit style renders 5 km or 3.1 mi in the locale's own way, and it is easy to assume it handles measurement for you. It does not. Pass unit: "kilometer" to an en-US formatter and you get 5 km, while a US reader expects miles. The formatter localizes the label and never converts the quantity.

Two decisions stay with your code. First, which system a locale uses: only the United States, Liberia, and Myanmar are US-customary, the UK is mixed (miles on the road, Celsius on the forecast), and nearly everywhere else is metric. CLDR records this, but Intl does not expose it, so you map it yourself. Second, the conversion (kilometers to miles, Celsius to Fahrenheit) is plain arithmetic you run before formatting. The demo's converter formats both figures with Intl and runs the arithmetic by hand in between.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  1. 1
    In the money section, switch the currency from USD to JPY, keeping the same amount. The fraction digits disappear and the value rounds (¥ has 0 minor units: there are no “cents” of yen). Then read the minor-units callout: BHD has three.
  2. 2
    Set the locale to ar-EG and re-read every output. Digits become Arabic-Indic (١٢٣…), separators change, and the resolved BCP-47 tag at the top shows what the negotiation produced.
  3. 3
    Set the calendar dropdown to “Buddhist (Thai)”. The year jumps to 2569: same instant, different calendar. Users in Thailand see Buddhist-era years in government and daily contexts.
  4. 4
    In the four-locale hero, compare en-US and en-IN for the same amount. Same language, different grouping: 1,234,567.89 vs 12,34,567.89. India groups by lakh and crore. Grouping is locale data: the shared language does not decide it.
  5. 5
    In the locale matrix (Date & time mode), set Date style to none and Time style to short, and read the rows. Then force Hour cycle to h23. Under locale default, en-US reads 6:30 PM while en-GB, de-DE, and ja-JP read 18:30. The hour cycle is locale data. Forcing h23 flattens every row to 24-hour: the locale supplies the default, and the override comes from you.
  6. 6
    Switch the locale matrix to Currency mode and step Symbol display through symbol → code → name. One amount, three audiences: a symbol for UI, an unambiguous code (JPY) for finance exports, and a spelled-out name in each row's own language (“Japanische Yen” in de-DE) for screen readers and legal text.

Resolved BCP-47 tag: en-US

The same number, four locales that look nothing alike

en-US

$1,234,567.89

US (baseline)

en-IN

$12,34,567.89

Indian grouping

de-CH

$ 1'234'567.89

Swiss apostrophe

ar-EG

‏١٬٢٣٤٬٥٦٧٫٨٩ US$

Arabic-Indic digits

The locale matrix: flip a toggle, watch eleven locales react

One option set drives every row. This is the point of the fields-not- format-strings contract: you change what to say, each locale re-decides how.

LocaleOutput
en-USApr 21, 2026, 6:30 PM
en-GB21 Apr 2026, 18:30
de-DE21.04.2026, 18:30
fr-FR21 avr. 2026, 18:30
pt-BR21 de abr. de 2026, 18:30
ru-RU21 апр. 2026 г., 18:30
hi-IN21 अप्रैल 2026, 6:30 pm
th-TH21 เม.ย. 2569 18:30
ja-JP2026/04/21 18:30
zh-CN2026年4月21日 18:30
ar-EG٢١‏/٠٤‏/٢٠٢٦، ٦:٣٠ م

Same instant in every row (pinned to UTC). Only the presentation moves. Watch the field order (4/21 vs 21.04 vs 2026/04/21) and the hour cycle (en-US says 6:30 PM where en-GB says 18:30). Also watch th-TH's year: Thailand's default calendar is Buddhist, so 2026 renders as 2569.

Numbers & currency

StyleOutput
Decimal1,234,567.89
Currency$1,234,567.89
Compact1.2M
Percent (0.7321)73.21%

Minor-units trap (ISO 4217 e-table)

Most currencies have 2 decimals, but JPY, KRW, VND, CLP have 0 and BHD, KWD, OMR, JOD have 3. Suppose your payment code stores amounts in "minor units" (cents) and multiplies by 100. Then you overcharge Japanese customers 100× and undercharge Bahrainis 10×.

USD · 2 digits
1234567.89 → 123456789
JPY · 0 digits
1234567.89 → 1234568
BHD · 3 digits
1234567.89 → 1234567890

Use new Intl.NumberFormat(locale, {style:'currency',currency}).resolvedOptions().minimumFractionDigits to read the correct count.

Units

StyleOutput
Unit (long)42.5 kilometers
Unit (narrow)42.5km

Dates & times

StyleOutput
short4/21/26
mediumApr 21, 2026
longApril 21, 2026
full + timeTuesday, April 21, 2026 at 6:30:00 PM UTC

Relative time

StyleOutput
auto3 days ago

List formatting

StyleOutput
Conjunctionapples, oranges, and pears
Disjunctionapples, oranges, or pears

Display names

StyleOutput
Country 'DE'Germany
Language 'zh-Hant'Traditional Chinese
Currency 'JPY'Japanese Yen

Segmenter (graphemes & words)

Input

"👩‍👩‍👧‍👦 こんにちは"

Graphemes (7)

👩‍👩‍👧‍👦

Words from "Hello, 世界! How are 你?"

Hello世界Howare

The Chinese characters break into individual words: ICU's CJK word-boundary rules at work.

The same amount, formatted for every supported locale

en-US$1,234,567.89
en-GBUS$1,234,567.89
en-IN$12,34,567.89
de-DE1.234.567,89 $
de-CH$ 1'234'567.89
fr-FR1 234 567,89 $US
fr-CA1 234 567,89 $ US
es-ES1.234.567,89 US$
pt-BRUS$ 1.234.567,89
it-IT1.234.567,89 USD
nl-NLUS$ 1.234.567,89
ru-RU1 234 567,89 $
pl-PL1 234 567,89 USD
zh-CNUS$1,234,567.89
zh-TWUS$1,234,567.89
ja-JP$1,234,567.89
ko-KRUS$1,234,567.89
th-THUS$1,234,567.89
hi-IN$12,34,567.89
ar-SA‏١٬٢٣٤٬٥٦٧٫٨٩ US$
ar-EG‏١٬٢٣٤٬٥٦٧٫٨٩ US$
he-IL‏1,234,567.89 ‏$
fa-IR‎$۱٬۲۳۴٬۵۶۷٫۸۹
ur-PK$1,234,567.89

First day of the week & weekend

Which day a week starts on, and which days are the weekend, are locale data (Intl.Locale.weekInfo), not universal. A calendar grid or a "this weekend" label that hardcodes Sunday-start / Sat-Sun is wrong across much of the world.

startSun
Mon
Tue
Wed
Thu
Fri
Sat

The week starts on Sun. The weekend is Sat + Sun. (The US starts Sunday, most of Europe Monday. Much of the Middle East starts Saturday, with a Friday-Saturday weekend.)

Measurement systems

Intl formats the unit you hand it, but it never converts a value or chooses a system. That decision is yours: map the locale to a system, convert, then format.

5 km=3.1 mi

The current locale en-US maps to US customary. Intl formatted both figures above. The conversion between them was plain arithmetic that no formatter does for you.

If you remember one thing

Never hand-format numbers, dates, or money. Pass a locale and options to Intl.* and ship exactly what it returns, including the characters you did not expect.

What to do about it

  • Never roll your own formatter. Always use Intl.NumberFormat, Intl.DateTimeFormat, and Intl.RelativeTimeFormat against the user's negotiated locale. They handle currency precision, digit substitution, calendar systems, and DST.
  • For currency, pass { style: "currency", currency: "JPY" } and let the API decide the digit count. Don't hardcode .toFixed(2).
  • Switch the BCP-47 tag's u-ca- extension to support non-Gregorian calendars (Japanese imperial, Hijri, Buddhist) when launching in regions where those are the civil calendar.
  • Use Intl.Segmenter with granularity: "word" to find word boundaries in Chinese, Japanese, and Thai (no whitespace required).

Use this with

Stakeholders

EngineeringDesignProduct

Moments

  • ·Designing a checkout, invoice, or analytics dashboard
  • ·Launching into a market with a non-Gregorian calendar
  • ·Component library review

Field note

A common silent failure: French number output uses U+202F (narrow no-break space) as the group separator in “1 234,56”, instead of a regular space. Screenshot diffs pass, string comparisons in tests fail, and a well-meaning engineer “fixes” it with replace(' ', ''), which does not match either. A test that asserts against hand-typed expected output asserts the wrong bytes.

MDN: Intl.NumberFormat

Quick check

3 questions · pass at 2+

  1. Question 1/3

    What does new Intl.NumberFormat('ja-JP', {style:'currency', currency:'JPY'}).format(1234.56) produce?

  2. Question 2/3

    How does en-IN (English, India) group the number 1234567?

  3. Question 3/3

    Which character does fr-FR use as the thousands separator in Intl output?

Words you'll hear

Where to read more

Related lessons