Intl
Which parts of the ECMA-402 Intl API Cruft supports and how far each locale is covered. Number formatting, collation, plural rules, and segmentation are reliable for major locales; an unsupported locale silently falls back to English-shaped output rather than throwing, and this page maps that boundary.
Cruft ships a selected slice of ECMA-402 Intl, backed by its own locale data. There is no ICU library and no full CLDR/UCA database vendored into the runtime. Instead, each service carries its own curated tables, and the locale coverage differs from service to service. Where Cruft owns the data for a locale, formatting is genuinely locale-correct (German thousands separators, Arabic-Indic digits, Japanese era-free date order). Where it does not, an accepted locale can silently fall back to English-shaped output rather than throwing. This page maps that boundary.
Constructor status
| Constructor | Status | Locale coverage |
|---|---|---|
Intl.NumberFormat | Works: decimal, currency, percent, unit | Broad. Grouping/decimal/digits correct for most major locales; a few silently fall back (see nl-NL) |
Intl.DateTimeFormat | Works: dateStyle/timeStyle, explicit fields, timeZone | Narrow for localized month/weekday names. Time-zone math is broad |
Intl.Collator | Works: compare, sensitivity | Locale-sensitive ordering for de, sv, en |
Intl.PluralRules | Works: cardinal + ordinal select | Broad (en, pl, ar CLDR categories correct) |
Intl.RelativeTimeFormat | Works: format, numeric:"auto" | de/es/fr idiomatic forms |
Intl.Segmenter | Works: grapheme, word, sentence | Grapheme/sentence broad; word segmentation is space-based (no CJK dictionary) |
Intl.ListFormat | Works: conjunction, disjunction | en, de |
Intl.DisplayNames | Works: language, region, currency | Resolves real names (see note) |
Intl.Locale | Works: tag component getters | Structural, locale-independent |
Intl.getCanonicalLocales | Works | Canonicalizes tags |
Intl.supportedValuesOf | Works: currency, calendar, timeZone | Returns curated lists |
Intl.NumberFormat
Decimal grouping and the decimal separator follow the locale, and this coverage is broad:
$ cruft -e 'console.log(new Intl.NumberFormat("en-US").format(1234567.89))'
1,234,567.89
$ cruft -e 'console.log(new Intl.NumberFormat("de-DE").format(1234567.89))'
1.234.567,89
$ cruft -e 'console.log(new Intl.NumberFormat("fr-FR").format(1234567.89))'
1 234 567,89
$ cruft -e 'console.log(new Intl.NumberFormat("hi-IN").format(1234567.89))'
12,34,567.89
Non-Latin digit systems are genuine. Arabic and Persian locales emit their native digits rather than transliterated ASCII:
$ cruft -e 'console.log(new Intl.NumberFormat("ar").format(1234567.89))'
١٬٢٣٤٬٥٦٧٫٨٩
$ cruft -e 'console.log(new Intl.NumberFormat("fa-IR").format(1234567.89))'
۱٬۲۳۴٬۵۶۷٫۸۹
$ cruft -e 'console.log(new Intl.NumberFormat("bn-BD").format(1234567.89))'
১২,৩৪,৫৬৭.৮৯
Currency, percent, and unit styles resolve the locale-appropriate symbol and placement:
$ cruft -e 'console.log(new Intl.NumberFormat("de-DE",{style:"currency",currency:"EUR"}).format(1234.5))'
1.234,50 €
$ cruft -e 'console.log(new Intl.NumberFormat("ja-JP",{style:"currency",currency:"JPY"}).format(1234.5))'
¥1,235
$ cruft -e 'console.log(new Intl.NumberFormat("de-DE",{style:"percent"}).format(0.456))'
46 %
$ cruft -e 'console.log(new Intl.NumberFormat("de-DE",{style:"unit",unit:"kilometer-per-hour"}).format(50))'
50 km/h
Fallback boundary. Most major locales are correct, but a locale not in the number table silently uses English-shaped grouping instead of throwing. Dutch is one such case: nl-NL should group with . and use , as the decimal, but Cruft emits the en pattern:
$ cruft -e 'console.log(new Intl.NumberFormat("nl-NL").format(1234567.89))'
1,234,567.89 # Node/ICU: 1.234.567,89
Verify the exact locale you depend on before relying on its number shape.
Intl.DateTimeFormat
dateStyle/timeStyle presets and explicit field options both work, and timeZone conversion is broad and accurate:
$ cruft -e 'console.log(new Intl.DateTimeFormat("en-US",{dateStyle:"full",timeStyle:"short",timeZone:"UTC"}).format(new Date("2026-06-20T15:30:00Z")))'
Saturday, June 20, 2026 at 3:30 PM
$ cruft -e 'console.log(new Intl.DateTimeFormat("de-DE",{dateStyle:"full",timeZone:"UTC"}).format(new Date("2026-06-20T15:30:00Z")))'
Samstag, 20. Juni 2026
$ cruft -e 'console.log(new Intl.DateTimeFormat("ja-JP",{dateStyle:"full",timeZone:"UTC"}).format(new Date("2026-06-20T15:30:00Z")))'
2026年6月20日土曜日
$ cruft -e 'console.log(new Intl.DateTimeFormat("en-US",{timeZone:"Asia/Tokyo",hour:"2-digit",minute:"2-digit"}).format(new Date("2026-06-20T15:30:00Z")))'
12:30 AM
Fallback boundary (the main one for dates). Localized month and weekday names are the narrowest table in the Intl surface. Explicit month:"long" produces localized names only for a small set (en, de, fr, ja, ru); other accepted locales silently return the English name:
$ cruft -e 'console.log(new Intl.DateTimeFormat("ru-RU",{month:"long",timeZone:"UTC"}).format(new Date("2026-06-20T00:00:00Z")))'
июнь # localized (Russian month names now covered)
$ cruft -e 'console.log(new Intl.DateTimeFormat("it-IT",{month:"long",timeZone:"UTC"}).format(new Date("2026-06-20T00:00:00Z")))'
June # Node/ICU: giugno
Weekday-name coverage is slightly wider than month-name coverage (es, it, and ar localize the weekday but fall back for the month), so coverage is uneven even within one service. The numeric fields (year, day, hour, minute) and the time-zone math are correct regardless of locale; only the spelled-out names fall back.
Intl.Collator
Locale-sensitive ordering is real. German sorts the umlaut next to its base letter; Swedish sorts ö after z:
$ cruft -e 'console.log(["z","a","ä"].sort(new Intl.Collator("de").compare))'
[ 'a', 'ä', 'z' ]
$ cruft -e 'console.log(new Intl.Collator("sv").compare("z","ö"))'
-1
$ cruft -e 'console.log(new Intl.Collator("en",{sensitivity:"base"}).compare("a","A"))'
0
Intl.PluralRules
Cardinal and ordinal CLDR categories are correct across locales, including languages with rich plural systems (Polish few/many, Arabic zero/one/two/few):
$ cruft -e 'const p=new Intl.PluralRules("pl");console.log(p.select(1),p.select(2),p.select(5))'
one few many
$ cruft -e 'const p=new Intl.PluralRules("ar");console.log(p.select(0),p.select(1),p.select(2),p.select(6),p.select(100))'
zero one two few other
$ cruft -e 'const p=new Intl.PluralRules("en",{type:"ordinal"});console.log(p.select(1),p.select(2),p.select(3),p.select(4))'
one two few other
Intl.RelativeTimeFormat
format renders durations, and numeric:"auto" uses idiomatic words where the locale has them:
$ cruft -e 'console.log(new Intl.RelativeTimeFormat("en",{numeric:"auto"}).format(-1,"day"))'
yesterday
$ cruft -e 'console.log(new Intl.RelativeTimeFormat("de",{numeric:"auto"}).format(-1,"day"))'
gestern
$ cruft -e 'console.log(new Intl.RelativeTimeFormat("fr",{numeric:"auto"}).format(-1,"day"))'
hier
$ cruft -e 'console.log(new Intl.RelativeTimeFormat("es").format(2,"month"))'
dentro de 2 meses
Intl.Segmenter
Grapheme segmentation is Unicode-aware (an emoji stays one segment), and sentence segmentation works. Word segmentation is whitespace/punctuation based: it splits English correctly but does not carry a CJK word dictionary, so a Japanese run stays one segment:
$ cruft -e 'const s=new Intl.Segmenter("en",{granularity:"grapheme"});console.log([...s.segment("a👍b")].map(x=>x.segment))'
[ 'a', '👍', 'b' ]
$ cruft -e 'const s=new Intl.Segmenter("en",{granularity:"word"});console.log([...s.segment("Hello world foo")].filter(x=>x.isWordLike).map(x=>x.segment))'
[ 'Hello', 'world', 'foo' ]
$ cruft -e 'const s=new Intl.Segmenter("ja",{granularity:"word"});console.log([...s.segment("東京都")].map(x=>x.segment))'
[ '東京都' ] # ICU with a dictionary would split into words
Intl.ListFormat and Intl.DisplayNames
ListFormat joins with the locale connector for conjunction and disjunction:
$ cruft -e 'console.log(new Intl.ListFormat("en",{type:"conjunction"}).format(["a","b","c"]))'
a, b, and c
$ cruft -e 'console.log(new Intl.ListFormat("de",{type:"conjunction"}).format(["a","b","c"]))'
a, b und c
DisplayNames resolves codes to human-readable names for language, region, and currency types (an earlier build shipped this as a code-passthrough stub; it now returns real names):
$ cruft -e 'console.log(new Intl.DisplayNames("en",{type:"language"}).of("de"))'
German
$ cruft -e 'console.log(new Intl.DisplayNames("en",{type:"region"}).of("US"))'
United States
$ cruft -e 'console.log(new Intl.DisplayNames("en",{type:"currency"}).of("USD"))'
US Dollar
Static helpers
$ cruft -e 'console.log(Intl.getCanonicalLocales(["EN-us","Fr"]))'
[ 'en-US', 'fr' ]
$ cruft -e 'console.log(Intl.supportedValuesOf("currency").slice(0,5))'
[ 'ADP', 'AED', 'AFA', 'AFN', 'ALK' ]
$ cruft -e 'console.log(Intl.supportedValuesOf("calendar").slice(0,4))'
[ 'buddhist', 'chinese', 'coptic', 'dangi' ]
Intl.Locale parses a BCP-47 tag into components (language, region, baseName). Note the Intl namespace properties are non-enumerable, so Object.keys(Intl) returns [] by design.
Practical guidance
- Number, plural, collation, relative-time, list: broad and reliable for the major locales. Safe to depend on for common European and East-Asian tags.
- Date/time spelled-out names: verify your locale. Only a small set localizes month/weekday names; others silently emit English. Numeric date fields and time-zone conversion are locale-independent and correct.
- Any locale you ship against: run the one-liner and confirm real locale-specific output rather than assuming ICU-equivalent breadth. An accepted locale that produces English shapes is a coverage gap, not an error.