Числа и валюты

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

Ключевой элемент — интерполяция:

i18next.t('key', { value: 1234.56 })

Шаблон перевода:

{
  "key": "Значение: {{value}}"
}

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


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

Стандартный способ локализации чисел в i18next реализуется через кастомный форматтер.

Инициализация i18next с поддержкой форматирования:

import i18next from 'i18next';

i18next.init({
  lng: 'ru',
  resources: {
    ru: {
      translation: {
        price: 'Цена: {{value, number}}',
        amount: 'Количество: {{value, number}}'
      }
    }
  },
  interpolation: {
    format: (value, format, lng) => {
      if (format === 'number') {
        return new Intl.NumberFormat(lng).format(value);
      }
      return value;
    }
  }
});

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

i18next.t('price', { value: 1234567.89 });

Результат для ru:

Цена: 1 234 567,89

Разделение логики форматирования и переводов

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

Шаблон:

{
  "balance": "Баланс: {{amount, number}}"
}

Код:

i18next.t('balance', { amount: 10000 });

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


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

Поддержка валют реализуется через параметр currency в Intl.NumberFormat.

Расширение форматтера:

i18next.init({
  lng: 'en',
  interpolation: {
    format: (value, format, lng) => {
      if (format?.startsWith('currency')) {
        const [, currency] = format.split(':');

        return new Intl.NumberFormat(lng, {
          style: 'currency',
          currency
        }).format(value);
      }

      if (format === 'number') {
        return new Intl.NumberFormat(lng).format(value);
      }

      return value;
    }
  }
});

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

Шаблоны:

{
  "price_usd": "Стоимость: {{value, currency:USD}}",
  "price_eur": "Стоимость: {{value, currency:EUR}}"
}

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

i18next.t('price_usd', { value: 1999.99 });
i18next.t('price_eur', { value: 1999.99 });

Результаты зависят от локали:

  • en: $1,999.99
  • de: 1.999,99 €
  • ru: 1 999,99 $ (в зависимости от настроек локали)

Разделители, округление и точность

Intl.NumberFormat позволяет управлять точностью чисел.

Расширенный форматтер:

if (format === 'number_fixed') {
  return new Intl.NumberFormat(lng, {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(value);
}

Пример шаблона:

{
  "weight": "Вес: {{value, number_fixed}} кг"
}

Результат:

Вес: 12,00 кг

Проценты и относительные значения

Процентное форматирование реализуется через style: 'percent'.

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

if (format === 'percent') {
  return new Intl.NumberFormat(lng, {
    style: 'percent',
    minimumFractionDigits: 0,
    maximumFractionDigits: 2
  }).format(value);
}

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

{
  "progress": "Прогресс: {{value, percent}}"
}

Входное значение должно быть в диапазоне 0–1:

i18next.t('progress', { value: 0.42 });

Результат:

Прогресс: 42%

Обработка валют с кодами и символами

В ряде систем требуется отображение не только символа, но и ISO-кода валюты.

Расширенный вариант:

if (format?.startsWith('currencyCode')) {
  const [, currency] = format.split(':');

  return new Intl.NumberFormat(lng, {
    style: 'currency',
    currency,
    currencyDisplay: 'code'
  }).format(value);
}

Шаблон:

{
  "invoice": "Счёт: {{value, currencyCode:USD}}"
}

Результат:

Счёт: 1,999.99 USD

Локализация группировки разрядов

Разные локали используют разные разделители:

  • пробел (ru)
  • запятая (en-US)
  • точка (de)

Пример:

new Intl.NumberFormat('de-DE').format(1234567.89);

Результат:

1.234.567,89

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


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

Форматы могут комбинироваться через расширение логики:

if (format?.startsWith('currency_fixed')) {
  const [, currency] = format.split(':');

  return new Intl.NumberFormat(lng, {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(value);
}

Шаблон:

{
  "total": "Итого: {{value, currency_fixed:EUR}}"
}

Безопасность и предсказуемость значений

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

Правильный подход — использование чистых функций:

format: (value, format, lng) => {
  const formatter = new Intl.NumberFormat(lng);
  return formatter.format(value);
}

Интеграция с серверным рендерингом

При SSR важно фиксировать локаль:

i18next.init({
  lng: 'ru-RU',
  interpolation: {
    format: (value, format, lng) => {
      return new Intl.NumberFormat(lng).format(value);
    }
  }
});

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


Использование плагинов форматирования

В экосистеме i18next существуют расширения, упрощающие работу с форматированием:

  • ICU-подобные интерполяции
  • форматтеры дат и чисел
  • плагины для React/Vue с встроенной локализацией

Общий принцип остаётся неизменным: значения проходят через интерполяцию, а форматирование определяется отдельным слоем логики.


Особенности работы с плавающей точкой

JavaScript использует IEEE 754, что приводит к неточностям:

0.1 + 0.2 // 0.30000000000000004

Форматирование через Intl.NumberFormat скрывает эти особенности:

new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 2
}).format(0.1 + 0.2);

Результат:

0.30

Унификация форматов в проекте

В крупных приложениях форматтеры выносятся в отдельный модуль:

export const formatters = {
  number: (value, lng) =>
    new Intl.NumberFormat(lng).format(value),

  currency: (value, lng, currency) =>
    new Intl.NumberFormat(lng, {
      style: 'currency',
      currency
    }).format(value)
};

И подключаются в i18next:

format: (value, format, lng) => {
  if (format === 'number') return formatters.number(value, lng);
  if (format?.startsWith('currency')) {
    const [, currency] = format.split(':');
    return formatters.currency(value, lng, currency);
  }
  return value;
}

Поведение при отсутствии формата

Если формат не распознан, значение возвращается без изменений. Это обеспечивает устойчивость системы при ошибках в ключах перевода или неправильных параметрах интерполяции:

return value;