Skip to content
Locale Lab

Chapter 03·Three visible failures·~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

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. Nothing crashes when the convention is wrong: the page renders, the numbers read wrong, and nothing is logged.

How it works

The caller requests fields, and the locale decides everything else

Intl.DateTimeFormat has no format string. The caller declares which fields it needs (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.

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, with one fraction digit allowed): 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 few engineers know exist.

The week is locale data too

Formatting a date is only half the calendar. The other half is the grid it sits in. 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 the start column and the shaded weekend visibly move. Engines still disagree on the API shape, some exposing weekInfo as a getter and newer ones a getWeekInfo() method, so feature-detect both.

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 end to end. It does not. Pass unit: "kilometer" to an en-US formatter and the output is 5 km, while a US reader expects miles. The formatter localizes the label and never converts the quantity.

Two decisions stay with application 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. Second, the conversion (kilometers to miles, Celsius to Fahrenheit) is plain arithmetic that runs before formatting. The demo's converter formats both figures with Intl and runs the arithmetic by hand in between.

The demo

Try these, in order

Each step reproduces one specific failure in the demo below.

  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, and grouping follows the locale rather than the language.
  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. Forcing h23 flattens every row to 24-hour: the locale supplies the hour cycle, and the override comes from the caller.
  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. That is the fields-not-format-strings contract: the caller changes what to say, each locale re-decides how.

LocaleOutputWhat moved
en-USApr 21, 2026, 6:30 PMmonth first, 12-hour
en-GB21 Apr 2026, 18:30day first, 24-hour
de-DE21.04.2026, 18:30dots group, comma decimal
fr-FR21 avr. 2026, 18:30narrow no-break spaces
pt-BR21 de abr. de 2026, 18:30
ru-RU21 апр. 2026 г., 18:30
hi-IN21 अप्रैल 2026, 6:30 pmlakh/crore grouping
th-TH21 เม.ย. 2569 18:30Buddhist calendar by default
ja-JP2026/04/21 18:30year first
zh-CN2026年4月21日 18:30year first
ar-EG٢١‏/٠٤‏/٢٠٢٦، ٦:٣٠ مArabic-Indic digits, RTL

Same instant in every row, pinned to UTC. 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). th-TH's year reads 2569: Thailand's default calendar is Buddhist.

Numbers & currency

StyleOutputCode
Decimal1,234,567.89NumberFormat()
Currency$1,234,567.89NumberFormat({ style: 'currency', currency: 'USD' })
Compact1.2MNumberFormat({ notation: 'compact', maximumFractionDigits: 1 })
Percent (0.7321)73.21%NumberFormat({ style: 'percent' })

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. Payment code that stores amounts in "minor units" (cents) by multiplying by 100 overcharges Japanese customers 100× and undercharges 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

StyleOutputCode
Unit (long)42.5 kilometersNumberFormat({ style: 'unit', unitDisplay: 'long' })
Unit (narrow)42.5kmNumberFormat({ style: 'unit', unitDisplay: 'narrow' })

Dates & times

StyleOutputCode
short4/21/26{ dateStyle: 'short' }
mediumApr 21, 2026{ dateStyle: 'medium' }
longApril 21, 2026{ dateStyle: 'long' }
full + timeTuesday, April 21, 2026 at 6:30:00 PM UTC{ dateStyle: 'full', timeStyle: 'long' }

Relative time

StyleOutputCode
auto3 days agoRelativeTimeFormat({ numeric: 'auto' })

List formatting

StyleOutputCode
Conjunctionapples, oranges, and pearsListFormat({ type: 'conjunction' })
Disjunctionapples, oranges, or pearsListFormat({ type: 'disjunction' })

Display names

StyleOutputCode
Country 'DE'GermanyDisplayNames({ type: 'region' })
Language 'zh-Hant'Traditional ChineseDisplayNames({ type: 'language' })
Currency 'JPY'Japanese YenDisplayNames({ type: 'currency' })

Segmenter (graphemes & words)

Input

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

Graphemes (7)

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

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

Hello世界Howare你

ICU's CJK word-boundary rules break the Chinese characters into individual words.

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). A calendar grid or a "this weekend" label that hardcodes Sunday-start / Sat-Sun is wrong in Germany (Monday start) and in Saudi Arabia (Friday–Saturday weekend).

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

Measurement systems

Intl formats the unit it is handed, but it never converts a value or chooses a system. That decision stays with application code: map the locale to a system, convert, then format.

5 km=3.1 mi

The current locale en-US maps to US customary.

The short version

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

What to do about it

  • Never hand-roll a 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 eras, 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).

Where this comes up

Who it concerns

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 ↗

Terms in this chapter

Where to read more

Related chapters