Hydration локализованного контента

Hydration локализованного контента в контексте SSR возникает на стыке двух факторов: недетерминированного форматирования и различий окружения выполнения между сервером и клиентом. Основной источник проблемы — Intl API, который опирается на локаль, часовой пояс и параметры среды выполнения, часто отличающиеся между Node.js и браузером.

В процессе серверного рендеринга формируется HTML с уже вычисленными строками дат, чисел и других локализованных значений. На клиенте React или другая библиотека повторно вычисляет дерево, и если результаты Intl отличаются, возникает mismatch.

Ключевые источники различий:

  • Локаль среды выполнения

    • сервер может использовать en-US по умолчанию
    • браузер использует локаль пользователя, например ru-RU
  • Часовой пояс

    • Node.js часто работает в UTC
    • браузер использует системный часовой пояс пользователя
  • Версия ICU данных

    • Node.js может быть собран с ограниченным ICU
    • браузер использует собственные актуальные данные
  • Нестабильные параметры форматирования

    • отсутствие явного указания locale и timeZone

Intl API как источник недетерминированности

Intl API включает несколько ключевых форматтеров, поведение которых зависит от окружения:

  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.RelativeTimeFormat
  • Intl.PluralRules
  • Intl.DisplayNames

Даже при одинаковых входных данных результат может отличаться:

new Intl.DateTimeFormat().format(new Date())
// "5/26/2026" на сервере (en-US по умолчанию)
// "26.05.2026" в браузере (ru-RU)

Гидратация и детерминированный вывод

Основная проблема гидратации заключается в том, что HTML, сгенерированный сервером, должен полностью совпадать с результатом первого рендера на клиенте. Любое расхождение в локализованных строках приводит к пересозданию узлов или предупреждениям о несоответствии.

Intl-форматирование становится критическим источником нестабильности, если оно выполняется в обеих средах независимо.

Стратегия фиксации локали и окружения

Одним из базовых подходов является жесткая фиксация параметров Intl:

const formatter = new Intl.DateTimeFormat("ru-RU", {
  timeZone: "UTC",
  dateStyle: "short"
});

formatter.format(new Date("2026-05-26T10:00:00Z"));

Ключевой принцип заключается в устранении зависимости от среды выполнения:

  • явное указание locale
  • явное указание timeZone
  • минимизация опций, зависящих от системы

Серверная пред-форматизация значений

Наиболее устойчивый подход в SSR — перенос форматирования на сервер и передача уже готовых строк в клиентский payload.

const date = new Date("2026-05-26T10:00:00Z");

const formatted = new Intl.DateTimeFormat("ru-RU", {
  timeZone: "UTC",
  year: "numeric",
  month: "2-digit",
  day: "2-digit"
}).format(date);

// передается в JSON как строка

На клиенте используется уже готовое значение без повторного вызова Intl.

Использование formatToParts для стабильного рендера

Метод formatToParts позволяет получить структурированное представление результата форматирования, что уменьшает риск различий в строковой сборке:

const parts = new Intl.NumberFormat("ru-RU", {
  style: "currency",
  currency: "RUB"
}).formatToParts(1234.56);

Результат представляет массив частей:

  • literal
  • integer
  • decimal
  • fraction
  • currency

При гидратации это позволяет собирать UI из фиксированных токенов, снижая вероятность расхождений из-за локализационных нюансов.

Стабильность числового форматирования

Intl.NumberFormat особенно чувствителен к локали и настройкам округления.

new Intl.NumberFormat("ru-RU", {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
}).format(1234.5);

Без фиксации параметров возможны различия:

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

Проблемы с относительным временем

Intl.RelativeTimeFormat добавляет дополнительный уровень нестабильности, поскольку результат зависит от текущего времени:

new Intl.RelativeTimeFormat("ru-RU").format(-1, "day");

При SSR и клиентском рендере даже небольшая задержка приводит к различию результата (например, “вчера” против “23 часа назад”).

Для стабилизации используется:

  • вычисление разницы на сервере
  • передача уже рассчитанного значения (delta)
  • фиксация “now” как параметра

Контроль локали через контекст приложения

В приложениях с мультиязычностью часто используется единый источник локали, который передается как часть состояния SSR:

  • locale
  • timeZone
  • numberingSystem
  • calendar

Важно, что даже при наличии глобального контекста Intl продолжает использовать системные значения, если они явно не переопределены.

Проблема смешивания серверного и клиентского Intl

Гидратационные ошибки часто возникают при следующем паттерне:

  • сервер рендерит Intl.DateTimeFormat("ru-RU")
  • клиент вызывает Intl.DateTimeFormat() без аргументов

Даже при одинаковых входных данных результат будет различен.

Стабилизация через сериализацию форматирования

Одним из надежных подходов является сериализация не только данных, но и уже отформатированных представлений:

{
  "dateRaw": "2026-05-26T10:00:00Z",
  "dateFormatted": "26.05.2026"
}

Такой подход устраняет необходимость повторного Intl-вызова на клиенте.

Влияние ICU и сборок Node.js

Node.js может быть собран с различными режимами ICU:

  • full ICU
  • small ICU
  • no ICU (fallback)

Это напрямую влияет на:

  • поддержку локалей
  • доступные календарные системы
  • корректность форматирования валют и дат

Даже при идентичном коде SSR может отличаться между окружениями.

Детерминированные паттерны использования Intl

Для предотвращения гидратационных расхождений используются следующие паттерны:

  • фиксированные locale
  • фиксированный timeZone
  • исключение неявных параметров
  • перенос форматирования на уровень данных
  • использование formatToParts для UI-композиции

Форматирование валют в SSR

Intl.NumberFormat с валютой требует особой стабильности:

new Intl.NumberFormat("ru-RU", {
  style: "currency",
  currency: "USD",
  currencyDisplay: "symbol"
}).format(100);

Различия могут возникать из-за:

  • локальных символов валют
  • пробелов (non-breaking space)
  • правил округления

Роль стабильного времени (time source)

Гидратация чувствительна к моменту вычисления Date.now(). При SSR обычно фиксируется базовое время:

  • передается timestamp от сервера
  • используется как источник “now”
  • исключается вызов new Date() без синхронизации

Архитектурный паттерн: изоляция Intl слоя

В устойчивых архитектурах Intl API не используется напрямую в UI-слое. Вместо этого вводится слой форматирования:

  • formatDate(timestamp, locale, timeZone)
  • formatCurrency(value, locale, currency)
  • formatRelativeTime(delta, locale)

Это позволяет:

  • централизовать правила
  • обеспечить идентичность SSR и CSR
  • устранить дублирование логики

Гидратация как проверка детерминизма

Любое использование Intl в контексте SSR фактически становится тестом на детерминированность системы. Несовпадение строк означает наличие скрытых зависимостей от среды выполнения, локали или времени, которые не были зафиксированы явно.