Formatting a date correctly for a global audience is a different problem than storing or converting it between time zones — for that side (native `Date` limitations, the new `Temporal` API, and which library is still current), see Store and Display Dates Properly in JavaScript. This post stays specifically on locale-aware formatting: getting the date order, the calendar system, and relative phrasing (“3 days ago”) right for wherever the user actually is.
In this post: Why Intl.DateTimeFormat exists · Customizing the output · Detecting the user’s actual locale · Relative time: “3 days ago” in the user’s own language · Non-Gregorian calendars: a real, common need · Libraries, when Intl alone isn’t enough · Related reading
Why Intl.DateTimeFormat exists
`Date`’s own formatting methods (`toDateString()`, `toLocaleString()` with no arguments) don’t give real control over locale-specific output — date component order, month names, and calendar conventions vary by region in ways a fixed format string can’t handle. `Intl.DateTimeFormat`, part of the ECMAScript Internationalization API, formats a date according to actual locale conventions automatically:
const date = new Date("2026-08-20T15:00:00Z");
console.log(new Intl.DateTimeFormat("en-US").format(date)); // "8/20/2026"
console.log(new Intl.DateTimeFormat("en-GB").format(date)); // "20/08/2026"
Same date, same code, different output — the month/day order flips between US and UK conventions without any manual logic to handle it. That distinction matters more than it might seem: a hardcoded “MM/DD/YYYY” format string handed to every user regardless of locale is a genuinely common source of dates being misread entirely, not just displayed slightly awkwardly — 03/04 means an entirely different day depending on which convention the reader assumes.
Customizing the output
const options = { year: "numeric", month: "long", day: "2-digit", weekday: "long" };
console.log(new Intl.DateTimeFormat("en-US", options).format(date));
// "Thursday, August 20, 2026"
// Time zone handling works the same way, combined with locale
const nyOptions = { timeZone: "America/New_York", timeStyle: "short" };
console.log(new Intl.DateTimeFormat("en-US", nyOptions).format(date));
// Time rendered in New York's local time
Detecting the user’s actual locale
`navigator.language` (or the fuller `navigator.languages` array, for a ranked list of preferences) gives the browser-reported locale to feed into the APIs above, rather than hardcoding a locale string:
const userLocale = navigator.language; // e.g. "ar-SA"
console.log(new Intl.DateTimeFormat(userLocale, { dateStyle: "full" }).format(date));
For determining which calendar a locale actually expects, rather than guessing, `Intl.Locale` exposes it directly:
const locale = new Intl.Locale(navigator.language);
console.log(locale.getCalendars()); // e.g. ["islamic-umalqura", "gregory", ...] for ar-SA
The first value returned is the locale’s preferred calendar — worth using that as the default rather than assuming Gregorian and only special-casing the handful of locales a developer happens to already know about.
“3 days ago” isn’t something to hand-roll with date subtraction and a lookup table — Intl.RelativeTimeFormat produces genuinely correct, locale-specific relative phrasing natively.
Relative time: “3 days ago” in the user’s own language
Worth using directly rather than hand-rolling with date subtraction and a hardcoded string table — `Intl.RelativeTimeFormat` produces genuinely correct, grammatically-appropriate relative phrasing per locale, including plural forms that differ from language to language in ways a simple lookup table won’t get right:
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-3, "day")); // "3 days ago"
console.log(rtf.format(2, "hour")); // "in 2 hours"
const rtfFr = new Intl.RelativeTimeFormat("fr", { numeric: "auto" });
console.log(rtfFr.format(-3, "day")); // "il y a 3 jours"
The `-3`/`2` values represent the number of units in the past or future, not a raw millisecond difference — the actual date-math to derive that number still needs to happen separately (via `Date` or `Temporal`), with `Intl.RelativeTimeFormat` only handling the final locale-aware phrasing step. The `numeric: “auto”` option is worth using deliberately: it produces idiomatic phrases like “yesterday” or “tomorrow” where the locale has one, falling back to the numeric form (“1 day ago”) only where it doesn’t — `numeric: “always”` skips that idiomatic substitution entirely and always uses the numeric phrasing, a real, visible difference worth choosing intentionally rather than defaulting to whichever option happened to be copied from an example.
Non-Gregorian calendars: a real, common need
Worth knowing this is a genuine, common requirement rather than an edge case: several major markets expect dates in their own calendar system, not a translated version of the Gregorian calendar. `Intl.DateTimeFormat` supports this directly via the `calendar` option:
// Islamic (Hijri) calendar -- common in Saudi Arabia and other markets
console.log(new Intl.DateTimeFormat("ar-SA", { calendar: "islamic", dateStyle: "full" }).format(date));
// Buddhist calendar -- used in Thailand
console.log(new Intl.DateTimeFormat("th-TH", { calendar: "buddhist", dateStyle: "long" }).format(date));
// Japanese imperial calendar -- era-based (e.g. "Reiwa 8")
console.log(new Intl.DateTimeFormat("ja-JP-u-ca-japanese", { dateStyle: "full" }).format(date));
Other supported values include `hebrew`, `chinese`, `persian`, `indian`, `coptic`, and `ethiopic` — worth checking which calendar a target market actually expects rather than assuming Gregorian-with-translated-month-names is sufficient, since for these locales it genuinely isn’t the convention users expect.
Libraries, when Intl alone isn’t enough
For most locale-formatting needs, `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat` are genuinely sufficient on their own, with no dependency required. Where a project needs more — complex date arithmetic, parsing ambiguous formats, or a more ergonomic chainable API — date-fns, Day.js, and Luxon remain the current, actively maintained options (Moment.js is not; see the related post below for that distinction in full). Luxon specifically is worth knowing as built on top of the native `Intl` API rather than its own separate implementation, which is part of why its locale support tends to match what `Intl.DateTimeFormat` already provides directly:
import { DateTime } from "luxon";
const date = DateTime.fromISO("2026-08-20T15:00:00", { zone: "utc" });
console.log(date.setLocale("fr").toLocaleString(DateTime.DATE_FULL));
// "jeudi 20 août 2026"
Related reading
- Store and Display Dates Properly in JavaScript — storage, time zone conversion, and the new native Temporal API.
Correct international date handling is mostly a matter of using the native `Intl` APIs deliberately rather than hand-rolling locale logic — the calendar-system and relative-time gaps above are the two most likely to get missed entirely, not because they’re hard, but because they don’t come up until an actual user in that locale notices something looks wrong. Testing against a handful of non-default locales — not just the development team’s own — during QA is a cheap way to catch these before a real user does, and worth building into a standard testing checklist rather than trusting it’ll surface naturally.
App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning


