Using the built-in JavaScript Intl API to render dates the way each locale expects.
Last reviewed on 25 April 2026.
JavaScript's Intl namespace covers a surprising amount of ground: numbers, currencies, lists, plural forms, segmenters, and — most useful for any site that touches users in more than one country — dates and times. For most projects it removes the reason to ship a heavy formatting library. This page is a working tour of the date and time pieces, with the gotchas that catch teams the first time they switch.
You can call date.toLocaleDateString('fr-FR') and get a French date. It works for one-off rendering. The problem is that every call constructs a fresh formatter under the hood, which is the slowest path the engine offers, and you cannot configure it as precisely as the dedicated API.
// One-off — fine for a quick value, slow if called in a loop
date.toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' });
// Reusable formatter — much faster, more configurable
const fr = new Intl.DateTimeFormat('fr-FR', {
day: '2-digit', month: 'long', year: 'numeric'
});
fr.format(date); // "03 avril 2025"
Construct the formatter once per locale-options pair, cache it, and reuse it. For a list of a thousand items rendered to the same locale, this is the difference between fast and slow.
Intl.DateTimeFormat takes a locale tag and an options object. The options describe which fields to include and how detailed each one should be; the locale decides the actual layout, separators, and capitalisation. You should almost never write a literal format string — let the locale render its own conventions.
const opts = {
full: { dateStyle: 'full' }, // "Thursday, 3 April 2025"
long: { dateStyle: 'long' }, // "3 April 2025"
medium: { dateStyle: 'medium' }, // "3 Apr 2025"
short: { dateStyle: 'short' }, // "03/04/2025"
weekday: { weekday: 'short', day: 'numeric', month: 'short' }, // "Thu, 3 Apr"
timeOnly:{ hour: '2-digit', minute: '2-digit' } // "14:30"
};
The dateStyle and timeStyle shortcuts give you the four standard CLDR sizes. Mix them with the explicit field options (weekday, era, year, month, day, hour, minute, second, fractionalSecondDigits, timeZoneName) when you need a non-standard combination — but pick one approach per call. Combining dateStyle with explicit field options throws a TypeError.
Locale tags follow BCP 47: language, optional script, optional region, optional Unicode extensions. en-GB and en-US render the same date differently because they have different region defaults. fr-CA and fr-FR diverge on small things like the use of "h" between hours and minutes.
You can pass an array; the engine picks the first locale it actually has data for.
// Try Welsh; fall back to British English; fall back to whatever the engine has.
const fmt = new Intl.DateTimeFormat(['cy-GB', 'en-GB']);
Whatever the user's actual preference is, the safe path is to read navigator.languages on the client (or the Accept-Language header on the server) and pass the array directly. Avoid hard-coding a single locale unless you genuinely have only one audience.
The timeZone option takes an IANA identifier and renders the value in that zone:
new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium', timeStyle: 'short', timeZone: 'Asia/Tokyo'
}).format(new Date()); // "3 Apr 2025, 14:30"
This is the simplest way to render a UTC instant in a user's local time. If you do not pass timeZone, the formatter uses the runtime's default zone, which is fine for an in-browser app but a hidden trap on a server: a Node process running in UTC and one running in a server's regional zone will format the same value differently. Always pass timeZone explicitly on the server.
For UI that places parts of the date into separate elements — say, a wall calendar tile with the day prominent and the month subtle — formatToParts returns an array of typed segments rather than a single string.
const fmt = new Intl.DateTimeFormat('en-GB', { weekday: 'short', day: '2-digit', month: 'short' });
fmt.formatToParts(new Date('2025-04-03'));
// [
// { type: 'weekday', value: 'Thu' },
// { type: 'literal', value: ', ' },
// { type: 'day', value: '03' },
// { type: 'literal', value: ' ' },
// { type: 'month', value: 'Apr' }
// ]
This is the right tool when you need to wrap individual fields in elements; concatenating the strings yourself loses the locale's correct separators.
"3–4 April 2025" is one string in English. "3 апреля – 4 апреля 2025 г." is the equivalent in Russian. Building these by hand from two formatted dates and a hyphen produces wrong output for any locale that joins ranges differently.
const fmt = new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' });
fmt.formatRange(
new Date('2025-04-03'),
new Date('2025-04-04')
); // "3–4 April 2025"
formatRangeToParts is the granular variant for the same UI cases as formatToParts.
"3 days ago", "in 2 hours", "yesterday" — these are relative phrasings, and they are handled by Intl.RelativeTimeFormat, not DateTimeFormat.
const rt = new Intl.RelativeTimeFormat('en-GB', { numeric: 'auto' });
rt.format(-1, 'day'); // "yesterday"
rt.format(2, 'hour'); // "in 2 hours"
rt.format(-3, 'week'); // "3 weeks ago"
The numeric: 'auto' option lets the locale substitute idiomatic phrases ("yesterday" instead of "1 day ago"). The unit names match the standard set: year, quarter, month, week, day, hour, minute, second. Computing the right unit and value for "the closest natural phrasing" is your job — the formatter only renders.
Most users want Gregorian dates regardless of locale, and that is the default. When you genuinely need an Islamic, Hebrew, Persian, Buddhist, or Japanese imperial calendar — see the calendar systems page for the broader context — pass the calendar option:
// Show today in the Islamic calendar
new Intl.DateTimeFormat('ar-SA-u-ca-islamic', { dateStyle: 'long' }).format(new Date());
// Equivalent options-object form
new Intl.DateTimeFormat('ar-SA', { calendar: 'islamic', dateStyle: 'long' }).format(new Date());
The Unicode locale extension -u-ca-... embeds the calendar choice in the tag itself, which is convenient when the calendar follows the user's locale preference. The options form is convenient when the calendar is decided by the page rather than the user.
A close cousin: the numberingSystem option (or -u-nu-...) controls which digits are used. arab renders Arabic-Indic digits; latn renders Western digits. Locales have sensible defaults; override only when the requirement is specific.
A comment posted 90 minutes ago should render as "an hour ago" or "1 hour ago" depending on the locale, and as a tooltip with the absolute date and time when hovered. The pieces fit together like this:
const ABS = new Intl.DateTimeFormat(navigator.languages, {
dateStyle: 'medium', timeStyle: 'short'
});
const REL = new Intl.RelativeTimeFormat(navigator.languages, { numeric: 'auto' });
function describePostedAt(postedAt) {
const seconds = (postedAt - Date.now()) / 1000;
const [unit, value] = pickUnit(seconds); // your helper, returns ['hour', -1] etc.
return {
relative: REL.format(value, unit),
absolute: ABS.format(postedAt)
};
}
Two formatters are constructed once, both honour the user's preferred languages, and both use the runtime's default zone (which is appropriate for in-browser code). On the server this code would set timeZone explicitly and accept the user's IANA zone identifier as input.
${dd}/${mm}/${yyyy} you re-implement what dateStyle: 'short' already gives you, badly.timeZone on the server. Server output drifts when the host's zone changes. Always pass it explicitly.dateStyle with explicit field options. The combination throws a TypeError; pick one approach per call.RelativeTimeFormat for absolute dates. "3 weeks ago" is fine for a recent comment; for a date six months in the future, render the absolute value.Intl.DateTimeFormat.supportedLocalesOf() tells you what is actually available.Intl handles formatting and relative time. It does not parse arbitrary input, manipulate dates (add a month, find the start of a week), or expose every CLDR pattern. For those, the JavaScript date libraries on the libraries page still earn their place. The right division of labour is: format and relative-format with Intl; parse, validate, and manipulate with whatever library suits your bundle. The date parsing page covers the parsing side.
calendar option.timeZone option.