Компиляция сообщений для production

В системах интернационализации на базе ICU MessageFormat исходные строки сообщений часто содержат сложную структуру: склонения, множественные формы, условия, вложенные выражения. Прямое выполнение таких шаблонов в рантайме приводит к значительным издержкам: парсинг строки, построение AST и вычисление результата для каждой отрисовки интерфейса.

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

Ключевой эффект такого подхода заключается в том, что:

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

Архитектура компиляции в экосистеме FormatJS

FormatJS разделяет процесс работы с сообщениями на несколько этапов:

  1. Извлечение сообщений из исходного кода
  2. Сериализация в промежуточный формат (JSON)
  3. Компиляция ICU-строк в JavaScript AST
  4. Генерация оптимизированного кода
  5. Использование в рантайме без парсинга ICU

Основной инструмент компиляции — пакет babel-plugin-react-intl и утилиты @formatjs/cli.


Извлечение сообщений из кода

Первый этап — статический анализ исходного кода. Babel-плагин проходит по AST и находит вызовы форматирующих функций:

  • defineMessages
  • intl.formatMessage
  • JSX-компоненты <FormattedMessage />

Пример исходного кода:

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  title: {
    id: 'app.title',
    defaultMessage: 'Привет, {name}!'
  }
});

После извлечения формируется JSON-каталог:

{
  "app.title": {
    "defaultMessage": "Привет, {name}!"
  }
}

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


ICU MessageFormat как промежуточный язык

FormatJS использует ICU MessageFormat как стандарт описания сообщений:

Привет, {name}!
У вас {count, plural, one {# сообщение} few {# сообщения} many {# сообщений} other {# сообщений}}

ICU-строки являются декларативными, но не исполняемыми напрямую. Их необходимо преобразовать в исполняемый код.


Компиляция ICU в JavaScript-код

Компилятор @formatjs/icu-messageformat-parser разбирает строку и строит AST, после чего генератор кода превращает его в оптимизированную функцию.

Исходное сообщение:

У вас {count, plural, one {# письмо} other {# писем}}

После компиляции:

function (values) {
  switch (getPluralRule(values.count)) {
    case 'one':
      return "У вас " + values.count + " письмо";
    default:
      return "У вас " + values.count + " писем";
  }
}

Важный момент: вся структура ICU исчезает из рантайма, остаётся только логика ветвления.


Роль @formatjs/cli в production-сборке

CLI-инструмент @formatjs/cli используется для автоматизации подготовки сообщений:

  • извлечение (extract)
  • проверка (validate)
  • компиляция (compile)

Типичный production-пайплайн:

formatjs extract "src/**/*.ts" --out-file messages.json
formatjs compile messages.json --out-file compiled-messages.json

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


Babel-плагин и inline-компиляция

babel-plugin-react-intl позволяет компилировать сообщения прямо во время сборки приложения.

Конфигурация:

{
  "plugins": [
    ["react-intl", {
      "compile": true,
      "removeDefaultMessage": false
    }]
  ]
}

После трансформации:

Исходный JSX:

<FormattedMessage id="greeting" defaultMessage="Привет, {name}" values={{ name }} />

Преобразуется в:

intl.formatMessage(
  { id: "greeting", defaultMessage: "Привет, {name}" },
  { name }
);

Если включена полная компиляция, formatMessage заменяется на заранее сгенерированную функцию.


Удаление runtime-парсера ICU

Одно из ключевых улучшений production-сборки — исключение парсера ICU из конечного бандла.

Без компиляции:

  • ICU-строка парсится в AST при каждом использовании
  • выполняется интерпретация структуры

С компиляцией:

  • AST уже преобразован в JS-код
  • парсер не включается в бандл
  • остаются только функции ветвления

Это особенно важно для мобильных и low-end устройств.


Оптимизация размера бандла

Компиляция сообщений влияет на размер сборки в нескольких направлениях:

  1. Удаление парсера ICU
  2. Упрощение структуры сообщений
  3. Возможность tree-shaking локалей
  4. Инлайнинг строк

Пример сравнения:

Без компиляции:

  • runtime ICU parser + messages JSON

С компиляцией:

  • только функции форматирования
  • минимальный runtime слой

В крупных приложениях разница может составлять десятки процентов от общего размера i18n-бандла.


Разделение локалей и code splitting

Production-архитектура часто предполагает раздельную загрузку локалей:

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

Компиляция позволяет:

  • хранить локали в виде JS-функций или оптимизированных структур
  • загружать только нужный язык
  • избегать повторного парсинга сообщений

Хеширование сообщений и кэширование

Компилированные сообщения часто сопровождаются хешированием:

{
  "app.title": {
    "hash": "a94f3b",
    "fn": function(values) { ... }
  }
}

Хеш используется для:

  • проверки актуальности переводов
  • кэширования в CDN
  • быстрого сравнения версий локалей

Обработка сложных ICU-конструкций

Компилятор FormatJS поддерживает все основные конструкции ICU:

  • pluralization
  • select
  • selectordinal
  • вложенные выражения

Пример:

{gender, select,
  male {Он добавил {count, plural, one {# файл} other {# файлов}}}
  female {Она добавила {count, plural, one {# файл} other {# файлов}}}
  other {Добавлено {count} файлов}
}

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


Производственные ограничения и стратегии

Компиляция сообщений требует контроля нескольких аспектов:

  • детерминированность входных данных — сообщения должны быть статическими
  • избежание динамических ICU-строк — иначе компиляция невозможна
  • синхронизация переводов — изменение defaultMessage требует пересборки

В крупных системах применяется стратегия:

  • заморозка ICU-форматов
  • версионирование сообщений
  • отдельный pipeline для локалей

Интеграция с современными сборщиками

FormatJS компиляция интегрируется с:

  • Webpack (через Babel loader)
  • Vite (через plugin-babel)
  • Rollup
  • esbuild (через трансформационные плагины)

В каждом случае цель одинакова: перенести ICU-логику из runtime в build-time слой.


Runtime-слой после компиляции

После production-компиляции runtime-часть FormatJS выполняет минимальную роль:

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

Фактически runtime становится тонким слоем оркестрации, а не интерпретатором сообщений.


Предсказуемость производительности

Компиляция сообщений обеспечивает стабильное поведение интерфейса:

  • отсутствие лагов при первом рендере перевода
  • линейное выполнение форматирования
  • отсутствие зависимости от сложности ICU-строки

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