DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Build a Multi-Language Countdown Component
Mads Stoumann
Mads Stoumann

Posted on

Build a Multi-Language Countdown Component

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

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

lang="fa-IR"
Enter fullscreen mode Exit fullscreen mode

Arabic

lang="zh-Hans-CN-u-nu-hanidec"
Enter fullscreen mode Exit fullscreen mode

Chinese

lang="fr-FR"
Enter fullscreen mode Exit fullscreen mode

French

Cool, right? Let's get started!


HTML

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

<time datetime="2023-01-01"></time>
Enter fullscreen mode Exit fullscreen mode

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

<time datetime="2022-08-12T09:30:00+02:00"></time> 
Enter fullscreen mode Exit fullscreen mode

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>):

function countDown(element) { ... }
Enter fullscreen mode Exit fullscreen mode

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:

const locale = element.lang || document.documentElement.getAttribute('lang') || 'en-US';
Enter fullscreen mode Exit fullscreen mode

End-date/time

The end-time is the datetime-attribute from the main element, converted to time:

const endTime = new Date(element.getAttribute('datetime')).getTime();
Enter fullscreen mode Exit fullscreen mode

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!):

const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const zero = new Intl.NumberFormat(locale).format(0);
Enter fullscreen mode Exit fullscreen mode

Showing Time

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

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

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

const getRemainingTime = (endTime, currentTime = new Date().getTime()) => endTime - currentTime;
Enter fullscreen mode Exit fullscreen mode

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

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

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:

if (remainingTime >= 1000) requestAnimationFrame(showTime);
Enter fullscreen mode Exit fullscreen mode

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:

const zero = new Intl.NumberFormat(locale).format(0);
Enter fullscreen mode Exit fullscreen mode

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:

requestAnimationFrame(showTime);
Enter fullscreen mode Exit fullscreen mode

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:

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

Demo

Below is a Codepen. Feel free to fork it and change the locales:

Top comments (2)

Collapse
 
alxtsg profile image
Alex Tsang

lang="zh-Hans-CN-u-nu-hanidec"

In Chinese the numerical values such as 141 and 16 are not written as "δΈ€ε››δΈ€" and "δΈ€ε…­" but "一百四十一" and "十六" respectively ("η™Ύ" means hundred and "十" means ten, so "δΈ€η™Ύ" means one hundred and "四十" means forty). The results look very weird. I tested other numeral systems like hanidays and hantfin but the results are the same as using en-US.

Collapse
 
madsstoumann profile image
Mads Stoumann Author

That’s interesting, thanks for sharing. I wonder if these are bugs in the Intl.RelativeTimeFormat api, or if these locales are not fully supported (yet)

🌚 Life is too short to browse without dark mode