Гидратация интернационализации в приложениях на базе FormatJS тесно связана с синхронизацией локали между серверным и клиентским рендерингом. При серверном рендеринге (SSR) формируется HTML, уже содержащий локализованный текст, числа, даты и валюты. На клиенте происходит повторное «оживление» этого HTML (hydration), и любое несоответствие локали или набора сообщений приводит к расхождению между серверным и клиентским деревьями.
В SSR-приложениях локаль используется дважды: сначала на сервере для
генерации HTML, затем на клиенте для восстановления состояния
интерфейса. Библиотека React Intl, входящая в экосистему FormatJS,
строит работу через IntlProvider, который передаёт локаль и
сообщения во все компоненты.
Ключевая проблема возникает, когда:
locale = "ru"locale = "en"В таком случае React фиксирует mismatch при гидратации.
Стабильность гидратации зависит от того, насколько строго согласованы входные параметры интернационализации.
Основные источники локали:
Accept-Language/ru/home)На сервере локаль должна быть определена до рендера React-дерева,
чтобы IntlProvider получил идентичные значения.
Пример серверной подготовки:
const locale = negotiateLocale(request.headers["accept-language"]);
const messages = loadMessages(locale);
const html = renderToString(
<IntlProvider locale={locale} messages={messages}>
<App />
</IntlProvider>
);
На клиенте критически важно использовать тот же locale и
те же messages:
const locale = window.__INITIAL_LOCALE__;
const messages = window.__INITIAL_MESSAGES__;
hydrateRoot(
document.getElementById("root"),
<IntlProvider locale={locale} messages={messages}>
<App />
</IntlProvider>
);
Любое отличие приведёт к повторной генерации текста и потенциальному разрушению гидратации.
FormatJS использует ключевые сообщения:
{
"welcome.message": "Добро пожаловать",
"cart.items": "Товаров: {count}"
}
Если на сервере набор сообщений включает ключ
cart.items, а на клиенте он отсутствует или имеет другую
структуру, возникают:
Особенно критично динамическое подгружение переводов после первичного рендера.
Стабильная гидратация требует, чтобы сообщения были доступны ДО рендера клиента.
Используются следующие подходы:
1. Инлайн-сериализация сообщений
Сервер передаёт JSON прямо в HTML:
<script>
window.__INITIAL_MESSAGES__ = {
"welcome.message": "Добро пожаловать"
};
</script>
2. Предзагрузка через bundler
Переводы импортируются статически:
import ru from "./locales/ru.json";
import en from "./locales/en.json";
3. Lazy-loading с блокировкой рендера
Если сообщения грузятся асинхронно, рендер откладывается:
const messages = await loadMessages(locale);
hydrateRoot(
root,
<IntlProvider locale={locale} messages={messages}>
<App />
</IntlProvider>
);
FormatJS опирается на Intl API браузера и Node.js.
Несмотря на стандарт, поведение может различаться:
Пример потенциального расхождения:
formatDate(new Date(), {
year: "numeric",
month: "long"
});
На сервере может использоваться UTC, на клиенте — локальная временная зона пользователя.
Для стабилизации применяется явная фиксация timezone:
formatDate(date, {
timeZone: "Europe/Moscow"
});
Основные источники ошибок:
locale между SSR и CSRmessagesMath.random() внутри форматируемых
строкДаже если визуально текст совпадает, React может фиксировать различия в текстовых нодах.
Pluralization в FormatJS зависит от локали:
intl.formatMessage(
{ id: "cart.items" },
{ count: 3 }
);
При смене локали меняется не только текст, но и логика выбора формы:
Если сервер и клиент используют разные локали, DOM-структура может измениться.
FormatJS поддерживает вложенные React-элементы:
intl.formatMessage(
{
id: "terms",
defaultMessage: "Принять <b>условия</b>"
},
{
b: chunks => <strong>{chunks}</strong>
}
);
При гидратации важно, чтобы:
Любая нестабильность приводит к различию виртуального DOM.
Одним из устойчивых подходов является привязка локали к маршруту:
/ru/products
/en/products
Это позволяет серверу всегда однозначно определить контекст:
const locale = req.path.split("/")[1];
Такой подход уменьшает риск рассинхронизации между слоями приложения.
CDN и серверное кэширование могут сохранять HTML, сгенерированный для одной локали, и отдавать его пользователю с другой локалью.
Это приводит к:
Для предотвращения используется разделение кэша по ключу:
Cache-Key: /page + locale
IntlProvider должен находиться максимально высоко в
дереве компонентов:
<IntlProvider locale={locale} messages={messages}>
<App />
</IntlProvider>
Если его размещать глубже, часть компонентов может получить один набор локали, а часть — другой, что приводит к несогласованной гидратации.
FormatJS использует fallback-стратегии:
defaultMessagedefaultMessage → используется
idПри SSR и CSR разные fallback-ветки могут сформировать разный текст, что ломает гидратацию.
После первичной гидратации возможна смена языка:
setLocale("en");
Это вызывает повторный рендер всего дерева IntlProvider.
Если не управлять этим процессом, возможны:
Используются два основных паттерна:
1. Server-driven locale
Сервер полностью контролирует локаль и передаёт её клиенту без изменений.
2. Client-hydrated but server-locked locale
Сервер фиксирует локаль в HTML, клиент не имеет права её менять до следующей навигации.
Оба подхода направлены на устранение несоответствий между SSR и CSR.
Диагностика включает:
locale на сервере и клиентеformatMessage на обеих
сторонахКритически важно проверять не только текст, но и структуру React-элементов, которые генерируются через форматирование сообщений.