SSR с Globalize

Globalize предназначена для построения многоязычных интерфейсов на основе данных CLDR и предоставляет единый API для форматирования чисел, дат, валют, сообщений и правил плюрализации. В SSR-архитектуре её поведение отличается от клиентского окружения прежде всего управлением локалями, загрузкой CLDR-данных и отсутствием глобального состояния браузера.

Server-Side Rendering в контексте JavaScript чаще всего реализуется через Node.js. Каждый запрос обрабатывается изолированно, что накладывает требования на международные библиотеки:

  • отсутствие глобального состояния локали между запросами
  • явная инициализация Globalize на каждый запрос или на пул локалей
  • контроль загрузки CLDR-данных в память процесса
  • предсказуемость форматирования при конкурентной обработке запросов

Globalize не хранит локаль глобально в классическом смысле, однако его конфигурация и загруженные CLDR-данные часто инициализируются на уровне приложения, что требует аккуратной архитектуры в SSR.

Подготовка CLDR-данных для серверной среды

Globalize полностью зависит от данных Unicode CLDR. В SSR важно заранее определить стратегию загрузки:

  1. Полная предзагрузка всех локалей при старте сервера
  2. Ленивое подгружение локалей по мере поступления запросов
  3. Гибридный подход с кэшем наиболее часто используемых локалей

Пример загрузки CLDR в Node.js:

import Globalize from "globalize";
import likelySubtags from "cldr-data/supplemental/likelySubtags.json";
import numbers from "cldr-data/main/ru/numbers.json";
import caGregorian from "cldr-data/main/ru/ca-gregorian.json";
import timeZoneNames from "cldr-data/main/ru/timeZoneNames.json";
import plurals from "cldr-data/supplemental/plurals.json";

// загрузка supplemental данных
Globalize.load(likelySubtags);
Globalize.load(plurals);

// загрузка локальных данных
Globalize.load(numbers);
Globalize.load(caGregorian);
Globalize.load(timeZoneNames);

В SSR важно выполнять эту загрузку один раз на процесс, иначе нагрузка на память и время старта становится критической.

Инициализация Globalize в SSR без утечек состояния

Ключевая ошибка при использовании Globalize в серверной среде — создание глобальной локали:

// небезопасный подход
Globalize.locale("ru");

В многопоточном или многозапросном окружении это приводит к гонкам состояния.

Корректная модель — создание экземпляра через Globalize(locale):

import Globalize from "globalize";

function getFormatter(locale) {
  return new Globalize(locale);
}

Каждый запрос получает собственный экземпляр форматтера, что исключает перекрёстное влияние локалей.

Форматирование данных в SSR

Числа

const globalize = new Globalize("ru");

const numberFormatter = globalize.numberFormatter();
numberFormatter(1234567.89);

В SSR это часто используется для предрендеринга UI:

  • цены товаров
  • статистика
  • агрегированные данные

Даты

const dateFormatter = globalize.dateFormatter({
  datetime: "medium"
});

dateFormatter(new Date());

Особенность SSR — дата должна быть вычислена с учётом локали пользователя, а не сервера.

Передача локали из запроса

В архитектуре SSR локаль обычно определяется на основе:

  • заголовка Accept-Language
  • параметра маршрута (/ru/products)
  • пользовательских настроек в сессии

Пример:

function detectLocale(req) {
  return req.headers["accept-language"]?.split(",")[0] || "en";
}

Далее локаль передаётся в слой рендеринга:

function renderPage(req) {
  const locale = detectLocale(req);
  const globalize = new Globalize(locale);

  const formattedPrice = globalize.numberFormatter()(1999.99);

  return `<div>${formattedPrice}</div>`;
}

Кэширование форматтеров

Создание форматтеров дорогостоящая операция, особенно при большом количестве запросов. В SSR применяется кэширование по локали:

const formatterCache = new Map();

function getNumberFormatter(locale) {
  if (!formatterCache.has(locale)) {
    const g = new Globalize(locale);
    formatterCache.set(locale, g.numberFormatter());
  }
  return formatterCache.get(locale);
}

Однако кэшировать следует аккуратно:

  • форматтеры зависят от параметров (currency, style)
  • кэш должен учитывать конфигурацию, а не только локаль

Рендеринг сообщений и plural rules

Globalize поддерживает ICU-подобные сообщения:

const messageFormatter = globalize.messageFormatter(
  "You have {count, plural, one {# item} other {# items}}"
);

messageFormatter({ count: 5 });

В SSR это позволяет заранее формировать текст без дополнительной клиентской логики.

Интеграция с React SSR

При использовании React серверный рендеринг часто строится через Node.js поток рендера. Globalize внедряется через контекст:

function createRenderContext(locale) {
  const globalize = new Globalize(locale);

  return {
    formatNumber: globalize.numberFormatter(),
    formatDate: globalize.dateFormatter()
  };
}

Далее контекст передаётся в компоненты:

function Price({ value, ctx }) {
  return `<span>${ctx.formatNumber(value)}</span>`;
}

Гидратация и согласованность данных

Одна из критичных проблем SSR — расхождение между серверным и клиентским форматированием.

Если на сервере используется Globalize, на клиенте должна быть:

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

Иначе при гидратации возможны:

  • предупреждения React о несоответствии DOM
  • визуальные расхождения
  • ошибки вычисления дат

Решение — сериализация локали и конфигурации:

window.__LOCALE__ = "ru";
window.__CLDR_LOADED__ = true;

Ленивое подключение CLDR в SSR

Для больших приложений загрузка всех локалей сразу неэффективна. Используется динамическая загрузка:

async function loadLocaleData(locale) {
  const data = await import(`cldr-data/main/${locale}/numbers.json`);
  Globalize.load(data);
}

Проблема SSR здесь — асинхронность перед рендером. Поэтому загрузка должна завершаться до отправки HTML.

Потоковая генерация HTML

В потоковом SSR важно заранее подготовить Globalize:

  • до начала stream rendering
  • без изменения состояния во время рендера
async function handleRequest(req, res) {
  const locale = detectLocale(req);

  await loadLocaleData(locale);

  const globalize = new Globalize(locale);

  res.end(render(globalize));
}

Производительность и память

Основные узкие места SSR с Globalize:

  • хранение CLDR JSON в памяти
  • создание множества форматтеров
  • повторная загрузка локалей

Оптимизации:

  • singleton для CLDR
  • пул форматтеров
  • разделение runtime и build-time загрузки
  • минимизация локалей (tree-shaking CLDR)

Изоляция между запросами

SSR требует строгой изоляции:

  • нельзя изменять глобальную локаль
  • нельзя мутировать shared formatter instances
  • нельзя использовать глобальные кеши без ключа locale+options

Без этого возникают race conditions при параллельных запросах.

Серверная архитектура с несколькими локалями

Типичная схема:

  • слой middleware определяет locale
  • слой i18n инициализирует Globalize
  • слой рендера получает готовые форматтеры
  • слой ответа не содержит логики i18n

Такое разделение позволяет масштабировать SSR горизонтально без деградации качества локализации.