Встроенные механизмы интернационализации в JavaScript основаны на
принципе явного или неявного определения локали. Поведение большинства
классов Intl — Intl.DateTimeFormat,
Intl.NumberFormat, Intl.Collator,
Intl.RelativeTimeFormat, Intl.PluralRules,
Intl.DisplayNames — зависит от значения локали, которое
либо передаётся напрямую, либо выводится из окружения выполнения.
Ключевая архитектурная проблема интернационализации заключается в управлении источником локали: она может быть глобальной, контекстной или передаваться как зависимость. Именно последний подход — dependency injection локали — обеспечивает предсказуемость, тестируемость и масштабируемость кода.
Локаль в Intl представляет собой строку вида
"en", "en-US", "ru",
"zh-Hans-CN" и т.д. При создании форматтеров она передаётся
как первый аргумент конструктора:
const formatter = new Intl.NumberFormat("ru-RU");
formatter.format(123456.78);
Если локаль не указана, используется значение окружения:
const formatter = new Intl.NumberFormat();
В этом случае движок JavaScript определяет локаль из среды
выполнения: браузера, операционной системы или переменных окружения
Node.js (process.env.LANG, LC_ALL и др.).
Такое поведение превращает локаль в скрытую зависимость, что усложняет контроль над результатом форматирования.
Неявное получение локали приводит к нескольким архитектурным проблемам:
function formatPrice(value) {
return new Intl.NumberFormat().format(value);
}
Результат этой функции зависит от внешнего состояния, хотя явно это не отражено в сигнатуре.
Базовый уровень dependency injection локали заключается в передаче её через аргументы функций:
function formatPrice(value, locale) {
return new Intl.NumberFormat(locale).format(value);
}
Теперь зависимость становится явной. Поведение функции определяется только входными параметрами.
Преимущества:
При усложнении системы локаль обычно становится частью общего контекста приложения:
function formatPrice(value, context) {
const { locale } = context;
return new Intl.NumberFormat(locale).format(value);
}
Контекст может содержать не только локаль, но и валюту, таймзону, формат даты и другие параметры интернационализации.
Такой подход особенно характерен для серверных приложений и API-слоёв, где запрос пользователя несёт информацию о предпочтительной локали.
В Node.js и backend-средах локаль часто извлекается из HTTP-заголовков:
Accept-LanguageДалее она передаётся вниз по стеку вызовов:
function handler(req) {
const locale = req.headers["accept-language"];
return formatResponse(locale);
}
function formatResponse(locale) {
return new Intl.DateTimeFormat(locale).format(new Date());
}
Здесь локаль становится частью потока данных, а не глобального состояния.
В более структурированных архитектурах локаль внедряется через контейнер зависимостей:
const container = {
locale: "ru-RU",
formatNumber(value) {
return new Intl.NumberFormat(this.locale).format(value);
}
};
Или через фабрики:
function createI18n(locale) {
return {
formatNumber: (value) =>
new Intl.NumberFormat(locale).format(value),
formatDate: (date) =>
new Intl.DateTimeFormat(locale).format(date)
};
}
Фабричный подход позволяет создавать изолированные экземпляры интернационализации для разных пользователей.
Конструкторы Intl достаточно тяжёлые с точки зрения
производительности. Поэтому часто используется кеширование:
const cache = new Map();
function getNumberFormatter(locale) {
if (!cache.has(locale)) {
cache.set(locale, new Intl.NumberFormat(locale));
}
return cache.get(locale);
}
function formatNumber(value, locale) {
return getNumberFormatter(locale).format(value);
}
Здесь локаль становится ключом кеша, а форматтер — зависимостью, управляемой через DI-подобный механизм.
Функциональный подход позволяет создавать специализированные функции с зафиксированной локалью:
function createPriceFormatter(locale) {
const formatter = new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD"
});
return (value) => formatter.format(value);
}
const formatPrice = createPriceFormatter("en-US");
Здесь локаль внедряется один раз, после чего функция становится независимой от внешнего контекста.
В компонентных архитектурах локаль часто распространяется через context:
const LocaleContext = React.createContext("en-US");
function Price({ value }) {
const locale = React.useContext(LocaleContext);
const formatter = new Intl.NumberFormat(locale);
return formatter.format(value);
}
Локаль становится зависимостью, которую получает каждый компонент без явной передачи через props, что снижает связанность.
Часто встречается анти-паттерн глобальных форматтеров:
const formatter = new Intl.NumberFormat("en-US");
function format(value) {
return formatter.format(value);
}
Такая реализация фиксирует локаль навсегда, исключая гибкость. При необходимости поддержки нескольких локалей возникает необходимость пересоздания глобального состояния или усложнённого кеширования.
Явная передача локали упрощает тестирование:
test("formats number in Russian locale", () => {
const result = formatPrice(1000, "ru-RU");
expect(result).toBe("1 000");
});
Отсутствие скрытых зависимостей исключает необходимость мокать глобальное окружение или системные настройки.
Intl поддерживает механизм выбора ближайшей доступной
локали:
new Intl.NumberFormat(["fr-CA", "fr-FR", "en"]).format(1000);
Dependency injection локали часто дополняется логикой нормализации:
В сложных приложениях локаль может изменяться на разных уровнях:
DI позволяет формировать цепочку переопределений:
function formatDate(date, { locale = "en-US" } = {}) {
return new Intl.DateTimeFormat(locale).format(date);
}
Такой подход создаёт гибкую систему приоритизации зависимостей.
При DI локаль становится ключевым параметром мемоизации:
const memo = new Map();
function format(locale, value) {
const key = `${locale}:${value}`;
if (memo.has(key)) return memo.get(key);
const result = new Intl.NumberFormat(locale).format(value);
memo.set(key, result);
return result;
}
Таким образом локаль участвует в построении кэша как часть контракта функции.
Dependency injection локали по сути реализует инверсию управления: не функция запрашивает локаль у окружения, а окружение передаёт её функции. Это устраняет скрытые связи и делает поток данных явным.
// плохо: функция сама "знает", где взять локаль
function format(value) {
return new Intl.NumberFormat(getLocaleFromGlobal()).format(value);
}
// лучше: локаль передана извне
function format(value, locale) {
return new Intl.NumberFormat(locale).format(value);
}