Принципы локализации в timeago.js

Локализация в timeago.js построена на функциональной модели: каждая локаль представлена функцией, которая на основе числа и индекса интервала возвращает строку. Этот подход позволяет реализовать любую грамматику — от простых языков с одной формой множественного числа до языков со сложными падежными и числовыми системами.


Архитектура локализации

Каждая локаль — это функция следующей сигнатуры:

type LocaleFunc = (number: number, index: number, totalSeconds?: number) => [string, string];

Аргументы:

  • number — количество единиц времени (например, 5 для “5 минут”)
  • index — индекс временного интервала (0–13)
  • totalSeconds — общая разница в секундах (доступна в некоторых версиях)

Возвращаемое значение — массив из двух строк: [прошлое, будущее].


Индексы и соответствующие интервалы

index Прошлое (пример) Будущее (пример)
0 только что прямо сейчас
1 1 секунду назад через 1 секунду
2 X секунд назад через X секунд
3 1 минуту назад через 1 минуту
4 X минут назад через X минут
5 1 час назад через 1 час
6 X часов назад через X часов
7 1 день назад через 1 день
8 X дней назад через X дней
9 1 неделю назад через 1 неделю
10 X недель назад через X недель
11 1 месяц назад через 1 месяц
12 X месяцев назад через X месяцев
13 1 год назад через 1 год
14 X лет назад через X лет

Плейсхолдер %s

В строках локали %s заменяется числом при формировании результата:

"%s минут назад" → "5 минут назад"
"через %s часов" → "через 3 часов"

Для индексов с единственным числом (индексы 3, 5, 7, 9, 11, 13) плейсхолдер %s обычно не нужен, так как число там фиксировано (1).


Регистрация локали

import { register } from 'timeago.js';

register('de', (number, index) => {
  return [
    ['gerade eben', 'gleich'],
    ['vor %s Sekunden', 'in %s Sekunden'],
    ['vor 1 Minute', 'in 1 Minute'],
    ['vor %s Minuten', 'in %s Minuten'],
    ['vor 1 Stunde', 'in 1 Stunde'],
    ['vor %s Stunden', 'in %s Stunden'],
    ['vor 1 Tag', 'in 1 Tag'],
    ['vor %s Tagen', 'in %s Tagen'],
    ['vor 1 Woche', 'in 1 Woche'],
    ['vor %s Wochen', 'in %s Wochen'],
    ['vor 1 Monat', 'in 1 Monat'],
    ['vor %s Monaten', 'in %s Monaten'],
    ['vor 1 Jahr', 'in 1 Jahr'],
    ['vor %s Jahren', 'in %s Jahren'],
  ][index];
});

Простые языки (одна форма множественного числа)

Для языков типа турецкого или японского, где множественное число не изменяется:

register('tr', (number, index) => {
  return [
    ['az önce', 'az sonra'],
    ['%s saniye önce', '%s saniye sonra'],
    ['1 dakika önce', '1 dakika sonra'],
    ['%s dakika önce', '%s dakika sonra'],
    ['1 saat önce', '1 saat sonra'],
    ['%s saat önce', '%s saat sonra'],
    ['1 gün önce', '1 gün sonra'],
    ['%s gün önce', '%s gün sonra'],
    ['1 hafta önce', '1 hafta sonra'],
    ['%s hafta önce', '%s hafta sonra'],
    ['1 ay önce', '1 ay sonra'],
    ['%s ay önce', '%s ay sonra'],
    ['1 yıl önce', '1 yıl sonra'],
    ['%s yıl önce', '%s yıl sonra'],
  ][index];
});

Сложные языки (несколько форм множественного числа)

Русский язык требует трёх форм в зависимости от окончания числа:

  • 1, 21, 31… → “минута”, “час”, “день”
  • 2–4, 22–24… → “минуты”, “часа”, “дня”
  • 5–20, 25–30… → “минут”, “часов”, “дней”
function pluralRu(number, one, few, many) {
  const n = Math.abs(number) % 100;
  const n1 = n % 10;
  if (n > 10 && n < 20) return many;
  if (n1 > 1 && n1 < 5) return few;
  if (n1 === 1) return one;
  return many;
}

register('ru', (number, index) => {
  return [
    ['только что', 'прямо сейчас'],
    [`${number} секунду назад`, `через ${number} секунду`],
    [`${number} ${pluralRu(number, 'секунду', 'секунды', 'секунд')} назад`,
     `через ${number} ${pluralRu(number, 'секунду', 'секунды', 'секунд')}`],
    ['1 минуту назад', 'через 1 минуту'],
    [`${number} ${pluralRu(number, 'минуту', 'минуты', 'минут')} назад`,
     `через ${number} ${pluralRu(number, 'минуту', 'минуты', 'минут')}`],
    // ... и так далее
  ][index];
});

Встроенные локали в пакете

timeago.js поставляется с набором готовых локалей. Путь к ним:

timeago.js/esm/lang/ru.js
timeago.js/esm/lang/zh_CN.js
timeago.js/esm/lang/de.js
timeago.js/esm/lang/fr.js
timeago.js/esm/lang/ja.js
timeago.js/esm/lang/ko.js
timeago.js/esm/lang/es.js

Использование встроенной локали

import { register, format } from 'timeago.js';
import ru from 'timeago.js/esm/lang/ru';

register('ru', ru);

format(Date.now() - 3600000, 'ru');
// → "1 час назад"

Принцип направленности: прошлое и будущее

Каждая локаль обязана предоставить оба варианта строки — для прошлого и для будущего. Библиотека автоматически выбирает нужный:

const [past, future] = locale(number, index);

return diff < 0 ? past : future;

Динамическая локаль

Функция локали может быть динамической — принимать решение в runtime:

register('ru_dynamic', (number, index) => {
  const base = getBaseStrings(index); // загружается из словаря
  return [
    base.past.replace('%n', number),
    base.future.replace('%n', number),
  ];
});

Итог: принципы системы локализации

  1. Функциональная модель — локаль это функция, не объект конфигурации.
  2. Индексная система — единицы времени адресуются числовыми индексами.
  3. Двунаправленность — каждый элемент возвращает пару [прошлое, будущее].
  4. Плейсхолдер %s — число подставляется в строку библиотекой.
  5. Расширяемость — любая грамматика реализуется через логику в теле функции.
  6. Глобальный реестр — один раз зарегистрированная локаль доступна везде.