Fast and Light Relative Time Strings in JS
Tags: javascript, performance, webdev
Published: 2023-01-24 17:57:58

Are you ever building something simple in JavaScript and go *woah:*

![Screenshot of moment.js taking up 19.7kb gzipped](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4iamqsy92lenodu8dzj.png)

20kb for a simple date transformation??

All I wanted is a pretty output for a relative time. Like time is `in 5 minutes` or `2 weeks ago` or `tomorrow` . You know, like your calendar can do, and Twitter, and kinda anything that needs to refer to a relative time in a friendly way.

Now, you may think, *I got this*, and begin doing something like:

```tsx
// 🚩🚩 No no no 🚩🚩
function nativeGetRelativeTime(unit = 'days', amount = 2) {
  return `in ${amount} ${unit}s`
}
```

Yeah… please don’t. While relative times may seem like a simple problem for a moment, you should begin to realize that relative time strings have a lot of complexities to address, like:

- **Pluralization**:  we don’t say `"1 days ago"` or `"in 1 minutes"`
- **Abbreviation**: who even says `"1 day ago"`? Humans say words like `"yesterday"`, `"tomorrow"`, or  `"next year"`
- **Future vs past**: such as how we don’t say `"in -2 days"`, we say `"2 days ago"`
- **Localization**: This is the big one. Not only is translating complex across many languages (including localized differences to the above bullets), if you even built all of that into your function, you would end up with a massive payload of localized rules and variations.

Why do you *think* `moment.js` is so large? It’s not because of incompetent developers. It’s because time and date handling across all of the above criteria is simply complex.

So, before you go and try to do something like this:

```jsx
// 🚩🚩 This is an example of what NOT to do 🚩🚩
function nativeGetRelativeTime(locale, unit, amount) {
  if (locale === 'en-US') {
    const isPlural = amount !== 1 && amount !== -1
    const isPast = amount < 0
    if (amount === 1 && unit === 'day') return 'tomorrow'
    if (amount === -1 && unit === 'day') return 'yesterday'
    if (amount === 1 && unit === 'year') return 'next year'
    // ... repeat for every possible abbreviation ...
    if (isPast) {
      return `${amount} ${unit}${isPlural ? 's' : ''} ago`
    }
    return `in ${amount} ${day}${isPlural ? 's' : ''}`
  }
  if (locale === 'es-US') { 
    // Repeat all the rules again, but specific to this locale...
  }
  // Repeat for every possible locale ...
}
```

Just, please, don’t. There is a solution, and it’s built into browsers.

## `Intl.RelativeTimeFormat` to the rescue

When you're in these situations, it's important to remember that the web platform has been evolving a lot, and these days has so many solutions to common problems built-in.

There is where the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) object has a number of solutions. For our use case today, we want [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat)

```jsx
// 😍
const rtf = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
})

rtf.format(1, 'day') // 'tomorrow'
rtf.format(-2, 'year') // '2 years ago'
rtf.format(10, 'minute') // 'in 10 minutes'
```

And, as expected, it works beautifully across locales:

```jsx
const rtf = new Intl.RelativeTimeFormat('es-ES', {
  numeric: 'auto'
})
rtf.format(1, 'day') // 'mañana'
rtf.format(-2, 'year') // 'hace 2 años'
rtf.format(10, 'minute') // 'dentro de 10 minutos'
```

To make things more convenient, you can even just pass in the current `navigator.language` as the first argument as well:

```jsx
const rtf = new Intl.RelativeTimeFormat(
  navigator.language // ✅
)
```

And supported units include: `"year"`, `"quarter"`, `"month"`, `"week"`, `"day"`, `"hour"`, `"minute"`, and `"second"`

## Making it useful

Returning to our original example, what we really want is something that can automatically pick the appropriate unit to use, so it will print `"yesterday"` for a date that is 1 day ago, and `"next year"` for a date that is a year from now (as opposed to `"in 370 days"` or `"in 52 weeks"` etc).

So, ideally, we’d have just a simple wrapper function like:

```jsx
function Relative(props) {
  const timeString = getRelativeTimeString(props.date) // ⬅️
  
  return <>
    {timeString}
  </>
}
```

Now that we have `Intl.RelativeTimeFormat` in our toolkit, we can implement this pretty simply. The only real question is what unit we should choose for the given time delta (`"hour"`, `"day"`, etc)

Here is one simple solution in TypeScript:

```jsx
/**
 * Convert a date to a relative time string, such as
 * "a minute ago", "in 2 hours", "yesterday", "3 months ago", etc.
 * using Intl.RelativeTimeFormat
 */
export function getRelativeTimeString(
  date: Date | number,
  lang = navigator.language
): string {
  // Allow dates or times to be passed
  const timeMs = typeof date === "number" ? date : date.getTime();

  // Get the amount of seconds between the given date and now
  const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);

  // Array reprsenting one minute, hour, day, week, month, etc in seconds
  const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];

  // Array equivalent to the above but in the string representation of the units
  const units: Intl.RelativeTimeFormatUnit[] = ["second", "minute", "hour", "day", "week", "month", "year"];

  // Grab the ideal cutoff unit
  const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));

  // Get the divisor to divide from the seconds. E.g. if our unit is "day" our divisor
  // is one day in seconds, so we can divide our seconds by this to get the # of days
  const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;

  // Intl.RelativeTimeFormat do its magic
  const rtf = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
  return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}
```

Thank you to [LewisJEllis](https://gist.github.com/LewisJEllis) for this code snippet that is a simplification of my [original quick and dirty solution](https://gist.github.com/steve8708/ada9bff2600228789fce2fcc95427e39).

## Using `date-fns`

If you don’t want to have to roll your own solution, there is an open source answer for you as well. [date-fns](https://github.com/date-fns/date-fns) is a great utility library for dates in JavaScript, each exported individually in a tree shakeable way.

Built into `date-fns` is an [intlFormatDistance](https://date-fns.org/v2.29.3/docs/intlFormatDistance) function that is a tiny wrapper over `Intl.RelativeTimeFormat` and does exactly what we need, and under 2kb. 


![Image of intlFormatDistance from date-fns taking 1.7kb](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dtqay5qg5p8aa4z9ttv7.png)

You can see the source code for it [here](https://github.com/date-fns/date-fns/blob/fadbd4eb7920bf932c25f734f3949027b2fe4887/src/intlFormatDistance/index.ts) and compare it to our above example implementation, if you like.

## Browser and runtime support

Heres the best part. `Intl.RelativeTimeFormat` is supported by all major browsers, as well as Node.js and Deno:

![Screenshot from the link below (MDN) of the browser support table showing support by all modern browsers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqvedvfuemhdveisep0s.png)

*Source: [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat)*

## Explore more of what `Intl`  can do

Now that we’ve got our relative time string solution, let’s quickly peek at a couple other cool constructors on the `Intl` object, to get a sense of what else it can do.

A couple of my favorites are:

### Date formatting with `Intl.DateTimeFormat`

[Intl.DateTimeformat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) gives you browser native date and time formatting:

```jsx
new Intl.DateTimeFormat('en-US').format(new Date())
// -> 1/23/2023

new Intl.DateTimeFormat('en-US', {
  dateStyle: 'full'
}).format(new Date())
// -> Monday, January 23, 2023

new Intl.DateTimeFormat('en-US', {
  timeStyle: 'medium'
}).format(new Date())
// -> 4:17:23 PM

new Intl.DateTimeFormat('en-US', {
  dayPeriod: 'short', hour: 'numeric'
}).format(new Date())
// -> 4 in the afternoon
```

`Intl.DateTimeFormat` is [supported](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#browser_compatibility) by all major browsers, Node.js, and Deno.

### Number formatting with `Intl.NumberFormat`

[Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) gives you browser native number formatting:

```jsx
// Decimals, commas, and currency
new Intl.NumberFormat('en', { 
  style: 'currency', currency: 'USD' 
}).format(123456.789)
// -> $123,456.79

new Intl.NumberFormat('de-DE', { 
  style: 'currency', currency: 'EUR' 
}).format(123456.789)
// -> 123.456,79 €

// Formatting with units
new Intl.NumberFormat('pt-PT', {
  style: 'unit', unit: 'kilometer-per-hour'
}).format(50));
// -> 50 km/h

// Shorthand
(16).toLocaleString('en-GB', {
  style: 'unit', unit: 'liter', unitDisplay: 'long',
}));
// -> 16 litres
```

`Intl.NumberFormat` is [supported](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat#browser_compatibility) by all major browsers, Node.js, and Deno.

## Conclusion

If you’re still using large date handling libraries like `moment` - modern JavaScript has got you fam, with `Intl.RelativeTimeFormat`, `Intl.DateTimeFormat`, and [more](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#constructor_properties).

## About me

Hi! I'm [Steve](https://twitter.com/Steve8708?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor), CEO of [Builder.io](https://www.builder.io/).

We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually.

You can read more about how this can improve your workflow [here](https://www.builder.io/blog/headless-cms-workflow).

You may find it interesting or useful:

![Gif of Builder.io](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5t8ti43cci5n6t6cfb.gif)