Несколько лет назад я создал компонент обратного отсчета с поддержкой нескольких языков. Редакторам приходилось заполнять такие метки, как «дни», «часы» и «минуты» для каждого языка — как в единственном, так и во множественном числе. К счастью, с тех пор ситуация значительно улучшилась!
В этом руководстве мы создадим ванильный компонент обратного отсчета, который будет работать со всеми языками — без необходимости заполнять какие-либо метки для каждого языка.
Мы будем использовать API Intl.RelativeTimeFormat
и Intl.NumberFormat
для создания такого компонента обратного отсчета:
Затем, просто изменив lang
-атрибут, мы создадим такие варианты, как этот:
lang="fa-IR"
lang="zh-Hans-CN-u-nu-hanidec"
lang="fr-FR"
Круто, правда? Давайте начнем!
HTML
Мы будем использовать тег <time>
с конечной датой, заданной в свойстве datetime
:
<time datetime="2023-01-01"></time>
Если вы хотите, чтобы обратный отсчет заканчивался в определенное время, также добавьте это свойство в строку:
<time datetime="2022-08-12T09:30:00+02:00"></time>
ПРИМЕЧАНИЕ: Не забудьте добавить смещение часового пояса того места, где заканчивается отсчет. Это не должно быть время сервера, так как ваше местоположение может находиться в центральной Европе, в то время как ваш сервер/CDN находится в США. В примере выше смещение равно
+02:00
.
JavaScript
Создадим новую функцию с одним аргументом, element
, который является ссылкой на основной элемент (<time>
):
function countDown(element) { ... }
Далее мы установим некоторые константы и значения по умолчанию:
Локаль
Локаль — самая важная часть. Этот код будет искать lang
-атрибут на главном элементе, затем — если он не найден — на самой странице, и, наконец, вернет обратный вариант, используя en-US
:
const locale = element.lang || document.documentElement.getAttribute('lang') || 'en-US';
Конечная дата/время
Конечное время — это datetime
-атрибут основного элемента, преобразованный во время:
const endTime = new Date(element.getAttribute('datetime')).getTime();
API
Наконец, мы создаем экземпляр Intl.RelativeTimeFormat
— а также сохраняем const со значением locale равным нулю (подробнее об этом позже!):
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const zero = new Intl.NumberFormat(locale).format(0);
Отображение времени
Теперь о коде, который возвращает и форматирует время и его подчасти:
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');
}
Функция вызывает вспомогательный метод, который вычитает текущее время из ‘end-time’:
const getRemainingTime = (endTime, currentTime = new Date().getTime()) => endTime - currentTime;
… а также timePart
, самую важную функцию, которая возвращает время в локальном формате:
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
возвращает массив единиц времени и меток на локальном языке. Мы раскладываем их на unit
и label
, и выводим в strong>
и <small>
-тегах (не стесняйтесь заменить их на любые теги, которые вам нужны!).
requestAnimationFrame
Функция showTime
должна вызывать себя постоянно, для чего мы используем requestAnimationFrame
:
if (remainingTime >= 1000) requestAnimationFrame(showTime);
Ноль — это множественное число
Теперь вы можете удивиться, почему я вызываю formatToParts
с 2
, если само значение 0
(ноль). Это потому, что Intl.RelativeTimeFormat
вернет строку «now» (на локальном языке), если значение равно нулю — и никакой метки (по какой-то причине).
Мы не хотим этого, но мы также не хотим показывать английский ноль в языках, где есть свой собственный ноль!
Вот почему в начале мы объявили это:
const zero = new Intl.NumberFormat(locale).format(0);
Для метки нам нужно значение больше, чем 1
. Если в качестве примера мы используем «секунды», то значение 1
вернет метку «секунда», а значение 2
вернет «секунды». Ноль — это множественное число (вы говорите «ноль секунд», а не «ноль секунд»), поэтому 2
😄.
Запутались? Я тоже!
Наконец, чтобы инициализировать и запустить его:
requestAnimationFrame(showTime);
Фух! Много кода, но всего около 400 байт при минимизации и сжатии!
CSS
CSS — это просто сетка, подробности смотрите в демонстрации ниже. Мне нравится использовать CSS Custom Props для частей компонента, которые могут иметь вариации. Я предпочитаю формат [component]-[part]-[emmet abbrevation of property], так:
.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%);
}
Демонстрация
Ниже приведен Codepen. Не стесняйтесь использовать его и изменять локали: