Переход на Intl API в JavaScript редко происходит
одномоментно. В реальных кодовых базах форматирование чисел, дат и строк
обычно размазано по проекту: часть реализована через самописные функции,
часть — через сторонние библиотеки, часть — через локальные условные
ветки. Постепенная миграция строится вокруг идеи сосуществования старого
и нового форматирования с поэтапной заменой без нарушения поведения
приложения.
Ключевой принцип — изолировать форматирование в единый слой, который становится точкой контроля. Любые изменения внутри этого слоя не затрагивают бизнес-логику.
Перед внедрением Intl API требуется выявить все места,
где происходит локализация:
Datemoment.js,
numeral.jsОсобое внимание уделяется скрытым форматированиям:
Результатом инвентаризации становится карта миграции: какие части можно заменить напрямую, а какие требуют адаптера.
Intl является частью ECMAScript Internationalization
API, но поведение может отличаться в разных окружениях.
Базовая проверка доступности:
if (typeof Intl !== "undefined") {
// поддержка доступна
}
Для конкретных возможностей используется проверка через попытку создания форматтера:
const supported = typeof Intl === "object" &&
typeof Intl.NumberFormat === "function";
Важно учитывать, что наличие API не гарантирует поддержку всех локалей. Некоторые окружения имеют ограниченный ICU набор.
Основная точка миграции — Intl.NumberFormat.
Старый подход:
function formatPrice(value) {
return value.toFixed(2).replace(".", ",");
}
Переходный слой:
const numberFormatter = new Intl.NumberFormat("ru-RU", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
function formatPrice(value) {
return numberFormatter.format(value);
}
Гибридная стратегия:
const useIntl = typeof Intl !== "undefined";
function formatPrice(value) {
if (useIntl) {
return numberFormatter.format(value);
}
return value.toFixed(2).replace(".", ",");
}
Такой подход позволяет сохранять одинаковое поведение на старых платформах.
Intl.DateTimeFormat заменяет ручные реализации работы с
датами.
До миграции:
function formatDate(date) {
const d = new Date(date);
return `${d.getDate()}.${d.getMonth() + 1}.${d.getFullYear()}`;
}
После внедрения:
const dateFormatter = new Intl.DateTimeFormat("ru-RU", {
year: "numeric",
month: "2-digit",
day: "2-digit"
});
function formatDate(date) {
return dateFormatter.format(new Date(date));
}
Переходный вариант часто требует сохранения старого формата для совместимости с legacy-интерфейсами:
function formatDate(date, mode = "intl") {
if (mode === "intl") {
return dateFormatter.format(new Date(date));
}
const d = new Date(date);
return `${d.getDate()}.${d.getMonth() + 1}.${d.getFullYear()}`;
}
Intl.Collator используется для корректной локализованной
сортировки.
До миграции:
items.sort((a, b) => a.localeCompare(b));
После:
const collator = new Intl.Collator("ru-RU", {
sensitivity: "base"
});
items.sort(collator.compare);
Особенность миграции заключается в том, что изменение
sensitivity, numeric и caseFirst
может изменить порядок элементов, поэтому внедрение требует согласования
с существующей бизнес-логикой.
Для минимизации рисков вводится абстракция:
const i18n = {
number: new Intl.NumberFormat("ru-RU"),
currency: new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB"
}),
date: new Intl.DateTimeFormat("ru-RU"),
formatNumber(value) {
return this.number.format(value);
},
formatCurrency(value) {
return this.currency.format(value);
},
formatDate(value) {
return this.date.format(new Date(value));
}
};
Такой слой позволяет постепенно заменять внутреннюю реализацию без изменения потребителей.
Fallback необходим для окружений с ограниченной поддержкой ICU.
function createNumberFormatter(locale) {
if (typeof Intl !== "undefined" && Intl.NumberFormat) {
return new Intl.NumberFormat(locale);
}
return {
format: (value) => value.toString()
};
}
Такая конструкция гарантирует стабильное поведение, даже если форматирование будет упрощено.
Для унификации поведения на сервере и в браузере используются polyfill-решения:
@formatjs/intl семействоfull-icu для Node.jsNode.js может запускаться в режиме без полного ICU, что приводит к ограничению локалей:
node --icu-data-dir=node_modules/full-icu
Сервер часто используется для предварительного рендера, но форматирование может отличаться от клиентского из-за различий в локалях.
Типичный подход:
IntlАльтернативный подход:
IntlВыбор зависит от требований к консистентности и SEO.
В процессе миграции важно централизовать управление локалью:
let currentLocale = "ru-RU";
function setLocale(locale) {
currentLocale = locale;
}
function getNumberFormatter() {
return new Intl.NumberFormat(currentLocale);
}
Динамическая смена локали требует пересоздания форматтеров, так как они кэшируют локаль и параметры.
Миграция часто сопровождается включением Intl через
флаги:
function formatNumber(value) {
if (featureFlags.intlNumbers) {
return intlFormatter.format(value);
}
return legacyFormatNumber(value);
}
Это позволяет:
Несовпадение форматов
Разные локали могут давать неожиданные результаты:
Кэширование форматтеров
Intl объекты тяжелы для создания, но безопасны для
переиспользования. Ошибкой является создание нового форматтера при
каждом вызове.
Нестабильность сортировки
Изменение параметров Collator может менять порядок
элементов даже при одинаковых данных.
Различия ICU окружений
Node.js и браузеры могут возвращать разные результаты для одной локали.
Миграция требует тестов на равенство поведения старой и новой системы:
test("formatNumber parity", () => {
const legacy = legacyFormat(12345.67);
const intl = intlFormat(12345.67);
expect(intl).toBe(legacy);
});
Для локалей тесты часто строятся параметризованно:
Дополнительно используются snapshot-тесты для UI-слоя, фиксирующие отображение.
На промежуточных этапах обе системы работают одновременно:
Intl используется в новых модуляхЭто создаёт временную двойственность, которая постепенно устраняется после стабилизации поведения.