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:
Then, by simply changing the lang
-attribute, we'll create variations like these:
lang="fa-IR"
lang="zh-Hans-CN-u-nu-hanidec"
lang="fr-FR"
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>
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>
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) { ... }
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';
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();
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);
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');
}
The function calls a helper-method, that deduct the current time from 'end-time':
const getRemainingTime = (endTime, currentTime = new Date().getTime()) => endTime - currentTime;
... 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>`
}
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);
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);
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);
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%);
}
Demo
Below is a Codepen. Feel free to fork it and change the locales:
Top comments (2)
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
andhantfin
but the results are the same as usingen-US
.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)