Интерполяция — механизм подстановки динамических значений в локализованные сообщения. В экосистеме FormatJS она используется практически в каждом переводе: для вывода имён пользователей, чисел, дат, валют, ссылок, React-компонентов и HTML-разметки.
Простейший пример:
intl.formatMessage(
{
id: 'welcome',
defaultMessage: 'Добро пожаловать, {name}!',
},
{
name: 'Алексей',
}
)
Результат:
Добро пожаловать, Алексей!
Значение name подставляется в шаблон сообщения через
фигурные скобки.
FormatJS использует ICU MessageFormat. Все переменные внутри строки объявляются в фигурных скобках:
{variable}
При форматировании библиотека ищет соответствующее значение в объекте параметров.
intl.formatMessage(
{
id: 'profile',
defaultMessage: 'Пользователь: {username}',
},
{
username: 'root',
}
)
Если параметр отсутствует:
intl.formatMessage(
{
id: 'profile',
defaultMessage: 'Пользователь: {username}',
}
)
FormatJS выбросит ошибку:
The intl string context variable "username" was not provided
Это важная особенность безопасности: библиотека не подставляет
undefined молча.
Во многих системах локализации перевод превращается в источник XSS-уязвимостей.
Опасный пример:
const html = `<div>${userInput}</div>`
Если userInput содержит:
<script>alert('XSS')</script>
скрипт будет выполнен.
FormatJS спроектирован так, чтобы по умолчанию предотвращать подобные ошибки.
При использовании react-intl значения интерполируются
безопасно благодаря механизму React.
Пример:
<FormattedMessage
id="comment"
defaultMessage="Комментарий: {text}"
values={{
text: '<script>alert(1)</script>',
}}
/>
React автоматически экранирует строку.
На странице появится:
Комментарий: <script>alert(1)</script>
Скрипт не выполнится.
React никогда не вставляет строку как HTML напрямую.
Вместо этого:
<div>{value}</div>
преобразуется в безопасный текстовый узел DOM.
Именно поэтому комбинация:
react-intlсчитается безопасной для большинства сценариев.
dangerouslySetInnerHTMLСамая распространённая ошибка:
<div
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: 'message',
defaultMessage: 'Текст: {value}',
}),
}}
/>
Если в переводе или параметрах окажется HTML:
<img src=x oner ror=alert(1)>
произойдёт XSS.
Опасный шаблон:
defaultMessage: 'Нажмите <b>{text}</b>'
и затем:
text: '<script>alert(1)</script>'
Если строка позже будет вставлена как HTML — приложение уязвимо.
FormatJS поддерживает безопасную вставку React-компонентов вместо HTML-строк.
<FormattedMessage
id="cta"
defaultMessage="Нажмите <b>сюда</b>"
values={{
b: chunks => <b>{chunks}</b>,
}}
/>
Здесь:
<b> не является HTMLFormatJS разбирает сообщение:
Нажмите <b>сюда</b>
как структуру токенов.
Затем вызывает функцию:
b(chunks)
где chunks — содержимое тега.
Финальный результат:
<b>сюда</b>
без прямой HTML-вставки.
defaultMessage: 'Перейдите по <a href="{url}">ссылке</a>'
Это плохая практика.
Причины:
<FormattedMessage
id="docs"
defaultMessage="Откройте <link>документацию</link>"
values={{
link: chunks => (
<a href="/docs">
{chunks}
</a>
),
}}
/>
HTML больше не хранится внутри переводов.
FormatJS умеет безопасно внедрять JSX-компоненты.
Пример:
<FormattedMessage
id="user"
defaultMessage="Пользователь: {name}"
values={{
name: <strong>Admin</strong>,
}}
/>
React безопасно обработает компонент.
intl.formatMessage(
{
id: 'balance',
defaultMessage: 'Баланс: {amount}',
},
{
amount: 1500,
}
)
Результат:
Баланс: 1500
formatNumberЛучше использовать встроенные форматтеры.
<FormattedNumber
value={1500}
style="currency"
currency="USD"
/>
или:
intl.formatNumber(1500, {
style: 'currency',
currency: 'USD',
})
Преимущества:
defaultMessage: 'Дата: {date}'
и:
date: new Date().toString()
Результат зависит от окружения.
<FormattedDate
value={new Date()}
year="numeric"
month="long"
day="2-digit"
/>
или:
intl.formatDate(new Date(), {
year: 'numeric',
month: 'long',
day: '2-digit',
})
Любые данные пользователя считаются недоверенными:
Даже если данные пришли из собственной базы.
url: 'jav * ascript:alert(1)'
Если такой URL попадёт в:
<a href={url}>link</a>
произойдёт XSS.
function isSafeUrl(url) {
try {
const parsed = new URL(url)
return ['http:', 'https:'].includes(parsed.protocol)
} catch {
return false
}
}
Использование:
{
isSafeUrl(url) && (
<a href={url}>link</a>
)
}
Плохой пример:
{
"welcome": "<h1>Добро пожаловать</h1>"
}
Проблемы:
Добро пожаловать, <b>{name}</b>
values={{
b: chunks => <b>{chunks}</b>
}}
Так:
FormatJS поддерживает комбинацию ICU-конструкций.
Пример:
defaultMessage:
'{gender, select, male {Он} female {Она} other {Они}} отправил {count, plural, one {# сообщение} few {# сообщения} many {# сообщений} other {# сообщения}}'
Параметры:
{
gender: 'male',
count: 5,
}
ICU MessageFormat не выполняет код.
Конструкции:
pluralselectselectordinalявляются декларативными шаблонами.
Поэтому:
{count, plural, ...}
не может привести к code injection.
Если значения интерполяции формируются из внешнего объекта:
const values = JSON.parse(userInput)
может появиться:
{
"__proto__": {
"polluted": true
}
}
Лучше использовать:
const values = Object.create(null)
или:
const values = {
name: user.name,
count: user.count,
}
вместо слепого копирования.
TypeScript помогает предотвращать ошибки отсутствующих параметров.
type Messages = {
welcome: {
name: string
}
}
function t(
id: string,
values?: Record<string, string | number>
) {
return intl.formatMessage({ id }, values)
}
Можно дополнительно ограничить допустимые типы.
Полезная практика:
type Primitive =
| string
| number
| boolean
| null
| undefined
и затем:
Record<string, Primitive>
Это предотвращает случайную передачу опасных объектов.
Иногда HTML всё же требуется.
Например:
Безопасный подход:
npm install dompurify
Пример:
import DOMPurify fr om 'dompurify'
const safeHtml = DOMPurify.sanitize(html)
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
intl.formatMessage({
id: 'content',
})
),
}}
/>
Однако даже в этом случае предпочтительнее избегать HTML внутри переводов.
FormatJS не интерпретирует значения как HTML.
Пример:
{
name: '<b>Admin</b>'
}
станет обычным текстом.
Это фундаментальная гарантия безопасности библиотеки.
Наиболее безопасная архитектура:
| Зона | Ответственность |
|---|---|
| Перевод | Только текст |
| React | DOM |
| FormatJS | Интерполяция |
| Санитайзер | Очистка HTML |
| Backend | Валидация данных |
Content Security Policy дополнительно снижает риск XSS.
Пример заголовка:
Content-Security-Policy:
default-src 'self';
script-src 'self';
Даже если вредоносный HTML случайно попадёт в DOM, CSP может заблокировать выполнение скрипта.
Плохо:
{
"title": "<b>Hello</b>"
}
dangerouslySetInnerHTML без санитизацииПлохо:
dangerouslySetInnerHTML={{
__html: translated
}}
Плохо:
<a href={userUrl}>
Плохо:
values: {
user
}
Лучше:
values: {
name: user.name
}
Только ICU syntax.
<b>{chunks}</b>
вместо HTML-строк.
dangerouslySetInnerHTMLЕсли возможно — полностью избегать.
Особенно пользовательские.
Не вручную.
Через DOMPurify или аналогичные библиотеки.
Типизация значительно снижает количество ошибок интерполяции.
<FormattedMessage
id="profile"
defaultMessage="
Пользователь: <b>{name}</b>.
Открыть <link>профиль</link>
"
values={{
name: user.name,
b: chunks => (
<strong>{chunks}</strong>
),
link: chunks => (
<a href={`/users/${user.id}`}>
{chunks}
</a>
),
}}
/>
Здесь: