Интерфейс Intl в современных JavaScript-окружениях
реализован неравномерно: базовые классы могут присутствовать, но
отдельные части API (например, Intl.PluralRules,
Intl.RelativeTimeFormat, Intl.Segmenter) часто
отсутствуют или имеют ограниченную реализацию.
Типичная стратегия начинается с определения доступности конкретной функции:
const hasDateTimeFormat = typeof Intl !== "undefined" &&
typeof Intl.DateTimeFormat === "function";
const hasPluralRules = typeof Intl !== "undefined" &&
typeof Intl.PluralRules === "function";
const hasRelativeTimeFormat = typeof Intl !== "undefined" &&
typeof Intl.RelativeTimeFormat === "function";
Важный аспект заключается в том, что наличие глобального
Intl не гарантирует полноту реализации. Поэтому проверка
выполняется на уровне конкретных конструкторов.
Статическое подключение полифилов увеличивает размер бандла без гарантии использования. Динамическая загрузка позволяет подключать полифилы только при необходимости, минимизируя первичную загрузку.
Базовая схема:
async function ensurePluralRules() {
if (typeof Intl.PluralRules !== "function") {
await import("@formatjs/intl-pluralrules/polyfill");
await import("@formatjs/intl-pluralrules/locale-data/ru");
}
}
Ключевая идея заключается в ленивой инициализации: полифил подключается только при первом обращении к соответствующему API.
Разные части Intl требуют отдельных пакетов или
модулей:
Intl.DateTimeFormat — форматирование дат и времениIntl.NumberFormat — числовые форматыIntl.PluralRules — правила множественных формIntl.RelativeTimeFormat — относительное времяIntl.Segmenter — сегментация текстаКаждый модуль может иметь собственный полифил, что делает возможной точечную загрузку:
async function ensureRelativeTimeFormat() {
if (typeof Intl.RelativeTimeFormat !== "function") {
await import("@formatjs/intl-relativetimeformat/polyfill");
await import("@formatjs/intl-relativetimeformat/locale-data/ru");
}
}
Большинство полифилов Intl требуют загрузки языковых данных отдельно. Это важный фактор оптимизации: сами полифилы часто малы, но локали могут занимать значительный объём.
Структура загрузки:
async function loadIntlLocale(locale) {
switch (locale) {
case "ru":
await import("@formatjs/intl-pluralrules/locale-data/ru");
await import("@formatjs/intl-relativetimeformat/locale-data/ru");
break;
case "en":
await import("@formatjs/intl-pluralrules/locale-data/en");
await import("@formatjs/intl-relativetimeformat/locale-data/en");
break;
}
}
Разделение логики позволяет избежать загрузки ненужных языков.
Одним из устойчивых подходов является создание абстракции над
Intl, которая гарантирует готовность API:
let pluralRulesPromise;
function getPluralRules(locale) {
if (!pluralRulesPromise) {
pluralRulesPromise = (async () => {
if (typeof Intl.PluralRules !== "function") {
await import("@formatjs/intl-pluralrules/polyfill");
}
return new Intl.PluralRules(locale);
})();
}
return pluralRulesPromise;
}
Подобная схема полезна для предотвращения повторных загрузок и гонок инициализации.
Часто Intl используется точечно: форматирование дат, валют, сообщений. Это позволяет строить ленивые функции-обёртки:
export async function formatDate(date, locale) {
if (typeof Intl.DateTimeFormat !== "function") {
await import("@formatjs/intl-datetimeformat/polyfill");
await import("@formatjs/intl-datetimeformat/locale-data/ru");
}
const formatter = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric"
});
return formatter.format(date);
}
Загрузка происходит только при первом вызове, после чего используется нативная реализация или уже загруженный полифил.
Инструменты сборки влияют на эффективность динамической загрузки.
Webpack поддерживает код-сплиттинг через import():
const loadIntl = () => import("./intl-polyfills");
Дальнейшая сегментация позволяет разделить:
Vite использует ES-модули и позволяет автоматически разделять чанки:
if (!Intl.RelativeTimeFormat) {
await import("intl-relativetimeformat");
}
В серверном окружении (Node.js) поведение Intl может
отличаться от браузерного. В некоторых версиях Node поддержка
расширяется, но не всегда совпадает с клиентской.
Условная логика:
const isBrowser = typeof window !== "undefined";
async function ensureIntl() {
if (!isBrowser) return;
if (typeof Intl.RelativeTimeFormat !== "function") {
await import("@formatjs/intl-relativetimeformat/polyfill");
}
}
Такой подход предотвращает избыточную загрузку в SSR.
Повторная загрузка модулей нежелательна даже при динамическом импорте. Механизм кэширования строится на промисах:
const cache = new Map();
async function loadPolyfill(key, importer) {
if (!cache.has(key)) {
cache.set(key, importer());
}
return cache.get(key);
}
Использование:
await loadPolyfill("pluralRules", async () => {
if (typeof Intl.PluralRules !== "function") {
await import("@formatjs/intl-pluralrules/polyfill");
}
});
Фрагментация API позволяет минимизировать загрузку:
Пример комбинированной стратегии:
async function ensureIntlCapabilities(locale) {
if (typeof Intl.DateTimeFormat !== "function") {
await import("@formatjs/intl-datetimeformat/polyfill");
}
if (typeof Intl.NumberFormat !== "function") {
await import("@formatjs/intl-numberformat/polyfill");
}
if (typeof Intl.RelativeTimeFormat !== "function") {
await import("@formatjs/intl-relativetimeformat/polyfill");
}
await import(`@formatjs/intl-pluralrules/locale-data/${locale}`);
}
При отсутствии полного Intl API возможна каскадная деградация:
Пример fallback-цепочки:
function formatNumber(value, locale) {
if (typeof Intl.NumberFormat === "function") {
return new Intl.NumberFormat(locale).format(value);
}
return String(value);
}
При поддержке нескольких языков загрузка локалей становится критической точкой. Стратегия:
async function switchLocale(locale) {
await import(`@formatjs/intl-numberformat/locale-data/${locale}`);
await import(`@formatjs/intl-datetimeformat/locale-data/${locale}`);
}
В крупных приложениях Intl-функции часто управляются через feature flags:
const features = {
relativeTime: true,
segmenter: false
};
async function initIntl() {
if (features.relativeTime && typeof Intl.RelativeTimeFormat !== "function") {
await import("@formatjs/intl-relativetimeformat/polyfill");
}
if (features.segmenter && typeof Intl.Segmenter !== "function") {
await import("@formatjs/intl-segmenter/polyfill");
}
}
Такой подход снижает поверхность загрузки и позволяет контролировать поведение на уровне конфигурации.
Intl.Segmenter относится к наиболее современным частям
API и часто отсутствует в браузерах.
async function ensureSegmenter() {
if (typeof Intl.Segmenter !== "function") {
await import("@formatjs/intl-segmenter/polyfill");
}
return new Intl.Segmenter("ru", { granularity: "word" });
}
Из-за сложности алгоритма сегментации этот полифил обычно имеет больший размер, что делает динамическую загрузку особенно значимой.
Динамические полифилы хорошо вписываются в модульную архитектуру:
Разделение ответственности позволяет избегать дублирования загрузок и упрощает контроль над зависимостями Intl API.