Даты и время

В экосистеме i18next работа с датами и временем строится не как отдельная подсистема, а как расширение механизма интерполяции. Базовая идея заключается в том, что перевод строки может содержать динамические значения, которые подставляются в момент выполнения, включая даты, числа и вычисляемые строки.

Стандартная интерполяция выглядит следующим образом:

i18next.t('lastVisit', {
  date: new Date()
});

Ресурс перевода:

{
  "lastVisit": "Последний визит: {{date}}"
}

По умолчанию объект Date преобразуется в строку через toString(), что почти всегда неприемлемо для локализации. Поэтому применяется форматирование через кастомные функции или встроенные форматтеры.


Форматирование через интерполяционные форматтеры

Механизм форматирования в i18next позволяет применять функции к интерполяционным значениям.

Регистрация форматтера:

i18next.services.formatter.add('datetime', (value, lng, options) => {
  return new Intl.DateTimeFormat(lng, options).format(value);
});

Использование в переводах:

{
  "lastVisit": "Последний визит: {{date, datetime}}"
}

Передача параметров формата:

i18next.t('lastVisit', {
  date: new Date(),
  formatParams: {
    date: {
      year: 'numeric',
      month: 'long',
      day: '2-digit'
    }
  }
});

Использование Intl.DateTimeFormat как базового слоя

Современный стандарт форматирования дат основан на Intl.DateTimeFormat. Он обеспечивает локализацию без сторонних зависимостей.

Пример прямого форматирования:

const formatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit'
});

formatter.format(new Date());

Интеграция с i18next обычно реализуется через обёртку форматтера:

i18next.services.formatter.add('date', (value, lng, options) => {
  return new Intl.DateTimeFormat(lng, options).format(value);
});

Разделение локали и формата

Формат даты определяется двумя независимыми параметрами:

  • локаль (ru, en, de)
  • набор опций форматирования

Такой подход позволяет переиспользовать один и тот же ключ перевода:

{
  "createdAt": "Создано: {{date, date}}"
}
i18next.t('createdAt', {
  date: new Date(),
  lng: 'ru',
  formatParams: {
    date: {
      dateStyle: 'long',
      timeStyle: 'short'
    }
  }
});

Поддержка времени и часовых поясов

Часовые пояса в JavaScript не хранятся в объекте Date, а вычисляются при форматировании. В i18n-слое это создаёт важный слой абстракции: форматтер обязан учитывать целевую локаль, но не всегда — временную зону пользователя.

Форматирование с явным часовым поясом:

new Intl.DateTimeFormat('ru-RU', {
  timeZone: 'Europe/Moscow',
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit'
}).format(new Date());

В связке с i18next это используется в пользовательских форматтерах:

i18next.services.formatter.add('datetimeTz', (value, lng, options) => {
  return new Intl.DateTimeFormat(lng, {
    ...options,
    timeZone: options.timeZone || 'UTC'
  }).format(value);
});

ICU-подход и форматирование дат

ICU-сообщения (International Components for Unicode) предоставляют расширенный синтаксис для интерполяции и форматирования.

При использовании ICU-плагина для i18next поддерживаются конструкции:

{
  "eventDate": "Событие состоится {date, date, long}"
}

Поддерживаемые типы:

  • date
  • time
  • number
  • plural

ICU обеспечивает стандартизированную модель, где форматирование отделено от кода и описывается декларативно.


Кастомные стратегии форматирования

Системы реального уровня редко ограничиваются Intl. Часто вводятся дополнительные слои:

Форматирование через dayjs

import dayjs from 'dayjs';

i18next.services.formatter.add('dayjs', (value, lng, options) => {
  return dayjs(value).format(options.format || 'YYYY-MM-DD');
});

Использование:

{
  "upd ated": "Обновлено: {{date, dayjs}}"
}

Форматирование через moment (устаревший подход)

import moment from 'moment';

i18next.services.formatter.add('moment', (value, lng, options) => {
  return moment(value).format(options.format);
});

Несмотря на распространённость, данный подход постепенно вытесняется из-за избыточного веса библиотеки и проблем с tree-shaking.


Сериализация дат в SSR и hydration

При серверном рендеринге значения дат передаются как строки, чтобы избежать различий между сервером и клиентом.

Проблемный вариант:

{
  date: new Date()
}

Корректный вариант:

{
  date: new Date().toISOString()
}

На клиенте происходит восстановление:

new Date(dateString)

В связке с i18next форматтеры применяются уже после гидратации, что обеспечивает стабильный результат.


Локализация относительного времени

Относительное время («2 минуты назад») не входит в базовый i18next, но часто реализуется через Intl.RelativeTimeFormat.

const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

rtf.format(-2, 'minute');

Интеграция:

i18next.services.formatter.add('relativeTime', (value, lng, options) => {
  const rtf = new Intl.RelativeTimeFormat(lng, options);
  return rtf.format(value.amount, value.unit);
});

Пример использования:

{
  "lastSeen": "Был в сети {{time, relativeTime}}"
}

Нормализация входных данных

Для стабильного форматирования даты требуется унификация входного типа:

Поддерживаемые формы:

  • Date
  • timestamp (number)
  • ISO-строка

Нормализация:

function normalizeDate(value) {
  if (value instanceof Date) return value;
  if (typeof value === 'number') return new Date(value);
  return new Date(Date.parse(value));
}

Форматтер:

i18next.services.formatter.add('date', (value, lng, options) => {
  const date = normalizeDate(value);
  return new Intl.DateTimeFormat(lng, options).format(date);
});

Производительность форматирования

Форматирование дат является дорогостоящей операцией при массовом рендеринге списков. В рамках i18next применяется несколько стратегий оптимизации:

  • кэширование Intl.DateTimeFormat
  • мемоизация форматтеров по (lng + options)
  • предварительное форматирование на уровне селекторов данных

Пример кэша:

const cache = new Map();

function getFormatter(lng, options) {
  const key = lng + JSON.stringify(options);
  if (!cache.has(key)) {
    cache.se t(key, new Intl.DateTimeFormat(lng, options));
  }
  return cache.get(key);
}

Работа с календарными локалями

Некоторые языки используют альтернативные календари (исламский, буддийский, японский). Intl.DateTimeFormat поддерживает параметр calendar.

new Intl.DateTimeFormat('ar-SA-u-ca-islamic', {
  day: 'numeric',
  month: 'long',
  year: 'numeric'
}).format(new Date());

Интеграция через i18next:

i18next.services.formatter.add('calendarDate', (value, lng, options) => {
  return new Intl.DateTimeFormat(lng, {
    ...options
  }).format(value);
});

Интеграция с языковыми переключателями

При смене языка форматирование дат автоматически меняется за счёт передачи lng в форматтер:

i18next.changeLanguage('de');

Дальнейшие вызовы:

i18next.t('createdAt', { date: new Date() });

дают локализованный результат без изменения бизнес-логики.


Ограничения модели форматирования

Форматирование дат в i18n-слое имеет ряд структурных ограничений:

  • отсутствие встроенного парсинга естественных языковых дат
  • зависимость от Intl в среде выполнения
  • неоднозначность локального времени без явного timeZone
  • невозможность унифицированного формата без договорённостей уровня приложения

Эти ограничения компенсируются внешними библиотеками и строгой нормализацией входных данных, а также единым форматтер-слоем внутри i18next