Работа с legacy кодом

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

Типичный устаревший код в JavaScript-проектах содержит три основных категории локализационных решений:

  • прямые вызовы toLocaleString() без централизованной настройки
  • кастомные функции форматирования дат и чисел
  • строковые шаблоны с ручной подстановкой значений

Пример legacy-кода:

function formatPrice(price) {
  return price.toFixed(2) + " $";
}

function formatDate(date) {
  return date.getDate() + "." + (date.getMonth() + 1) + "." + date.getFullYear();
}

Подобный код плохо масштабируется, не поддерживает локали и усложняет переход на единый i18n-слой.

Подход к интеграции Globalize поверх legacy-функций

Основной принцип миграции — обёртка вместо переписывания. Вместо удаления старых функций вводится слой адаптеров, который делегирует форматирование Globalize.

Инициализация базовой конфигурации

import Globalize from "globalize";

Globalize.load(
  require("cldr-data").entireSupplemental(),
  require("cldr-data").entireMainFor("en", "ru")
);

Globalize.locale("ru");

На этом этапе важно не пытаться сразу заменить весь legacy-код, а подготовить инфраструктуру.

Постепенная замена форматирования чисел

Legacy-функция:

function formatPrice(price) {
  return price.toFixed(2) + " $";
}

Переходный адаптер:

const numberFormatter = Globalize.numberFormatter({
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

function formatPrice(price) {
  return numberFormatter(price) + " $";
}

Ключевой момент: сохраняется интерфейс функции, но внутренность становится локализованной.

Расширение на разные валюты

Legacy-код часто предполагает одну валюту. Globalize позволяет параметризовать форматирование:

function formatPrice(price, currency) {
  const formatter = Globalize.currencyFormatter(currency, {
    style: "symbol"
  });

  return formatter(price);
}

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

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

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

function formatDate(date) {
  return date.getDate() + "." + (date.getMonth() + 1) + "." + date.getFullYear();
}

Адаптация через Globalize:

const dateFormatter = Globalize.dateFormatter({
  date: "short"
});

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

При этом сохраняется единая точка форматирования, что критично для постепенной миграции.

Оборачивание legacy API без изменения потребителей

В крупных приложениях невозможно изменить все вызовы функций сразу. Поэтому применяется паттерн compatibility layer.

import * as Legacy from "./legacy";

export function formatPrice(price) {
  return Legacy.formatPrice(price);
}

Позднее внутренняя реализация Legacy заменяется:

import Globalize from "globalize";

const numberFormatter = Globalize.numberFormatter({
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

export function formatPrice(price) {
  return numberFormatter(price) + " $";
}

Таким образом внешний контракт сохраняется, а внутренняя реализация постепенно мигрирует.

Проблема смешанных локалей в legacy-системах

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

Типичный анти-паттерн:

// module A
date.toLocaleDateString("en-US");

// module B
date.toLocaleDateString("de-DE");

При переходе на Globalize вводится централизованный менеджер локали:

let currentLocale = "ru";

export function setLocale(locale) {
  currentLocale = locale;
  Globalize.locale(locale);
}

export function getLocale() {
  return currentLocale;
}

Все форматтеры начинают зависеть от глобального состояния, но управляемого централизованно.

Инкапсуляция Globalize для legacy-кода

Чтобы минимизировать изменения по проекту, создаётся обёртка:

import Globalize from "globalize";

export const i18n = {
  formatNumber(value) {
    return Globalize.numberFormatter()(value);
  },

  formatDate(value) {
    return Globalize.dateFormatter({ date: "medium" })(value);
  },

  formatCurrency(value, currency) {
    return Globalize.currencyFormatter(currency)(value);
  }
};

Legacy-код может продолжать использовать единый объект:

import { i18n } from "./i18n";

function render(price, date) {
  return i18n.formatCurrency(price, "USD") + " " + i18n.formatDate(date);
}

Совместимость с прямыми вызовами toLocaleString

Полная миграция часто невозможна, поэтому часть системы продолжает использовать встроенные API.

Подход — унификация через полифилл-слой:

function formatNumber(value) {
  if (Globalize) {
    return Globalize.numberFormatter()(value);
  }

  return value.toLocaleString();
}

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

Обработка legacy-шаблонов строк

Старые системы часто используют конкатенацию:

"Hello " + name + ", you have " + count + " messages"

Globalize не заменяет шаблонизацию напрямую, но позволяет стандартизировать данные:

const messageFormatter = (name, count) => {
  const nf = Globalize.numberFormatter();
  return `Hello ${name}, you have ${nf(count)} messages`;
};

Для более сложных случаев рекомендуется ввод промежуточного слоя сообщений.

Инкрементальная миграция модулей

На практике переход выполняется поэтапно:

  1. внедрение Globalize рядом с legacy-кодом
  2. создание адаптеров без изменения API
  3. замена внутренних реализаций функций
  4. унификация локали
  5. удаление старых форматтеров

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

Типичные ошибки при интеграции с legacy-кодом

Создание новых форматтеров внутри функций

function format(price) {
  return Globalize.numberFormatter()(price);
}

Проблема — создание formatter на каждый вызов. В legacy-системах с высокой нагрузкой это приводит к деградации производительности.

Правильный подход — кэширование:

const formatter = Globalize.numberFormatter();

function format(price) {
  return formatter(price);
}

Разные инстансы Globalize

Частая ошибка при модульной архитектуре:

  • один модуль загружает CLDR
  • другой создаёт собственный Globalize

Это приводит к несогласованности локалей. В legacy-системе должен быть один источник конфигурации.

Частичная миграция без контроля

Если часть функций использует Globalize, а часть — ручное форматирование, возникает фрагментация отображения данных. Это особенно критично для финансовых и аналитических интерфейсов.

Стратегия безопасного перехода

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

  • legacy-режим (старые функции)
  • i18n-режим (Globalize)

Переключение может быть feature-flag’ом:

function formatDate(date) {
  if (FEATURES.i18n) {
    return Globalize.dateFormatter({ date: "short" })(date);
  }

  return legacyFormat(date);
}

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

Работа с тестами при миграции legacy-кода

При переходе на Globalize тесты часто ломаются из-за изменения форматов.

Решение — нормализация:

expect(formatPrice(10)).toMatch(/10/);

Либо фиксация локали в тестах:

Globalize.locale("en");

Это устраняет нестабильность результатов.

Итоговая архитектурная модель миграции

В результате перехода система обычно приобретает трёхслойную структуру:

  • legacy API (не изменяется)
  • адаптеры Globalize
  • CLDR-данные и конфигурация локали

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