Теги и компоненты в сообщениях

ICU MessageFormat, лежащий в основе FormatJS, изначально создавался как система интернационализации для текстовых сообщений с поддержкой параметров, множественных форм и контекстных вариантов. Однако в современных UI-приложениях текст редко бывает «плоским»: внутри сообщений часто требуется выделение фрагментов, вставка ссылок, форматирование отдельных слов, а также встраивание интерактивных компонентов. Для этого в FormatJS используется механизм rich text formatting — работа с тегами, которые сопоставляются с функциями или компонентами.

Rich text как расширение ICU-синтаксиса

ICU MessageFormat поддерживает параметрические выражения:

"Hello, {name}"

Rich text расширяет эту модель, добавляя псевдотеги:

"Click <link>here</link> to continue"

В отличие от HTML, эти теги не интерпретируются как DOM. Они являются частью AST сообщения и заменяются во время форматирования функциями, заданными разработчиком.

Ключевая особенность заключается в том, что:

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

Принцип сопоставления тегов и значений

В FormatJS каждый тег сообщения сопоставляется с функцией через объект values.

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

import { useIntl } from 'react-intl';

function Example() {
  const intl = useIntl();

  const message = intl.formatMessage(
    {
      id: 'welcome.message',
      defaultMessage: 'Hello <b>{name}</b>!'
    },
    {
      name: 'Alex',
      b: (chunks) => <strong>{chunks}</strong>
    }
  );

  return <div>{message}</div>;
}

Здесь происходит следующее:

  • <b> в сообщении не является HTML;
  • b в values — функция;
  • chunks — содержимое между тегами;
  • возвращаемый React-элемент заменяет тег в итоговом дереве.

Модель работы с вложенными тегами

Rich text поддерживает вложенность, аналогичную HTML, но управляемую через функции:

"Read <a>this <b>important</b> article</a>"

Обработка:

intl.formatMessage(
  {
    id: 'nested.example',
    defaultMessage: 'Read <a>this <b>important</b> article</a>'
  },
  {
    a: (chunks) => <a href="/article">{chunks}</a>,
    b: (chunks) => <strong>{chunks}</strong>
  }
);

Каждый уровень тегов получает свой набор chunks, уже обработанных внутренне.

Отличие от HTML и безопасность

Rich text FormatJS принципиально не является HTML-парсером. Это важно для архитектуры безопасности:

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

Даже если переводчик добавит <script> или неизвестные теги, они не будут интерпретированы, так как не зарегистрированы в values.

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

В React-экосистеме теги часто заменяются полноценными компонентами.

const messages = {
  terms: 'By continuing you agree to our <link>Terms of Service</link>.'
};

function Terms() {
  const intl = useIntl();

  return intl.formatMessage(messages.terms, {
    link: (chunks) => (
      <a href="/terms" target="_blank" rel="noreferrer">
        {chunks}
      </a>
    )
  });
}

Здесь компонент <a> создаётся динамически, а текст внутри остаётся частью локализованного сообщения.

Согласование типов и структуры сообщений

При использовании rich text важно учитывать типизацию сообщений. В TypeScript экосистеме FormatJS часто применяется генерация типов через CLI (@formatjs/cli), где структура тегов фиксируется заранее.

Типичная проблема возникает при несоответствии ключей:

  • сообщение содержит <highlight>;
  • в values отсутствует highlight;
  • результат — тег выводится как обычный текст.

Это поведение полезно для устойчивости: сообщение не ломается, а деградирует до plain text.

Передача параметров в компоненты тегов

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

const userId = 42;

intl.formatMessage(
  {
    id: 'profile.link',
    defaultMessage: 'Open <profile>profile</profile>'
  },
  {
    profile: (chunks) => (
      <a href={`/user/${userId}`}>
        {chunks}
      </a>
    )
  }
);

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

Сочетание с ICU-параметрами

Rich text может комбинироваться с другими возможностями ICU:

"You have {count} messages. <link>View all</link>"
intl.formatMessage(
  {
    id: 'inbox.summary',
    defaultMessage: 'You have {count} messages. <link>View all</link>'
  },
  {
    count: 5,
    link: (chunks) => <a href="/inbox">{chunks}</a>
  }
);

Здесь происходит совместная обработка:

  • count обрабатывается ICU-движком;
  • link обрабатывается как rich text.

Поведение при отсутствии тегов

Если тег не определён в values, он остаётся в тексте без обработки:

"Hello <b>world</b>"

при отсутствии b:

Hello <b>world</b>

Это важно для стабильности перевода: система не выбрасывает исключения.

Оптимизация и повторное использование компонентов

Rich text функции могут быть переиспользованы:

const bold = (chunks) => <strong>{chunks}</strong>;

const link = (href) => (chunks) =>
  <a href={href}>{chunks}</a>;

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

intl.formatMessage(msg, {
  b: bold,
  a: link('/home')
});

Такой подход снижает дублирование и стандартизирует поведение UI-элементов внутри переводов.

Ограничения модели тегов

Несмотря на гибкость, модель имеет ограничения:

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

Поэтому rich text используется только для семантической разметки, а не для построения интерфейса.

Разбор сообщения на уровне AST

FormatJS преобразует строку сообщения в абстрактное синтаксическое дерево:

  • текстовые узлы;
  • аргументы {value};
  • элементы rich text <tag>...</tag>.

На этапе форматирования AST обходится, и каждый тег заменяется соответствующей функцией. Это объясняет высокую предсказуемость поведения: результат не зависит от HTML-парсинга или DOM.

Стабильность при переводах

Переводчики работают с исходной строкой, где теги остаются неизменными:

"Click <link>here</link>"

Перевод:

"Kliknij <link>tutaj</link>"

Важно, что:

  • структура тегов сохраняется;
  • изменяется только текст внутри;
  • логика приложения остаётся неизменной.

Практика использования сложных компонентов

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

  • ссылки;
  • кнопки;
  • выделения;
  • встроенные бейджи состояния.

Пример:

"<warning>Warning:</warning> Your session will expire in {time} minutes."
warning: (chunks) => <span className="warning">{chunks}</span>

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

Расширение модели через кастомные обёртки

В некоторых архитектурах создаётся слой абстракции над formatMessage, где теги стандартизируются:

  • <b> — жирный текст;
  • <i> — курсив;
  • <link> — навигация;
  • <code> — моноширинный блок.

Это позволяет поддерживать единый контракт между переводами и компонентами интерфейса, независимо от конкретной реализации React или React Native.