Internationalisation: locales, calendars and relative time
Locale-aware formatting with Intl, numbering systems, non-Gregorian calendars, relative phrases and their limits, and the bundle-size cost of shipping CLDR data.
Formatting belongs to the platform
Do not hand-write month names or date order. Every platform ships locale data — the CLDR database — behind an API designed for it.
const dt = new Date("2026-09-18T10:30:00Z");
new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(dt);
// "September 18, 2026"
new Intl.DateTimeFormat("de-DE", { dateStyle: "long" }).format(dt);
// "18. September 2026" — day first, month name per locale
new Intl.DateTimeFormat("ja-JP", { dateStyle: "full", timeZone: "Asia/Tokyo" }).format(dt);
// the full form for that locale: era year, month, day and weekday
new Intl.RelativeTimeFormat("es", { numeric: "auto" }).format(-3, "day");
// "hace 3 dias"| Concern | Wrong | Right |
|---|---|---|
| Date order | Build MM/DD/YYYY by hand | Intl.DateTimeFormat with a locale |
| Month names | An English array | The formatter, or Intl.DisplayNames |
| Digits | Assume ASCII 0-9 | numberingSystem per locale (Arabic-Indic, Devanagari) |
| First day of week | Assume Monday | Intl.Locale.weekInfo |
| Time zone name | Hardcode a label | timeZoneName: "short" |
| Relative time | Concatenate "ago" | Intl.RelativeTimeFormat |
Non-Gregorian calendars
The same instant has different civil dates in different calendars. A user in Thailand may expect a Buddhist era year, and a user in Saudi Arabia may expect the Umm al-Qura calendar.
const dt = new Date("2026-09-18T10:30:00Z");
new Intl.DateTimeFormat("th-TH-u-ca-buddhist", { dateStyle: "long" }).format(dt);
// Buddhist era year 2569 (Gregorian year plus 543), rendered in Thai script
new Intl.DateTimeFormat("ar-SA-u-ca-islamic-umalqura", { dateStyle: "long" }).format(dt);
// the corresponding Hijri date, rendered in Arabic script
new Intl.DateTimeFormat("en-US-u-ca-hebrew", { dateStyle: "long" }).format(dt);
// the corresponding Hebrew calendar date, in English- A calendar choice changes the displayed date, never the stored instant.
- Never parse a localised date string back into an instant — it is a one-way format.
- Use the
-u-ca-extension on the locale tag to select a calendar. - Store an instant or an ISO string; apply the calendar only for display.
The size and correctness cost
// Node: full ICU is the default in modern releases
process.config.variables.icu_small; // false means full data
// Browsers ship the data; in Node you may need to build with full-icu
// or rely on the small-icu default, which supports only English formats- Bundling all CLDR locales into a web app can add hundreds of kilobytes;
Intlgives it to you for free from the platform. - Server-side rendering must know the user's locale and zone; guessing from the server's location produces wrong dates for everyone else.
- Hybrid: format on the server with an explicit locale and zone from the request, or send an ISO string and format in the browser.
- Relative phrases have limits: "in 3 days" is wrong for a date 8 months away, so switch to an absolute format beyond a threshold.
💡
Locale data goes stale. Zones are renamed, DST rules change and numbering preferences shift, so update the runtime and the tz database together. A frozen container image with year-old ICU data will disagree with the browser about what today is called.
FAQ
Should the server or the client format dates?
Format on the client when the user's device locale is the right answer. Format on the server when the output must be stable and cached, but pass the locale and zone explicitly rather than inferring them.
Why does my Buddhist-calendar test fail in Node?
The runtime was built with
small-icu, which only includes English locale data. Install a full-ICU build or set NODE_ICU_DATA to the full data file.Related
Date libraries compared Parsing and formatting dates safely
Last refreshed 2026-09-18.