Middleware для локализации

Промежуточные слои локализации в JavaScript обычно строятся вокруг стандарта Intl API и служат связующим звеном между данными приложения, выбранной локалью пользователя и механизмами форматирования. В такой архитектуре форматирование дат, чисел, валют, сравнений строк и сообщений не выполняется напрямую в бизнес-логике, а проходит через цепочку обработчиков, каждый из которых решает свою задачу: определение языка, загрузка ресурсов, нормализация значений и финальное преобразование через Intl.

Intl API представляет собой набор встроенных инструментов JavaScript для интернационализации. Основные компоненты:

  • Intl.DateTimeFormat — форматирование дат и времени
  • Intl.NumberFormat — форматирование чисел, валют и процентов
  • Intl.Collator — локализованная сортировка строк
  • Intl.RelativeTimeFormat — выражение относительного времени
  • Intl.PluralRules — правила множественных чисел

Каждый из этих объектов опирается на ICU (International Components for Unicode), что обеспечивает консистентное поведение между средами выполнения.

Middleware в контексте локализации не заменяет Intl, а организует его использование в приложении.

Архитектура middleware-цепочки локализации

Типичная цепочка локализации строится как последовательность функций, каждая из которых получает контекст запроса и передаёт его дальше:

  1. Определение локали пользователя
  2. Выбор набора переводов
  3. Настройка форматтеров Intl
  4. Обогащение контекста функциями форматирования
  5. Рендеринг слоя представления

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

Пример концептуальной модели:

request → detectLocale → loadMessages → attachIntlFormatters → handler → response

Определение локали как первый слой middleware

Первый этап — детектирование языка. Источники могут различаться:

  • заголовок Accept-Language
  • cookie или localStorage
  • параметр URL
  • профиль пользователя

Middleware нормализует результат в стандарт BCP 47:

function detectLocale(req, res, next) {
  const header = req.headers["accept-language"];
  const locale = parseLocale(header) || "en-US";
  req.locale = locale;
  next();
}

На этом этапе важно учитывать fallback-цепочки: если ru-KZ не поддерживается, система может откатиться к ru или en.

Загрузка и кэширование локализованных сообщений

После определения локали подключается слой ресурсов. Он может использовать JSON-файлы, ICU message format или базы данных.

Middleware отвечает за:

  • загрузку словарей
  • кэширование по ключу локали
  • дедупликацию запросов
  • ленивую подгрузку
async function loadMessages(req, res, next) {
  const locale = req.locale;
  if (!cache.has(locale)) {
    cache.set(locale, await fetchMessages(locale));
  }
  req.messages = cache.get(locale);
  next();
}

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

Интеграция Intl форматтеров в middleware

Ключевая задача — создать набор готовых функций форматирования, привязанных к текущей локали. Это позволяет избегать повторного создания Intl-объектов в каждом компоненте.

function attachIntl(req, res, next) {
  const locale = req.locale;

  req.intl = {
    number: new Intl.NumberFormat(locale),
    currency: new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }),
    date: new Intl.DateTimeFormat(locale),
    relativeTime: new Intl.RelativeTimeFormat(locale)
  };

  next();
}

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

Middleware для форматирования чисел и валют

Intl.NumberFormat используется для локализованного представления чисел, процентов и валют. Middleware может предоставлять абстракции поверх него:

req.formatMoney = (value, currency = "USD") =>
  new Intl.NumberFormat(req.locale, {
    style: "currency",
    currency
  }).format(value);

В более оптимизированных системах экземпляры форматтеров кэшируются по комбинации locale + options, поскольку создание новых объектов дорогостоящее.

Работа с датами через Intl.DateTimeFormat

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

req.formatDate = (date) =>
  new Intl.DateTimeFormat(req.locale, {
    year: "numeric",
    month: "long",
    day: "2-digit"
  }).format(date);

Middleware может также нормализовать входные данные, приводя их к единому формату UTC перед отображением.

Поддержка относительного времени

Intl.RelativeTimeFormat позволяет выражать время в виде «2 часа назад» или «через 3 дня». Middleware часто инкапсулирует логику выбора единицы измерения:

req.formatRelativeTime = (value, unit) =>
  new Intl.RelativeTimeFormat(req.locale, { numeric: "auto" }).format(value, unit);

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

  • секунды
  • минуты
  • часы
  • дни

Сортировка строк с Intl.Collator в middleware

Сортировка в разных языках отличается. Intl.Collator решает проблему локализованного сравнения:

req.collator = new Intl.Collator(req.locale, { sensitivity: "base" });

const sorted = items.sort(req.collator.compare);

Middleware может предоставлять готовую функцию сортировки для коллекций, снижая риск ошибок при ручной реализации.

Обработка множественных чисел и сообщений

Хотя Intl API напрямую не реализует полноценный message formatting, Intl.PluralRules используется для выбора правильной формы слова:

const rules = new Intl.PluralRules(req.locale);

function pluralKey(n) {
  return rules.select(n);
}

Middleware связывает это с набором переводов:

req.t = (key, count) => {
  const rule = new Intl.PluralRules(req.locale).select(count);
  return req.messages[key][rule];
};

Кэширование Intl-объектов как часть middleware-оптимизации

Создание объектов Intl относительно дорого, поэтому middleware часто включает стратегию кэширования:

  • ключ: locale + options
  • значение: экземпляр форматтера
const cache = new Map();

function getFormatter(locale, options) {
  const key = JSON.stringify({ locale, options });
  if (!cache.has(key)) {
    cache.set(key, new Intl.NumberFormat(locale, options));
  }
  return cache.get(key);
}

Это снижает нагрузку при высоком трафике.

Middleware в серверных и клиентских приложениях

В серверных приложениях (например, Node.js) middleware работает на уровне HTTP-запросов. В клиентских приложениях аналогичный слой может быть реализован через:

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

Принцип остается одинаковым: единая точка доступа к Intl-форматированию.

Интеграция с фреймворками

В Express-подобных системах middleware интегрируется напрямую в цепочку обработки запроса. В современных SPA-фреймворках аналогичная логика реализуется через контексты:

  • React Context для хранения locale и formatter-объектов
  • Vue provide/inject
  • Angular services

Во всех случаях Intl используется как низкоуровневый слой, а middleware — как управляющая оболочка.

Гидратация и универсальные приложения

В SSR (server-side rendering) возникает задача синхронизации:

  • сервер формирует форматтеры Intl
  • клиент должен использовать ту же локаль
  • предотвращается расхождение HTML

Middleware помогает сериализовать локаль и минимальный набор параметров в HTML, чтобы клиент мог восстановить контекст без повторного определения.

Ошибки и граничные случаи

При проектировании middleware важно учитывать:

  • неподдерживаемые локали
  • отсутствие данных перевода
  • различия ICU между окружениями
  • нестабильные форматы дат в разных браузерах
  • необходимость fallback-цепочек

Стратегии обработки включают:

  • fallback locale chain
  • дефолтные сообщения
  • строгую валидацию ICU-форматов

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

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

  • централизованный i18n-service
  • lazy-loading локалей
  • кэширование форматтеров
  • разделение runtime и build-time переводов
  • унификация интерфейса форматирования

Middleware становится слоем абстракции, скрывающим сложность Intl API и ICU-форматов за единым интерфейсом, доступным приложению на любом уровне.