Выбор стратегии локализации

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

Динамический подход предполагает, что все сообщения остаются доступными в рантайме, а форматирование выполняется непосредственно в браузере или на сервере через Intl и обёртки FormatJS.

Основной элемент этой стратегии — использование react-intl (или @formatjs/intl) без агрессивной предварительной компиляции сообщений.

Пример базовой структуры:

import { IntlProvider, FormattedMessage } from 'react-intl';

const messages = {
  ru: {
    greeting: 'Привет, {name}!',
  },
  en: {
    greeting: 'Hello, {name}!',
  },
};

export function App() {
  return (
    <IntlProvider locale="ru" messages={messages['ru']}>
      <FormattedMessage id="greeting" values={{ name: 'Alex' }} />
    </IntlProvider>
  );
}

Ключевая характеристика этого подхода — простота доставки переводов, так как они могут поставляться как JSON с сервера.

Преимущества:

  • отсутствие этапа компиляции сообщений;
  • возможность обновления переводов без пересборки приложения;
  • простая интеграция с CMS или переводческими платформами.

Ограничения:

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

Стратегия с извлечением сообщений (Message Extraction)

FormatJS предоставляет CLI-инструменты для статического анализа исходного кода и извлечения сообщений из компонентов. Это фундамент для более строгой и масштабируемой локализации.

Обычно используется пакет @formatjs/cli.

Пример команды:

formatjs extract "src/**/*.tsx" --out-file messages.json

Исходный код:

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  title: {
    id: 'page.title',
    defaultMessage: 'Главная страница',
  },
});

Результат extraction:

{
  "page.title": {
    "defaultMessage": "Главная страница"
  }
}

Данный подход позволяет:

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

Компиляция сообщений на этапе сборки

FormatJS поддерживает компиляцию ICU MessageFormat строк в оптимизированные структуры. Это снижает стоимость интерпретации сообщений в рантайме.

Используется пакет @formatjs/ts-transformer или Babel-плагин.

Пример исходного сообщения:

const message = {
  id: 'cart.items',
  defaultMessage: 'В корзине {count, plural, one {# товар} few {# товара} many {# товаров} other {# товаров}}',
};

После компиляции сообщение превращается в структуру, готовую к быстрому выполнению без полного парсинга ICU-строки в рантайме.

Преимущества:

  • ускорение рендеринга сообщений;
  • уменьшение нагрузки на клиент;
  • предсказуемость выполнения ICU-логики.

Недостатки:

  • усложнение build pipeline;
  • необходимость строгого контроля сборки;
  • увеличение времени компиляции проекта.

Гибридная стратегия

На практике наиболее распространённым является гибридный подход, при котором:

  • сообщения извлекаются статически;
  • часть ICU-компиляции выполняется на этапе сборки;
  • переводческие файлы загружаются динамически по локали;
  • критические переводы могут быть встроены в бандл.

Архитектура выглядит следующим образом:

  1. Source code содержит id + defaultMessage.
  2. CLI извлекает сообщения.
  3. Переводы хранятся в JSON per locale.
  4. На клиенте подгружается только нужная локаль.
  5. ICU интерпретация частично компилируется заранее.

Разделение переводов по чанкам

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

Пример структуры:

locales/
  ru/
    common.json
    auth.json
    dashboard.json
  en/
    common.json
    auth.json
    dashboard.json

Загрузка может быть ленивой:

async function loadMessages(locale) {
  const common = await import(`./locales/${locale}/common.json`);
  const auth = await import(`./locales/${locale}/auth.json`);

  return {
    ...common.default,
    ...auth.default,
  };
}

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


Runtime-first стратегия

Runtime-first модель предполагает минимальную предварительную обработку. Все ICU-строки остаются в исходном виде и интерпретируются в браузере через IntlMessageFormat.

Пример:

import { IntlProvider } from 'react-intl';

<IntlProvider locale="ru" messages={messages}>
  ...
</IntlProvider>

Сообщения:

{
  "price": "Цена: {value, number, currency}"
}

Особенности:

  • максимальная гибкость;
  • простота внедрения;
  • высокая нагрузка на клиент при большом объёме сообщений.

Эта стратегия подходит для небольших приложений или прототипов.


Стратегия строгой типизации переводов

TypeScript-интеграция FormatJS позволяет вводить строгие контракты для сообщений.

Пример типизации:

type Messages = {
  'page.title': string;
  'cart.items': string;
};

Или более продвинутый вариант через генерацию типов из extracted JSON:

  • CLI извлекает сообщения;
  • генерируется .d.ts файл;
  • приложение получает автодополнение ключей.

Преимущество подхода — снижение риска обращения к несуществующим ключам и улучшение качества поддержки локализации в крупных кодовых базах.


Выбор между ICU-строками и простыми шаблонами

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

Пример ICU:

{
  "notifications": "{count, plural, one {# уведомление} few {# уведомления} many {# уведомлений} other {# уведомлений}}"
}

Альтернативный подход — разбиение логики на код:

const label = plural(count, {
  one: 'уведомление',
  few: 'уведомления',
  many: 'уведомлений',
  other: 'уведомлений',
});

ICU-стратегия предпочтительнее при:

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

Кодовая стратегия — при:

  • минимальном количестве локалей;
  • необходимости сложной логики, выходящей за рамки ICU.

Стратегия загрузки локалей

Существует несколько моделей доставки переводов:

1. Полная загрузка

Все локали загружаются сразу:

import messages from './locales/all.json';

Простая, но не масштабируемая модель.

2. Ленивая загрузка по локали

Загружается только нужный набор:

const messages = await import(`./locales/${locale}.json`);

3. Гибрид с fallback

Если локаль неполная, используется базовая:

const merged = {
  ...messages['en'],
  ...messages[locale],
};

Стратегия fallback и деградации

FormatJS позволяет задавать fallback-локали и поведение при отсутствии ключей.

Типичный сценарий:

  • основная локаль: ru
  • fallback: en

Логика:

  • если перевод отсутствует в ru, используется en;
  • если отсутствует в en, используется defaultMessage.

Такой подход критически важен для стабильности интерфейса в продакшене.


Масштабирование стратегии в крупных приложениях

В больших системах стратегия локализации перестаёт быть техническим решением и становится архитектурным слоем.

Типовая модель включает:

  • extraction pipeline;
  • translation management system (TMS);
  • versioned message catalogs;
  • CI-проверки отсутствующих ключей;
  • runtime загрузчик локалей;
  • ICU-компиляцию на build этапе.

Особое значение приобретает контроль:

  • несоответствия ключей между локалями;
  • устаревших переводов;
  • дублирующихся сообщений;
  • несогласованных ICU-структур.

Баланс между стратегиями

Выбор подхода определяется не синтаксисом FormatJS, а требованиями системы:

  • скорость загрузки → compile-time + code splitting;
  • частые изменения текстов → runtime-first;
  • корпоративная локализация → extraction + TMS + ICU;
  • небольшие проекты → динамическая стратегия без сборки;
  • высоконагруженные интерфейсы → частичная компиляция ICU.

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