A surprising number of production bundles still ship a date library to do one thing: print "3 days ago" under a comment. The browser has done that natively for years, in every locale your users speak, through the Intl API. It formats dates, currencies, compact numbers, units, relative time and even lists, and it weighs nothing because it is already there. Let's have a look at the parts you will actually use.
Dates, properly localised
Intl.DateTimeFormat covers almost every date display you will ever need with two options:
const format = new Intl.DateTimeFormat('en-GB', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Europe/London',
});
format.format(new Date());
// "31 July 2026 at 14:00"
Swap 'en-GB' for 'fr', 'de' or 'ja' and the output translates itself, month names and all. The timeZone option means you can store UTC everywhere and render in the user's zone at the last moment, which is the arrangement you wanted anyway.
For a one-off you can skip constructing a formatter and call the shortcut on the date itself:
new Date().toLocaleDateString('en-GB', { dateStyle: 'medium' });
// "31 Jul 2026"
Numbers, currencies and units
Intl.NumberFormat handles the formatting jobs that usually attract hand-rolled regex:
new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP',
}).format(1499.99);
// "£1,499.99"
new Intl.NumberFormat('en', { notation: 'compact' }).format(1250000);
// "1.3M"
new Intl.NumberFormat('en-GB', {
style: 'unit',
unit: 'kilometer',
unitDisplay: 'long',
}).format(26.2);
// "26.2 kilometres"
That compact notation is the follower-count style you see on every social platform, free of charge. And notice the unit identifier uses the American spelling kilometer while the en-GB output comes back as "kilometres". The identifiers are fixed; the output localises.
Relative time, the bit everyone installs a library for
Intl.RelativeTimeFormat produces the "yesterday" and "in 3 hours" strings:
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(3, 'hour'); // "in 3 hours"
The numeric: 'auto' option is what turns "1 day ago" into "yesterday". The API wants a value and a unit, so in practice you pair it with a small helper that picks the largest sensible unit:
const units = [
['year', 31536000],
['month', 2592000],
['week', 604800],
['day', 86400],
['hour', 3600],
['minute', 60],
['second', 1],
];
function timeAgo(date) {
const delta = (date.getTime() - Date.now()) / 1000;
for (const [unit, seconds] of units) {
if (Math.abs(delta) >= seconds || unit === 'second') {
return rtf.format(Math.round(delta / seconds), unit);
}
}
}
timeAgo(new Date(Date.now() - 86400000 * 3));
// "3 days ago"
Fifteen lines, and that is the entire reason many projects still carry a date dependency.
Lists that read like sentences
The least known member of the family might be the most charming. Intl.ListFormat joins arrays the way a human would write them:
const list = new Intl.ListFormat('en-GB', { type: 'conjunction' });
list.format(['PHP', 'JavaScript', 'CSS']);
// "PHP, JavaScript and CSS"
Run the same code under en-US and you get "PHP, JavaScript, and CSS". The API knows about the Oxford comma so you do not have to have the argument.
Create formatters once
One genuine footgun: constructing a formatter is the expensive part, while calling .format() is cheap. Creating a new Intl.DateTimeFormat inside a loop or a hot render path will show up in a profile. Hoist them to module scope, or memoise per locale if the locale varies:
const cache = new Map();
function currencyFormatter(locale, code) {
const key = `${locale}:${code}`;
if (!cache.has(key)) {
cache.set(key, new Intl.NumberFormat(locale, {
style: 'currency',
currency: code,
}));
}
return cache.get(key);
}
Where Intl stops
Intl formats. It does not parse "31/07/2026", add a month to a date or convert a timestamp between zones for arithmetic. That is the territory of the incoming Temporal standard, which has started landing in browsers. Until it is everywhere, a fair split is a small library like date-fns for the maths and Intl for everything the user sees.
Recap
In this tutorial you've seen Intl.DateTimeFormat for localised dates, Intl.NumberFormat for currencies, compact notation and units, Intl.RelativeTimeFormat for "3 days ago", Intl.ListFormat for sentence-style lists, and why formatters should be created once and reused. For display work, the platform has quietly made the date library optional.
Which library did Intl let you delete? Tell us in the comments below.
Top comments (1)
I was particularly impressed by the
Intl.RelativeTimeFormatexample, which simplifies the process of generating "yesterday" and "in 3 hours" strings. Thenumeric: 'auto'option is a nice touch, allowing for more natural-sounding output. I've found that using a helper function to pick the largest sensible unit, as shown in thetimeAgoexample, is a good way to ensure that the output is both accurate and user-friendly. Have you found any scenarios where theIntlAPI's handling of edge cases, such as daylight saving time or leap seconds, required special consideration in your applications?