ICU MessageFormat, лежащий в основе FormatJS, изначально создавался как система интернационализации для текстовых сообщений с поддержкой параметров, множественных форм и контекстных вариантов. Однако в современных UI-приложениях текст редко бывает «плоским»: внутри сообщений часто требуется выделение фрагментов, вставка ссылок, форматирование отдельных слов, а также встраивание интерактивных компонентов. Для этого в FormatJS используется механизм rich text formatting — работа с тегами, которые сопоставляются с функциями или компонентами.
ICU MessageFormat поддерживает параметрические выражения:
"Hello, {name}"
Rich text расширяет эту модель, добавляя псевдотеги:
"Click <link>here</link> to continue"
В отличие от HTML, эти теги не интерпретируются как DOM. Они являются частью AST сообщения и заменяются во время форматирования функциями, заданными разработчиком.
Ключевая особенность заключается в том, что:
В 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 — содержимое между тегами;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, уже
обработанных внутренне.
Rich text FormatJS принципиально не является 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>
)
}
);
Таким образом, тег выступает как точка внедрения логики, а не просто стилизации.
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-элементов внутри переводов.
Несмотря на гибкость, модель имеет ограничения:
Поэтому rich text используется только для семантической разметки, а не для построения интерфейса.
FormatJS преобразует строку сообщения в абстрактное синтаксическое дерево:
{value};<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.