Промежуточные слои локализации в JavaScript обычно строятся вокруг стандарта Intl API и служат связующим звеном между данными приложения, выбранной локалью пользователя и механизмами форматирования. В такой архитектуре форматирование дат, чисел, валют, сравнений строк и сообщений не выполняется напрямую в бизнес-логике, а проходит через цепочку обработчиков, каждый из которых решает свою задачу: определение языка, загрузка ресурсов, нормализация значений и финальное преобразование через Intl.
Intl API представляет собой набор встроенных инструментов JavaScript для интернационализации. Основные компоненты:
Intl.DateTimeFormat — форматирование дат и времениIntl.NumberFormat — форматирование чисел, валют и
процентовIntl.Collator — локализованная сортировка строкIntl.RelativeTimeFormat — выражение относительного
времениIntl.PluralRules — правила множественных чиселКаждый из этих объектов опирается на ICU (International Components for Unicode), что обеспечивает консистентное поведение между средами выполнения.
Middleware в контексте локализации не заменяет Intl, а организует его использование в приложении.
Типичная цепочка локализации строится как последовательность функций, каждая из которых получает контекст запроса и передаёт его дальше:
Такой подход позволяет отделить инфраструктурную логику от бизнес-кода и UI.
Пример концептуальной модели:
request → detectLocale → loadMessages → attachIntlFormatters → handler → response
Первый этап — детектирование языка. Источники могут различаться:
Accept-LanguageMiddleware нормализует результат в стандарт 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-объектов в каждом компоненте.
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();
}
Такой подход уменьшает накладные расходы и обеспечивает единообразие форматирования.
Intl.NumberFormat используется для локализованного
представления чисел, процентов и валют. Middleware может предоставлять
абстракции поверх него:
req.formatMoney = (value, currency = "USD") =>
new Intl.NumberFormat(req.locale, {
style: "currency",
currency
}).format(value);
В более оптимизированных системах экземпляры форматтеров кэшируются
по комбинации locale + options, поскольку создание новых
объектов дорогостоящее.
Форматирование дат требует учета локальных правил отображения:
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
решает проблему локализованного сравнения:
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 часто включает стратегию кэширования:
locale + optionsconst 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);
}
Это снижает нагрузку при высоком трафике.
В серверных приложениях (например, Node.js) middleware работает на уровне HTTP-запросов. В клиентских приложениях аналогичный слой может быть реализован через:
Принцип остается одинаковым: единая точка доступа к Intl-форматированию.
В Express-подобных системах middleware интегрируется напрямую в цепочку обработки запроса. В современных SPA-фреймворках аналогичная логика реализуется через контексты:
Во всех случаях Intl используется как низкоуровневый слой, а middleware — как управляющая оболочка.
В SSR (server-side rendering) возникает задача синхронизации:
Middleware помогает сериализовать локаль и минимальный набор параметров в HTML, чтобы клиент мог восстановить контекст без повторного определения.
При проектировании middleware важно учитывать:
Стратегии обработки включают:
В зрелых системах формируется несколько устойчивых подходов:
Middleware становится слоем абстракции, скрывающим сложность Intl API и ICU-форматов за единым интерфейсом, доступным приложению на любом уровне.