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

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


Архитектура параллельного существования систем

На этапе миграции типичной практикой становится одновременная работа двух слоёв:

  • существующего механизма форматирования (legacy-слой);
  • нового слоя на базе Globalize.

Такой подход позволяет минимизировать риск регрессий и постепенно переносить функциональность без изменения всей кодовой базы одновременно.

Ключевым элементом становится абстракция над форматированием, которая изолирует конкретные реализации:

export function formatDate(value) {
  return legacyFormatDate(value);
}

После внедрения Globalize эта же точка становится маршрутизатором:

import Globalize from "globalize";

export function formatDate(value, locale) {
  return useGlobalize
    ? Globalize(locale).dateFormatter({ datetime: "medium" })(value)
    : legacyFormatDate(value);
}

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


Подготовка данных локализации

Globalize требует наличия CLDR-данных. На этапе миграции важным аспектом становится их предварительная загрузка и организация.

Типовая структура включает:

  • базовые данные языков;
  • числовые форматы;
  • правила плюрализации;
  • форматы дат и времени;
  • валютные обозначения.

Инициализация выполняется централизованно:

import Globalize from "globalize";
import likelySubtags from "cldr-data/supplemental/likelySubtags.json";
import numberingSystems from "cldr-data/supplemental/numberingSystems.json";
import plurals from "cldr-data/supplemental/plurals.json";

Globalize.load(likelySubtags);
Globalize.load(numberingSystems);
Globalize.load(plurals);

На этапе постепенной миграции загрузка данных часто разделяется по локалям, чтобы не увеличивать стартовый вес приложения. Это приводит к появлению динамической подгрузки:

async function loadLocaleData(locale) {
  const cldrData = await import(`cldr-data/main/${locale}/ca-gregorian.json`);
  Globalize.load(cldrData);
}

Инкрементальное внедрение форматирования чисел

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

Legacy-реализация:

function formatNumber(value) {
  return value.toLocaleString("ru-RU");
}

После введения Globalize добавляется альтернативный путь:

const globalize = Globalize("ru");

function formatNumber(value) {
  return globalize.numberFormatter()(value);
}

На промежуточном этапе часто сохраняются оба механизма, а переключение происходит на уровне конфигурации или feature flag.


Миграция дат и времени

Работа с датами требует большей осторожности из-за различий в форматах и временных зонах.

Legacy-код:

function formatDate(date) {
  return new Date(date).toLocaleDateString("ru-RU");
}

После подключения Globalize вводится форматтер:

const globalize = Globalize("ru");

const dateFormatter = globalize.dateFormatter({
  date: "long"
});

function formatDate(date) {
  return dateFormatter(new Date(date));
}

При постепенной миграции часто создаётся промежуточный адаптер, унифицирующий входные типы данных, поскольку разные части системы могут передавать строки, timestamp или объекты Date.


Миграция сообщений и строк с параметрами

Наиболее сложной частью является переход от конкатенации строк и простых шаблонов к message formatting.

Legacy-подход:

function greet(name) {
  return "Привет, " + name + "!";
}

Globalize-эквивалент:

const globalize = Globalize("ru");

const message = globalize.messageFormatter("greeting");

message({ name: "Иван" });

Где ресурс сообщений:

{
  "greeting": "Привет, {name}!"
}

На этапе миграции часто используется смешанный подход: часть сообщений остаётся в legacy-формате, часть переносится в CLDR-совместимые структуры.


Стратегия обёрток и адаптеров

Основным инструментом постепенной миграции становится слой адаптеров, который изолирует использование Globalize от остального кода.

Пример универсального форматтера:

class I18n {
  constructor(locale) {
    this.globalize = Globalize(locale);
  }

  formatNumber(value) {
    return this.globalize.numberFormatter()(value);
  }

  formatDate(value) {
    return this.globalize.dateFormatter({ date: "medium" })(value);
  }

  formatMessage(key, data) {
    return this.globalize.messageFormatter(key)(data);
  }
}

Дальнейшая миграция сводится к постепенному замещению внутренних реализаций методов этого класса без изменения его публичного интерфейса.


Пошаговое переключение функциональности

Переход на Globalize редко происходит сразу для всех типов форматирования. Типичный порядок миграции:

  1. числовые значения;
  2. валюты;
  3. даты;
  4. сообщения;
  5. сложные комбинированные шаблоны.

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


Управление feature flags

Для контроля миграции используется система флагов:

const useGlobalize = featureFlags.i18nGlobalize;

Флаг может применяться на уровне:

  • всего приложения;
  • отдельных модулей;
  • конкретных компонентов;
  • пользовательских сегментов.

Это позволяет проводить частичную миграцию без нарушения стабильности всей системы.


Проблемы совместимости типов данных

В процессе миграции часто выявляются расхождения в форматах данных:

  • строки даты вместо объектов Date;
  • числа в виде строк;
  • отсутствие нормализации локали;
  • различия в форматах валют.

Globalize требует строгой консистентности входных данных, поэтому вводится слой нормализации:

function normalizeDate(input) {
  return input instanceof Date ? input : new Date(input);
}

Такой слой становится частью адаптера, минимизируя расхождения между legacy и новым кодом.


Частичная локализация интерфейса

На этапе миграции интерфейс часто оказывается в состоянии смешанной локализации, когда разные компоненты используют разные механизмы форматирования.

Для устранения этого вводится правило приоритета:

  • UI-слой использует только адаптер Globalize;
  • legacy-функции запрещаются на уровне новых компонентов;
  • старые компоненты постепенно переписываются при изменении функциональности.

Динамическая подгрузка локалей

При постепенной миграции важным становится уменьшение первоначального объёма загружаемых данных. Используется ленивое подключение локалей:

const localeCache = new Map();

async function getGlobalize(locale) {
  if (!localeCache.has(locale)) {
    await loadLocaleData(locale);
    localeCache.set(locale, Globalize(locale));
  }
  return localeCache.get(locale);
}

Такой подход снижает нагрузку на старт приложения и позволяет масштабировать поддержку множества языков.


Контроль качества миграции

Проверка корректности перехода включает:

  • сравнение legacy и Globalize-выхода;
  • автоматизированные тесты форматирования;
  • snapshot-тестирование UI;
  • валидацию локалей.

Часто используется режим двойного вычисления:

const legacyResult = legacyFormat(value);
const newResult = globalizeFormat(value);

if (legacyResult !== newResult) {
  logMismatch(value, legacyResult, newResult);
}

Это позволяет выявлять расхождения до полного переключения системы.


Деинсталляция legacy-слоя

После завершения миграции удаление старой системы происходит постепенно:

  • сначала отключаются feature flags;
  • затем удаляются дублирующие функции;
  • после этого удаляются адаптеры legacy;
  • финально остаётся только слой Globalize.

При этом сохраняется единая точка инициализации локализации, обеспечивающая консистентность всей системы форматирования.