ICU MessageFormat, используемый в экосистеме FormatJS, изначально не оперирует «чистым HTML» как частью строки сообщения. Вместо этого применяется модель параметризованных сообщений, где разметка либо эмулируется через плейсхолдеры, либо реализуется через механизм rich text formatting. Это принципиально меняет подход к локализации: сообщения остаются текстовыми, а структурность добавляется на этапе рендеринга.
В классическом представлении интернационализации возникает соблазн включать HTML-теги прямо в переводимые строки:
"message": "Нажмите <b>подтвердить</b>, чтобы продолжить"
Однако ICU MessageFormat не интерпретирует такие конструкции как
HTML. Это просто текст с символами < и
>. Попытка обрабатывать такие строки как HTML приводит к
нескольким проблемам:
Вместо этого FormatJS предлагает два корректных механизма: rich text formatting и компонентную подстановку.
Rich text formatting позволяет использовать в сообщениях псевдо-теги, которые затем преобразуются в функции при форматировании строки.
Пример сообщения:
{
"welcome": "Нажмите <bold>подтвердить</bold>, чтобы продолжить"
}
При использовании formatMessage или
<FormattedMessage /> эти теги не становятся HTML.
Вместо этого они сопоставляются с функциями:
import { useIntl } from "react-intl";
const Component = () => {
const intl = useIntl();
const text = intl.formatMessage(
{
id: "welcome",
defaultMessage: "Нажмите <bold>подтвердить</bold>, чтобы продолжить"
},
{
bold: (chunks) => <strong>{chunks}</strong>
}
);
return <div>{text}</div>;
};
Ключевой момент заключается в том, что <bold> —
это не HTML, а синтаксический маркер ICU, который заменяется
функцией.
Поддерживаются вложенные конструкции:
{
"info": "См. <link>документацию <bold>FormatJS</bold></link>"
}
const message = intl.formatMessage(
{ id: "info" },
{
link: (chunks) => <a href="/docs">{chunks}</a>,
bold: (chunks) => <strong>{chunks}</strong>
}
);
Таким образом формируется дерево React-элементов без использования
опасного innerHTML.
Подход FormatJS принципиально отличается от вставки HTML-строк:
| Подход | Модель | Риски | Гибкость |
|---|---|---|---|
| HTML в строках | строковая интерполяция | высокий XSS-риск | низкая |
| ICU rich text | функция-рендеринг | отсутствует при корректном использовании | высокая |
| React компоненты | композиция UI | отсутствует | максимальная |
HTML-подобные конструкции в сообщениях не парсятся как DOM. Это защищает систему от случайного или намеренного внедрения скриптов.
Основная проблема работы с HTML в локализованных сообщениях — внедрение небезопасного содержимого.
Типичный опасный паттерн:
<div dangerouslySetInnerHTML={{ __html: message }} />
Если message содержит пользовательский ввод или
непроверенный перевод, возникает XSS-уязвимость.
FormatJS избегает этой проблемы через модель:
Дополнительно важно:
formatMessage в HTMLDOMPurify как основной механизм
исправления архитектуры (он лишь компенсация)В React-интеграции FormatJS ключевая практика — замена HTML-тегов компонентами.
Пример:
import { FormattedMessage } from "react-intl";
const App = () => (
<FormattedMessage
id="terms"
defaultMessage="Я согласен с <link>условиями использования</link>"
values={{
link: (chunks) => <a href="/terms">{chunks}</a>
}}
/>
);
Внутри происходит:
Этот механизм масштабируется на сложные случаи:
Низкоуровневый API formatToParts позволяет получить
структуру сообщения без финального HTML или JSX:
const parts = intl.formatMessage(
{ id: "welcome" },
{
bold: (chunks) => `<b>${chunks}</b>`
}
);
или через formatToParts:
const parts = intl.formatMessageToParts(
{ id: "welcome" },
{ bold: (chunks) => `<b>${chunks}</b>` }
);
Результатом является массив сегментов:
Это используется при:
В серверных сценариях FormatJS часто применяется для генерации HTML-страниц или email.
Важно различать два подхода:
const html = renderToString(
<IntlProvider locale="ru" messages={messages}>
<App />
</IntlProvider>
);
Здесь rich text остаётся React-деревом до финального рендера.
Если требуется HTML-строка, форматирование должно быть явным:
const message = intl.formatMessage(
{ id: "email" },
{
bold: (chunks) => `<strong>${chunks}</strong>`
}
);
При этом критично:
Некорректная архитектура часто возникает при попытке объединить HTML и ICU:
{
"bad": "Нажмите <button>OK</button> и перейдите на страницу"
}
Проблемы:
Корректная модель:
{
"good": "Нажмите <btn>OK</btn> и перейдите на страницу"
}
btn: (chunks) => <button>{chunks}</button>
Rich text форматирование позволяет строить сложные композиции:
{
"complex": "Согласие с <link><bold>условиями</bold> сервиса</link> обязательно"
}
{
link: (chunks) => <a href="/terms">{chunks}</a>,
bold: (chunks) => <strong>{chunks}</strong>
}
Порядок вложенности управляется ICU, а не DOM, что делает поведение предсказуемым независимо от языка перевода.
На уровне архитектуры выделяются устойчивые подходы:
1. Полный отказ от HTML в переводах
Переводы содержат только ICU-разметку без реальных тегов.
2. Компонентная модель UI
Любая визуальная структура определяется через
values-функции.
3. Централизация ссылок
Все внешние ссылки задаются через именованные плейсхолдеры:
values: {
helpLink: (chunks) => <a href={HELP_URL}>{chunks}</a>
}
4. Разделение данных и представления
Сообщение не знает о DOM, CSS или HTML — только о структуре текста.