Rich text форматирование

Rich text форматирование в экосистеме FormatJS опирается на расширение ICU MessageFormat, позволяющее встраивать структурированные элементы разметки прямо в строки локализации. В отличие от классической интерполяции значений, здесь допускается передача не только примитивов, но и компонентов или функций, которые интерпретируются как семантические элементы текста.

Ключевая идея заключается в том, что строка перевода перестаёт быть просто текстом и становится шаблоном с вложенной структурой:

  • текстовые узлы
  • плейсхолдеры значений
  • rich text теги (логические контейнеры)

Пример базовой концепции:

"You agree to our <b>Terms of Service</b> and <a>Privacy Policy</a>."

Здесь <b> и <a> не являются HTML, а представляют абстрактные rich text элементы, которые связываются с реальными компонентами на стороне приложения.


Архитектура rich text в FormatJS

FormatJS реализует rich text через библиотеку intl-messageformat, которая парсит ICU-сообщения и формирует дерево узлов. Каждый тег в сообщении становится отдельным узлом AST (Abstract Syntax Tree).

Процесс обработки можно представить следующим образом:

  1. Строка сообщения парсится ICU-парсером
  2. Формируется дерево сообщений
  3. При форматировании вызываются функции для каждого rich text тега
  4. Результат собирается в финальный вывод (React-элементы, строки или DOM-структуры)

Важный момент: rich text не является HTML. Это абстрактная модель, независимая от платформы.


Синтаксис rich text в ICU MessageFormat

Rich text в FormatJS реализуется через XML-подобные теги внутри строки сообщения:

{
  "terms": "Accept <b>terms</b> and <link>privacy policy</link>."
}

Теги могут быть произвольными, но должны соответствовать переданным форматерам.

Поддерживаемые элементы:

  • <b> — жирное выделение
  • <i> — курсив
  • <link> — ссылка
  • <span> — нейтральный контейнер
  • пользовательские теги

Каждый тег требует соответствующего обработчика при форматировании.


Использование rich text в React через react-intl

В react-intl rich text реализуется через передачу React-компонентов в values объекта FormattedMessage.

Пример:

import { FormattedMessage } from "react-intl";

<FormattedMessage
  id="policy.text"
  defaultMessage="Accept <b>terms</b> and <link>privacy policy</link>."
  values={{
    b: (chunks) => <strong>{chunks}</strong>,
    link: (chunks) => <a href="/privacy">{chunks}</a>
  }}
/>

Механика работы

  • chunks — это вложенное содержимое между тегами
  • каждая функция возвращает React-элемент
  • структура сохраняется независимо от языка

Разделение текста и логики

Rich text форматирование в FormatJS принципиально отделяет текст локализации от логики UI. Переводчик работает с абстрактными тегами, не зная их визуального представления.

Пример строки перевода:

"readMore": "Read <link>more details</link> about this topic."

А логика отображения задаётся в коде:

link: (chunks) => <Link to="/details">{chunks}</Link>

Такой подход позволяет:

  • менять UI без изменения переводов
  • переиспользовать переводы в разных контекстах
  • избегать HTML в переводах

Вложенные rich text элементы

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

Пример:

"nested": "<b>Important: <i>read carefully</i></b>"

Обработка:

values={{
  b: (chunks) => <strong>{chunks}</strong>,
  i: (chunks) => <em>{chunks}</em>
}}

В результате получается корректная вложенная структура React-элементов.


Ограничения вложенности

Несмотря на гибкость, существует ряд ограничений:

  • нельзя использовать произвольный HTML
  • теги должны быть согласованы между переводами и кодом
  • вложенность должна быть предсказуемой
  • нельзя динамически генерировать имена тегов внутри строки

Нарушение этих правил приводит к разрыву структуры AST и ошибкам форматирования.


Rich text без React: форматирование строк

FormatJS может возвращать не только React-элементы, но и строки или структуры данных.

Пример использования IntlMessageFormat напрямую:

import IntlMessageFormat from "intl-messageformat";

const msg = new IntlMessageFormat(
  "Click <b>here</b> to continue",
  "en"
);

msg.format({
  b: (chunks) => `**${chunks}**`
});

В этом случае rich text превращается в текстовую разметку Markdown-подобного вида.


Сопоставление тегов и форматтеров

Каждый rich text тег соответствует функции в объекте values. Это ключевой механизм связывания структуры сообщения с логикой приложения.

Таблица соответствий:

Тег в сообщении Обработчик Результат
<b> b: fn <strong>
<link> link: fn <a> или <Link>
<i> i: fn <em>
<user> user: fn кастомный компонент

Безопасность и контроль вывода

Rich text форматирование требует строгого контроля, так как строки переводов могут приходить из внешних источников.

Основные принципы безопасности:

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

FormatJS не рендерит HTML напрямую, что снижает риск XSS-уязвимостей при корректной реализации.


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

Rich text особенно полезен для интеграции UI-компонентов в локализацию.

Пример:

values={{
  button: (chunks) => <Button>{chunks}</Button>,
  highlight: (chunks) => <span className="highlight">{chunks}</span>
}}

Сообщение:

"cta": "Click <button>this button</button> to continue"

Это позволяет переводчикам управлять структурой текста без вмешательства в код.


Динамические значения внутри rich text

Rich text можно комбинировать с обычной интерполяцией:

"welcome": "Hello <b>{name}</b>, welcome back!"
values={{
  name: userName,
  b: (chunks) => <strong>{chunks}</strong>
}}

Таким образом один шаблон может содержать одновременно:

  • текстовые переменные
  • структурные элементы
  • форматируемые блоки

Работа с порядком элементов

ICU MessageFormat не требует фиксированного порядка тегов. Переводчик может изменять структуру предложения:

"msg": "<link>Click here</link> to learn more"

или

"msg": "To learn more, <link>click here</link>"

Код остаётся неизменным, поскольку логика привязана к тегу, а не позиции.


Производительность rich text форматирования

Rich text увеличивает сложность обработки сообщений, так как включает:

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

Оптимизации включают:

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

Типичные ошибки при использовании

Часто встречающиеся проблемы:

  • несовпадение тегов в переводе и коде
  • отсутствие обработчика для тега
  • попытка вставить HTML вместо ICU-тегов
  • чрезмерная вложенность элементов
  • использование одного тега для разных смыслов

Каждая из этих ошибок приводит либо к потере форматирования, либо к падению рендеринга.


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

Для сложных интерфейсов рекомендуется разбивать сообщения:

  • отдельное сообщение для каждого UI-блока
  • минимизация количества rich text элементов
  • избегание вложенных логических конструкций в одной строке

Пример разделения:

"title": "Welcome"
"body": "Click <link>here</link> to continue"
"footer": "Need help? <support>Contact support</support>"

Интеграция с системой интернационализации

Rich text является частью более широкой архитектуры i18n в FormatJS:

  • react-intl — UI слой
  • intl-messageformat — парсинг и форматирование
  • @formatjs/icu-messageformat-parser — разбор ICU
  • @formatjs/intl — полифиллы и расширения

Rich text выступает связующим звеном между переводами и компонентной моделью приложения.