The browser already does your date formatting

Most production bundles still ship a date library to print "3 days ago" under a comment. The browser has handled that natively for years through the Intl API. It formats dates, currencies, compact numbers, units, relative time, and even lists — in every locale your users speak — and it weighs nothing because it's already there.

Dates, properly localized

Intl.DateTimeFormat covers almost every date display you'll 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 lets you store UTC everywhere and render in the user's zone at the last moment — the arrangement you wanted anyway.

For a one-off, 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. Notice the unit identifier uses the American spelling kilometer while the en-GB output comes back as "kilometres". The identifiers are fixed; the output localizes.

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 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. That's 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 don't 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 memoize 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's the territory of the incoming Temporal standard, which has started landing in browsers. Until it's everywhere, a fair split is a small library like date-fns for the maths and Intl for everything the user sees.

Your move

Audit your bundles. If you're pulling in a date library just for display, replace it with Intl today. Start with Intl.RelativeTimeFormat and Intl.DateTimeFormat. You'll cut bytes and get free localization. For date math, keep date-fns until Temporal ships everywhere.

Which library did Intl let you delete? Tell us in the comments.