Build a Multi-Language Countdown Component
Tags: javascript, webdev, tutorial, css
Published: 2022-08-14 05:12:45

Some years ago, I built a countdown component with support for multiple languages. Editors had to fill in labels like "days", "hours" and "minutes" for each language — in both singular and plural versions. Luckily, things have greatly improved since then!

In this tutorial, we'll be building a vanilla countdown component that'll work with **all languages** — without having to fill out any labels per language.

We'll be using the `Intl.RelativeTimeFormat` and `Intl.NumberFormat` API's to build a countdown component like this:

![Countdown](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g0vglloszzpa5a4z1n24.png)

Then, by simply changing the `lang`-attribute, we'll create variations like these:

```html
lang="fa-IR"
```

![Arabic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/br2b97yv14ec75qylqmo.png)
 
```html
lang="zh-Hans-CN-u-nu-hanidec"
```

![Chinese](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxzg5evhf6sx6awwa1od.png)

```html
lang="fr-FR"
```

![French](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n1daljodkvw3stc8xaks.png)

Cool, right? Let's get started!

---

## HTML

We'll be using the `<time>`-tag, with the end-date set in the `datetime`-property:

```html
<time datetime="2023-01-01"></time>
``` 

If you want your countdown to end at a specific **time** also, add that to the string:

```html
<time datetime="2022-08-12T09:30:00+02:00"></time> 
```

> **NOTE:** Remember to add the **timezone offset** of the location, where the countdown expires. This shouldn't be the server-time, as your location might be in central Europe, while your server/CDN is in the US. In the example above, the offset is `+02:00`.

---

## JavaScript
We'll create a new function with a single argument, `element`, which is a reference to the main element (`<time>`):

```js
function countDown(element) { ... }
```

Next, we'll set up some consts and defaults:

### Locale
The locale is the most important part. This code will look for a `lang`-attribute on the main element, then — if not found — on the page itself, and finally return a fallback,  using `en-US`:

```js
const locale = element.lang || document.documentElement.getAttribute('lang') || 'en-US';
```

### End-date/time
The end-time is the `datetime`-attribute from the main element, converted to time:

```js
const endTime = new Date(element.getAttribute('datetime')).getTime();
```

### API's
Finally, we create an instance of `Intl.RelativeTimeFormat` — as well as storing a const with the locale-value of zero (more on that later!):

```js
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const zero = new Intl.NumberFormat(locale).format(0);
```

---
## Showing Time

Now, for the code that returns and formats the time and it's sub-parts:

```js
const showTime = () => {
  const remainingTime = getRemainingTime(endTime);
  element.innerHTML = 
    timePart(Math.floor(remainingTime / (24 * 60 * 60 * 1000)), 'day') +
    timePart(Math.floor((remainingTime / (60 * 60 * 1000)) % 24), 'hour') +
    timePart(Math.floor((remainingTime / (60 * 1000)) % 60), 'minute') +
    timePart(Math.floor((remainingTime / 1000) % 60), 'second');
}
```

The function calls a helper-method, that deduct the current time from 'end-time':

```js
const getRemainingTime = (endTime, currentTime = new Date().getTime()) => endTime - currentTime;
```

... as well as `timePart`, the **most important** function, that returns locale-formatted time:

```js
const timePart = (part, type) => {
  const parts = rtf.formatToParts(part === 0 ? 2 : part, type);
  if (parts && parts.length === 3) parts.shift();
  const [unit, label] = parts; 
  return `<span><strong>${part === 0 ? zero : unit.value}</strong><small>${label.value}</small></span>`
}
```

`formatToParts` returns an array of time-units and labels in the locale-language. We spread these into `unit` and `label`, and output them in `strong>` and `<small>`-tags (feel free to replace with whatever tags you want!).

### requestAnimationFrame

The `showTime`-function need to call itself continuously, for which we use `requestAnimationFrame`:

```js
if (remainingTime >= 1000) requestAnimationFrame(showTime);
```

---

## Zero is Plural

Now, you might wonder why I call `formatToParts` with `2`, if the value itself is `0` (zero). That's because `Intl.RelativeTimeFormat` will return the string "now" (in the locale-language), if the value is zero — and **no** label (for some reason). 

We don't want that, but neither do we want to show the english zero in languages that have their own zero!

That's why, in the beginning, we declared this:

```js
const zero = new Intl.NumberFormat(locale).format(0);
```

For the label, we need a value larger than `1`. If we use "seconds" as an example, the value `1`, will return the label "second", while the value `2` will return "seconds". Zero is plural (you say "zero seconds", not "zero second"), hence the `2` 😄

Confused? So was I!

---

Finally, to initialize and run it:

```js
requestAnimationFrame(showTime);
```

---

Phew! A lot of code, but only approx. **400 bytes** when minified and gzipped!

---

## CSS
The CSS is simply a grid, see the demo below for details. I like to use CSS Custom Props for the parts of a component, that can have variations. The format I prefer is [component]-[part]-[emmet abbrevation of property], so: 

```css
.variant {
--countdown-bgc: hsl(0, 35%, 45%);
--countdown-time-bgc: hsl(0, 35%, 80%);
--countdown-time-lbl-c: hsl(0, 35%, 15%);
--countdown-time-val-c: hsl(0, 35%, 25%);
}
```

---

## Demo

Below is a Codepen. Feel free to fork it and change [the locales](https://gist.github.com/jacobbubu/1836273):

{% codepen https://codepen.io/stoumann/pen/OJvowaG %}