DEV Community

Cover image for Format Dates, Numbers and Relative Time in JavaScript Without a Library
Accreditly
Accreditly

Posted on

Format Dates, Numbers and Relative Time in JavaScript Without a Library

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
topstar_ai profile image
Luis Cruz

I was particularly impressed by the Intl.RelativeTimeFormat example, which simplifies the process of generating "yesterday" and "in 3 hours" strings. The numeric: '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 the timeAgo example, is a good way to ensure that the output is both accurate and user-friendly. Have you found any scenarios where the Intl API's handling of edge cases, such as daylight saving time or leap seconds, required special consideration in your applications?