Locale-специфичное форматирование

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

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


Механизм интерполяции i18next использует шаблоны вида:

t('price', { value: 1234.56 })

И строку перевода:

{
  "price": "{{value}}"
}

Без дополнительной настройки значение просто вставляется как есть. Однако интерполяция поддерживает форматирование через расширенный синтаксис:

t('price', { value: 1234.56 })
{
  "price": "{{value, currency}}"
}

Здесь currency — идентификатор форматтера, который обрабатывает значение до подстановки в строку.


Форматирование чисел через Intl.NumberFormat

Основой локализованного представления чисел является Intl.NumberFormat. Он учитывает:

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

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

import i18next from 'i18next';

i18next.init({
  lng: 'en',
  resources: {
    en: {
      translation: {
        price: '{{value, currency}}'
      }
    }
  }
});

Добавление форматтера:

i18next.services.formatter.add('currency', (value, lng) => {
  return new Intl.NumberFormat(lng, {
    style: 'currency',
    currency: lng === 'ru' ? 'RUB' : 'USD'
  }).format(value);
});

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

  • 1,234.56 $ для en
  • 1 234,56 ₽ для ru

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


Локализация дат и времени

Работа с датами требует учета календарных форматов, порядка компонентов и локальных названий месяцев и дней недели. Встроенный Intl.DateTimeFormat решает эти задачи:

i18next.services.formatter.add('date', (value, lng, options) => {
  return new Intl.DateTimeFormat(lng, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    ...options
  }).format(new Date(value));
});

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

{
  "created": "Создано: {{date, date}}"
}

Вызов:

t('created', { date: Date.now() })

Поведение зависит от локали:

  • 29 May 2026 в английской локали
  • 29 мая 2026 г. в русской локали

Форматтер может принимать дополнительные параметры:

t('created', {
  date: Date.now(),
  formatParams: {
    date: { weekday: 'long' }
  }
})

Механизм форматтеров i18next

Внутри i18next форматирование реализуется через i18next.services.formatter. Он представляет собой слой, который:

  • принимает значение из интерполяции
  • определяет имя форматтера
  • вызывает зарегистрированную функцию
  • возвращает преобразованное значение

Регистрация базируется на имени:

i18next.services.formatter.add('uppercase', (value) => {
  return String(value).toUpperCase();
});

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

{
  "welcome": "{{name, uppercase}}"
}

Такой подход позволяет создавать доменно-специфичные преобразования без изменения переводов.


Параметризация форматтеров

Форматтеры могут принимать дополнительные аргументы через интерполяцию:

{
  "bytes": "{{value, bytes}}"
}
i18next.services.formatter.add('bytes', (value, lng, options) => {
  const units = ['B', 'KB', 'MB', 'GB'];
  let i = 0;
  let val = value;

  while (val >= 1024 && i < units.length - 1) {
    val /= 1024;
    i++;
  }

  return `${val.toFixed(1)} ${units[i]}`;
});

Расширенные параметры позволяют учитывать контекст:

{
  "download": "{{size, bytes}} файл"
}

Связь локали и форматирования

Ключевая особенность i18next — синхронизация языка перевода и языка форматирования. При смене lng:

i18next.changeLanguage('ru');

все форматтеры получают обновленный локальный контекст. Это важно, поскольку:

  • числовые разделители зависят от локали
  • валютные символы различаются
  • порядок даты меняется
  • грамматические формы могут зависеть от числовых значений

Форматтеры не хранят состояние языка, а получают его как аргумент, что исключает рассинхронизацию.


Fallback-локали и деградация формата

При отсутствии поддержки конкретной локали Intl использует fallback-цепочку. В i18next это соответствует fallbackLng:

i18next.init({
  lng: 'kk',
  fallbackLng: 'en'
});

Если форматтер получает локаль, которую Intl не поддерживает напрямую, он автоматически переходит к ближайшему доступному варианту.

Это важно для чисел и дат, поскольку:

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

Комбинирование нескольких форматтеров

i18next допускает цепочки преобразований через вложенную интерполяцию:

{
  "report": "{{value, number}} ({{value, percent}})"
}

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


Контекстные форматтеры и динамическое поведение

Форматтер может учитывать не только значение и язык, но и дополнительные параметры:

i18next.services.formatter.add('status', (value, lng, options) => {
  if (options.context === 'api') {
    return `[API] ${value}`;
  }
  return value;
});

В переводе:

{
  "state": "{{state, status}}"
}

Вызов:

t('state', {
  state: 'active',
  formatParams: {
    state: { context: 'api' }
  }
});

Такой механизм позволяет разделять представление данных для разных слоев приложения.


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

Использование Intl может быть затратным при частых вызовах, особенно при рендеринге списков. Оптимизация обычно строится вокруг:

  • кэширования Intl.NumberFormat и Intl.DateTimeFormat
  • повторного использования форматтеров i18next
  • минимизации пересоздания объектов опций

Пример кэширования:

const cache = new Map();

function getFormatter(lng) {
  if (!cache.has(lng)) {
    cache.set(lng, new Intl.NumberFormat(lng));
  }
  return cache.get(lng);
}

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


Особенности работы с разными системами счисления и символами

Некоторые локали используют альтернативные наборы цифр или нестандартные разделители. Intl учитывает:

  • арабские и индийские цифры
  • неразрывные пробелы
  • специфические символы валют

i18next не преобразует эти данные самостоятельно, а полагается на системный слой форматирования, что обеспечивает согласованность с браузерной или Node.js реализацией.


Интеграция с кастомными пайплайнами данных

Форматирование в i18next часто используется как последний шаг перед отображением данных. Типичный поток:

  1. получение сырого значения
  2. передача в t()
  3. интерполяция
  4. вызов форматтера
  5. возврат локализованной строки

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


Обработка нестандартных значений

Форматтеры должны учитывать некорректные входные данные:

  • null и undefined
  • строки вместо чисел
  • некорректные даты

Типичная защита:

i18next.services.formatter.add('safeNumber', (value, lng) => {
  const num = Number(value);
  if (Number.isNaN(num)) return '0';
  return new Intl.NumberFormat(lng).format(num);
});

Это предотвращает разрыв отображения при ошибках данных на уровне приложения.


Влияние направления текста (LTR/RTL)

Хотя i18next не занимается напрямую визуальным направлением текста, локаль влияет на форматирование чисел и дат в сочетании с RTL-языками. В таких сценариях:

  • разделители могут отображаться зеркально
  • смешанный текст требует корректного Unicode ordering
  • числовые значения сохраняют порядок, но окружение меняется

Форматирование должно учитывать контекст рендеринга, особенно в UI-компонентах, где RTL включается на уровне документа.