Постепенная миграция

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

Ключевой принцип — изолировать форматирование в единый слой, который становится точкой контроля. Любые изменения внутри этого слоя не затрагивают бизнес-логику.


Инвентаризация существующих форматирований

Перед внедрением Intl API требуется выявить все места, где происходит локализация:

  • ручное форматирование дат через Date
  • конкатенация строк для представления чисел
  • кастомные функции округления и разделителей
  • локализация через таблицы строк
  • сторонние библиотеки вроде moment.js, numeral.js

Особое внимание уделяется скрытым форматированиям:

  • логирование дат в UI и консоли
  • сериализация данных перед отправкой пользователю
  • отображение валют и процентов
  • сортировка списков вручную

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


Проверка поддержки Intl и feature detection

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-слоя

Fallback необходим для окружений с ограниченной поддержкой ICU.

function createNumberFormatter(locale) {
  if (typeof Intl !== "undefined" && Intl.NumberFormat) {
    return new Intl.NumberFormat(locale);
  }

  return {
    format: (value) => value.toString()
  };
}

Такая конструкция гарантирует стабильное поведение, даже если форматирование будет упрощено.


Polyfill и расширение ICU

Для унификации поведения на сервере и в браузере используются polyfill-решения:

  • @formatjs/intl семейство
  • full-icu для Node.js
  • сборки с embedded ICU

Node.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);
}

Динамическая смена локали требует пересоздания форматтеров, так как они кэшируют локаль и параметры.


Постепенное внедрение через feature flags

Миграция часто сопровождается включением 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);
});

Для локалей тесты часто строятся параметризованно:

  • ru-RU
  • en-US
  • de-DE
  • ja-JP

Дополнительно используются snapshot-тесты для UI-слоя, фиксирующие отображение.


Параллельный режим работы систем форматирования

На промежуточных этапах обе системы работают одновременно:

  • legacy используется в критичных частях интерфейса
  • Intl используется в новых модулях
  • результаты могут логироваться для сравнения

Это создаёт временную двойственность, которая постепенно устраняется после стабилизации поведения.