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

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

Извлечение сообщений выполняет функцию промежуточного слоя между кодовой базой и системами локализации. Исходный код содержит декларативные описания сообщений, а итоговые файлы переводов формируются автоматически на основе анализа AST (Abstract Syntax Tree).

Основные задачи процесса:

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

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

Формат описания сообщений

В React и других JavaScript-приложениях на базе FormatJS сообщения обычно задаются через defineMessages или компонент FormattedMessage.

Пример декларации:

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  welcome: {
    id: 'app.welcome',
    defaultMessage: 'Добро пожаловать в систему',
    description: 'Приветствие на главной странице'
  },
  logout: {
    id: 'app.logout',
    defaultMessage: 'Выйти'
  }
});

Каждое сообщение содержит:

  • id — стабильный идентификатор, используемый в файлах переводов;
  • defaultMessage — текст по умолчанию, выступающий как исходный язык;
  • description — контекст для переводчиков, не влияющий на выполнение приложения.

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

import { FormattedMessage } from 'react-intl';

<FormattedMessage
  id="app.title"
  defaultMessage="Панель управления"
/>

Механизм статического анализа

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

Основной инструмент — babel-plugin-react-intl.

Принцип работы:

  1. Парсинг JavaScript/TypeScript кода в AST.
  2. Поиск вызовов defineMessages, formatMessage, FormattedMessage.
  3. Извлечение литералов defaultMessage.
  4. Формирование промежуточного JSON-каталога.

Конфигурация Babel-плагина

Настройка плагина выполняется через Babel-конфигурацию:

{
  "plugins": [
    [
      "react-intl",
      {
        "messagesDir": "./build/messages/",
        "extractSourceLocation": true
      }
    ]
  ]
}

Параметры конфигурации:

  • messagesDir — директория для выгрузки извлечённых сообщений;
  • extractSourceLocation — добавляет информацию о файле и позиции строки;
  • moduleSourceName — переопределение импорта react-intl при необходимости.

Структура извлечённых сообщений

После обработки формируется JSON-файл, содержащий нормализованные сообщения:

[
  {
    "id": "app.welcome",
    "defaultMessage": "Добро пожаловать в систему",
    "description": "Приветствие на главной странице",
    "file": "src/components/Home.js",
    "start": {
      "line": 10,
      "column": 5
    }
  }
]

Такая структура обеспечивает:

  • трассируемость источника сообщения;
  • упрощение ревью переводов;
  • интеграцию с системами локализации (Crowdin, Lokalise и др.).

CLI-инструменты FormatJS

FormatJS предоставляет CLI для автоматизации извлечения сообщений без прямой настройки Babel.

Основная команда:

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

Дополнительные параметры:

  • --id-interpolation-pattern — шаблон генерации ID;
  • --format — формат выходного файла;
  • --extract-source-location — включение метаданных о позиции;
  • --ignore — исключение файлов из обработки.

Пример использования шаблона идентификаторов:

formatjs extract "src/**/*.{js,ts,tsx}" \
  --id-interpolation-pattern '[sha512:contenthash:base64:6]'

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

Стратегии генерации идентификаторов

В FormatJS поддерживаются несколько стратегий формирования id:

  1. Ручной режим Идентификатор задаётся явно разработчиком.

  2. Хеширование содержимого Используется хеш от defaultMessage.

  3. Интерполяция шаблона Комбинация текста, файла и контекста.

Пример хеш-идентификатора:

{
  "id": "a1b2c3",
  "defaultMessage": "Сохранить изменения"
}

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

ICU Message Format в извлечении

FormatJS поддерживает ICU Message Syntax, что позволяет включать в сообщения:

  • множественные формы;
  • параметры;
  • условия;
  • даты и числа.

Пример:

<FormattedMessage
  id="cart.items"
  defaultMessage="{count, plural, one {# товар} few {# товара} many {# товаров} other {# товара}}"
  values={{ count: 5 }}
/>

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

Извлечение из динамических выражений

Сложность возникает при использовании динамических сообщений:

intl.formatMessage({
  id: `error.${code}`,
  defaultMessage: 'Неизвестная ошибка'
});

Статический анализ в таких случаях:

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

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

Обработка TypeScript-кода

FormatJS поддерживает TypeScript через Babel-пайплайн или ts-jest/tsc совместимые конфигурации.

Пример TS-кода:

const messages = defineMessages({
  title: {
    id: 'page.title',
    defaultMessage: 'Заголовок страницы'
  }
});

Извлечение происходит аналогично JavaScript, так как TypeScript преобразуется в AST до анализа.

Интеграция с CI/CD

Извлечение сообщений обычно включается в pipeline:

  • проверка наличия новых строк;
  • генерация JSON;
  • отправка в систему переводов;
  • сравнение с предыдущими версиями.

Пример шагов CI:

formatjs extract "src/**/*.{js,ts,tsx}" --out-file build/messages.json
node scripts/upload-translations.js

В некоторых системах применяется fail-fast стратегия: сборка прерывается при появлении новых неразмеченных сообщений.

Проблемы и ограничения извлечения

Процесс статического извлечения имеет ряд ограничений:

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

Особенно проблемными являются случаи:

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

Оптимизация структуры сообщений

Для повышения качества извлечения применяется ряд практик:

  • отказ от динамических id;
  • централизованное хранение сообщений;
  • использование defineMessages вместо inline-строк;
  • единый формат именования ключей;
  • добавление description для контекста.

Такая структура улучшает:

  • предсказуемость каталогов переводов;
  • качество машинного анализа;
  • скорость интеграции переводов.

Связь с системой интернационализации

Извлечение сообщений является частью более широкой цепочки:

  1. декларация сообщений в коде;
  2. статический анализ и извлечение;
  3. генерация файлов переводов;
  4. перевод в внешних системах;
  5. загрузка локализованных ресурсов;
  6. runtime-рендеринг через IntlProvider.

Каждый этап зависит от стабильности извлечённых данных, что делает этот процесс фундаментальным элементом архитектуры FormatJS.