Предкомпиляция сообщений

В экосистеме FormatJS сообщения обычно описываются в формате ICU Message Syntax и компилируются во время выполнения приложения. Такой подход удобен на этапе разработки, однако в крупных приложениях возникают дополнительные издержки:

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

Предкомпиляция сообщений решает эти проблемы путём преобразования ICU-строк в сериализованные AST-структуры ещё на этапе сборки.

Пример обычного сообщения:

{
  "title": "Hello, {name}!"
}

После предкомпиляции сообщение превращается в массив токенов:

{
  "title": [
    {
      "type": 0,
      "value": "Hello, "
    },
    {
      "type": 1,
      "value": "name"
    },
    {
      "type": 0,
      "value": "!"
    }
  ]
}

Во время выполнения FormatJS больше не требуется разбирать ICU-строку — библиотека сразу использует готовое AST-представление.


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

Внутри FormatJS используется пакет:

@formatjs/icu-messageformat-parser

Он преобразует ICU Message Syntax в AST.

Этапы обработки:

  1. чтение ICU-строки;
  2. лексический анализ;
  3. построение дерева токенов;
  4. сериализация результата;
  5. использование AST через IntlMessageFormat.

Схема обработки:

ICU Message
     ↓
Parser
     ↓
AST
     ↓
Serialized JSON
     ↓
Runtime Formatting

ICU Message Syntax и AST

Сообщение:

Hello, {name}!

После парсинга превращается в AST:

[
  {
    type: 0,
    value: 'Hello, '
  },
  {
    type: 1,
    value: 'name'
  },
  {
    type: 0,
    value: '!'
  }
]

Типы токенов:

Type Назначение
0 обычный текст
1 переменная
6 plural
5 select
8 tag

Установка инструментов предкомпиляции

Основной CLI-инструмент:

npm install --save-dev @formatjs/cli

Проверка установки:

npx formatjs --help

Компиляция переводов через CLI

Структура проекта:

src/
  locales/
    en.json
    ru.json

Содержимое файла:

{
  "hello": "Hello, {name}!"
}

Компиляция:

npx formatjs compile src/locales/en.json --out-file dist/en.json

Результат:

{
  "hello": [
    {
      "type": 0,
      "value": "Hello, "
    },
    {
      "type": 1,
      "value": "name"
    },
    {
      "type": 0,
      "value": "!"
    }
  ]
}

Компиляция нескольких локалей

npx formatjs compile-folder src/locales compiled-locales

Структура после обработки:

compiled-locales/
  en.json
  ru.json

Использование предкомпилированных сообщений в React

Исходный вариант

<IntlProvider
  locale="en"
  messages={messages}
>
  <App />
</IntlProvider>

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

import messages from './compiled-locales/en.json';

<IntlProvider
  locale="en"
  messages={messages}
>
  <App />
</IntlProvider>

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


Почему предкомпиляция ускоряет приложение

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

Runtime:
ICU string → parse → AST → format

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

Build:
ICU string → parse → AST

Runtime:
AST → format

Наиболее тяжёлая часть — разбор ICU — переносится из браузера в этап сборки.


Исключение ICU-парсера из production-бандла

После перехода на AST можно удалить runtime-парсер из клиентской сборки.

Это уменьшает:

  • размер JavaScript-бандла;
  • время загрузки;
  • объём памяти;
  • нагрузку при гидратации React.

Особенно заметен эффект в:

  • мобильных приложениях;
  • SSR;
  • микрофронтендах;
  • больших enterprise-системах.

Предкомпиляция и tree-shaking

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

AST-представление позволяет bundler’у эффективнее выполнять:

  • tree-shaking;
  • dead code elimination;
  • code splitting.

Webpack, Rollup и Vite работают значительно эффективнее с уже скомпилированными сообщениями.


Компиляция с минификацией AST

FormatJS поддерживает дополнительную оптимизацию.

Пример:

npx formatjs compile src/locales/en.json \
  --ast \
  --out-file dist/en.json

Минифицированное AST:

[
  {
    "type": 0,
    "value": "Hello "
  },
  {
    "type": 1,
    "value": "name"
  }
]

Размер JSON-файлов уменьшается особенно заметно при больших plural-конструкциях.


Работа с plural-сообщениями

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

{
  "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}

Предкомпилированная версия:

{
  "items": [
    {
      "type": 6,
      "value": "count",
      "options": {
        "=0": {
          "value": [
            {
              "type": 0,
              "value": "No items"
            }
          ]
        },
        "one": {
          "value": [
            {
              "type": 7
            },
            {
              "type": 0,
              "value": " item"
            }
          ]
        },
        "other": {
          "value": [
            {
              "type": 7
            },
            {
              "type": 0,
              "value": " items"
            }
          ]
        }
      }
    }
  ]
}

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


Интеграция с Babel

FormatJS предоставляет Babel-плагин:

npm install --save-dev babel-plugin-formatjs

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

module.exports = {
  plugins: [
    [
      'formatjs',
      {
        ast: true
      }
    ]
  ]
};

Теперь сообщения компилируются автоматически во время transpilation.


Интеграция с TypeScript

Типизация AST-сообщений:

import {MessageFormatElement} from 'react-intl';

type Messages = Record<
  string,
  MessageFormatElement[]
>;

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

import messages from './compiled/en.json';

const localized: Messages = messages;

Предкомпиляция в Vite

Пример build-скрипта:

{
  "scripts": {
    "i18n:compile": "formatjs compile-folder src/locales src/compiled-locales"
  }
}

Интеграция в pipeline:

{
  "scripts": {
    "build": "npm run i18n:compile && vite build"
  }
}

Предкомпиляция в Webpack

Пример через npm scripts:

{
  "scripts": {
    "prebuild": "formatjs compile-folder src/locales compiled",
    "build": "webpack"
  }
}

Использование compile-folder

Команда:

formatjs compile-folder input output

Поддерживает:

  • множественные локали;
  • рекурсивную обработку;
  • AST-компиляцию;
  • автоматическую сериализацию.

Пример:

npx formatjs compile-folder lang compiled-lang --ast

Флаг --format

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

Пример:

npx formatjs compile src/en.json \
  --format simple

Возможные форматы:

Формат Назначение
simple упрощённая структура
smartling интеграция Smartling
crowdin интеграция Crowdin

Предкомпиляция и SSR

При серверном рендеринге предкомпиляция особенно важна.

Без AST:

Request
  ↓
Server parses ICU
  ↓
HTML render

С AST:

Request
  ↓
Ready AST
  ↓
HTML render

Это уменьшает:

  • CPU-нагрузку сервера;
  • latency;
  • TTFB;
  • время гидратации.

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

Предкомпилированные AST-файлы удобно кэшировать:

CDN
Browser Cache
Memory Cache
SSR Cache

Поскольку сообщения становятся статическими JSON-структурами, их можно эффективно хранить и переиспользовать.


Динамическая загрузка локалей

Пример lazy loading:

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

В сочетании с AST это даёт:

  • меньшие чанки;
  • быстрое переключение локалей;
  • минимальную runtime-нагрузку.

Предкомпиляция и React Native

В мобильной среде преимущества особенно заметны:

  • слабые CPU;
  • ограниченная память;
  • чувствительность к размеру bundle.

AST уменьшает количество runtime-операций и ускоряет startup приложения.


Использование compile в Node.js

Программный API:

const {compile} = require('@formatjs/cli-lib');

const result = compile(
  {
    hello: 'Hello {name}'
  },
  {
    ast: true
  }
);

Предкомпиляция и CI/CD

Часто используется отдельный pipeline:

Extract Messages
      ↓
Translate
      ↓
Compile AST
      ↓
Build Application

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

npm run i18n:compile

Проверка ошибок во время компиляции

FormatJS валидирует ICU-синтаксис.

Ошибка:

Expected "}" but end of input found

Пример некорректного сообщения:

Hello {name

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


Обработка missing translations

Пример проверки:

formatjs compile-folder lang compiled \
  --throws

Сборка завершится ошибкой при некорректных сообщениях.


Оптимизация больших словарей

При тысячах сообщений рекомендуется:

  • разделять локали по модулям;
  • использовать code splitting;
  • компилировать AST заранее;
  • хранить сообщения в отдельных чанках.

Пример:

locales/
  dashboard/
  profile/
  admin/

Использование хеширования

Часто применяется:

en.a1f2c3.json
ru.b8d7e2.json

Это улучшает:

  • CDN cache invalidation;
  • долгосрочное кэширование;
  • контроль версий локалей.

Сравнение runtime и precompiled подходов

Характеристика Runtime ICU Precompiled AST
Парсинг в браузере Да Нет
Размер runtime Больше Меньше
Скорость startup Ниже Выше
SSR производительность Ниже Выше
Проверка ошибок Частично На build-этапе
Оптимизация bundler Ограничена Лучше

Когда предкомпиляция обязательна

Практически необходима при:

  • крупных React-приложениях;
  • SSR;
  • Next.js;
  • React Native;
  • enterprise SPA;
  • low-end устройствах;
  • большом количестве plural/select сообщений;
  • мультиязычных системах с десятками локалей.

Ограничения предкомпиляции

AST-файлы:

  • менее читаемы;
  • сложнее редактируются вручную;
  • увеличивают объём JSON относительно простых строк в некоторых сценариях;
  • требуют build-step.

Поэтому исходные ICU-сообщения обычно хранят отдельно от compiled-версий.


Рекомендуемая структура проекта

src/
  locales/
    raw/
      en.json
      ru.json

    compiled/
      en.json
      ru.json

Рекомендуемый production pipeline

ICU Messages
     ↓
Translation Platform
     ↓
FormatJS Compile
     ↓
AST JSON
     ↓
Bundler
     ↓
Production Build

Практика хранения исходников и AST отдельно

Исходные файлы:

{
  "welcome": "Welcome, {name}"
}

Compiled:

{
  "welcome": [
    {
      "type": 0,
      "value": "Welcome, "
    },
    {
      "type": 1,
      "value": "name"
    }
  ]
}

Редактирование выполняется только в raw-локалях.


Совместимость с react-intl

react-intl полностью поддерживает AST-сообщения:

<IntlProvider messages={compiledMessages}>

Дополнительная конфигурация не требуется.


Производительность на больших объёмах

При тысячах сообщений предкомпиляция позволяет:

  • сократить parse-time;
  • уменьшить нагрузку GC;
  • снизить CPU spikes;
  • ускорить hydration;
  • уменьшить blocking time.

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


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

Типичная схема:

packages/
  ui/
  shared-i18n/
  admin/
  mobile/

Пакет shared-i18n содержит:

  • исходные ICU;
  • compiled AST;
  • типы;
  • генераторы локалей.

Это позволяет переиспользовать переводы между приложениями.