Обновление legacy систем

Легаси-системы, построенные до появления современного стандарта ECMAScript Internationalization API, часто содержат самодельные решения для форматирования дат, чисел, валют, сортировки и локализации текста. Эти решения обычно основаны на строковых шаблонах, регулярных выражениях и ручной поддержке локалей, что приводит к накоплению технического долга и росту числа скрытых ошибок при масштабировании системы на новые регионы.

Классические реализации локализации в старых кодовых базах чаще всего обладают рядом характерных ограничений:

  • фиксированные форматы дат вида DD.MM.YYYY или MM/DD/YYYY без учета региона;
  • ручная конкатенация строк для чисел и валют;
  • отсутствие корректной обработки множественных форм;
  • неконсистентная сортировка строк в зависимости от языка;
  • дублирование логики форматирования в разных слоях приложения;
  • зависимость от внешних библиотек, которые перестают поддерживаться.

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

Intl API как основа современной локализации

ECMAScript Internationalization API (Intl) предоставляет стандартизированный набор инструментов для работы с локалями непосредственно в языке JavaScript. Он опирается на ICU (International Components for Unicode), что обеспечивает согласованное поведение между средами выполнения.

Ключевые компоненты API:

  • Intl.DateTimeFormat — форматирование дат и времени
  • Intl.NumberFormat — форматирование чисел и валют
  • Intl.Collator — лексикографическая сортировка строк
  • Intl.PluralRules — работа с множественными формами
  • Intl.RelativeTimeFormat — относительное время («2 дня назад»)
  • Intl.ListFormat — форматирование списков

Замена самодельного форматирования дат

В legacy-коде часто встречается подобная логика:

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

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

Современная замена:

const formatter = new Intl.DateTimeFormat('de-DE', {
  year: 'numeric',
  month: '2-digit',
  day: '2-digit'
});

formatter.format(new Date());

Использование Intl.DateTimeFormat устраняет необходимость ручного управления форматами и снижает риск ошибок при переходе между регионами.

Особое значение имеет работа с часовыми поясами:

const formatter = new Intl.DateTimeFormat('en-GB', {
  timeZone: 'Europe/London',
  dateStyle: 'full',
  timeStyle: 'short'
});

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

Миграция числовых форматов и валют

Legacy-решения часто используют примитивную замену точек и запятых:

function formatNumber(n) {
  return n.toString().replace(".", ",");
}

Подобная логика не учитывает группировку разрядов, локальные правила округления и валютные обозначения.

Intl.NumberFormat решает эти проблемы:

const formatter = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
});

formatter.format(1234567.89);

Также возможно форматирование процентов и единиц измерения:

new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(0.42);

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

const cache = new Map();

function getNumberFormatter(locale, options) {
  const key = locale + JSON.stringify(options);
  if (!cache.has(key)) {
    cache.set(key, new Intl.NumberFormat(locale, options));
  }
  return cache.get(key);
}

Замена кастомной сортировки строк

Legacy-системы часто используют простое сравнение:

items.sort((a, b) => a.name > b.name ? 1 : -1);

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

Intl.Collator обеспечивает корректную локализованную сортировку:

const collator = new Intl.Collator('tr', {
  sensitivity: 'base'
});

items.sort((a, b) => collator.compare(a.name, b.name));

Особенно критично это для языков с особыми правилами сортировки, где простое сравнение Unicode-кодов приводит к некорректному порядку.

Работа с множественными формами

Legacy-код часто содержит цепочки условий:

function formatItems(count) {
  if (count === 1) return count + " item";
  if (count < 5) return count + " items";
  return count + " items";
}

Подобные конструкции не масштабируются на языки с более сложной грамматикой.

Intl.PluralRules позволяет выделить категорию множественности:

const rules = new Intl.PluralRules('ru-RU');

rules.select(1); // "one"
rules.select(2); // "few"
rules.select(5); // "many"

На основе результата строится таблица переводов:

const messages = {
  one: '1 файл',
  few: 'несколько файлов',
  many: 'много файлов'
};

const rule = rules.select(count);
const message = messages[rule];

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

Относительное время в пользовательских интерфейсах

Ручные реализации вида:

function timeAgo(ms) {
  const diff = Date.now() - ms;
  return Math.floor(diff / 1000 / 60) + " minutes ago";
}

не учитывают локализацию и грамматику.

Intl.RelativeTimeFormat стандартизирует этот процесс:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

rtf.format(-1, 'day'); // "yesterday"
rtf.format(-5, 'day'); // "5 days ago"

Это особенно важно в интерфейсах с высокой динамикой данных: ленты новостей, уведомления, журналы активности.

Форматирование списков без ручной конкатенации

Legacy-подход:

function formatList(items) {
  return items.slice(0, -1).join(", ") + " and " + items.at(-1);
}

Такой код не учитывает языковые различия.

Intl.ListFormat:

const lf = new Intl.ListFormat('en', {
  style: 'long',
  type: 'conjunction'
});

lf.format(['apples', 'bananas', 'oranges']);

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

Стратегии миграции legacy-систем

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

Изоляция форматирования

Форматирование выносится в отдельный слой:

  • formatters/date.js
  • formatters/number.js
  • formatters/text.js

Это снижает связность и упрощает замену реализации.

Функциональные фасады

Старые функции сохраняются как оболочки:

function formatCurrencyLegacy(value) {
  return getNumberFormatter('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(value);
}

Постепенно внутренняя реализация полностью заменяется Intl.

Feature detection

Некоторые окружения могут не поддерживать полный набор API:

if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
  // использование Intl
} else {
  // fallback на legacy-реализацию
}

Согласование локалей

В legacy-системах локаль часто определяется хаотично: часть через backend, часть через frontend.

Intl требует централизованного определения:

  • HTTP-заголовок Accept-Language
  • пользовательские настройки
  • региональные параметры аккаунта

Производственные особенности использования Intl

Хотя Intl API встроен в движки JavaScript, существуют нюансы:

  • создание форматтера относительно дорого, поэтому требуется кэширование;
  • ICU data может отличаться в Node.js сборках;
  • поведение зависит от версии браузера или runtime;
  • некоторые локали могут отсутствовать в минимальных сборках Node.

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

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

Интеграция Intl в серверные системы

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

  • форматирование дат в логах;
  • генерация локализованных email-уведомлений;
  • формирование отчетов;
  • агрегация числовых данных.

При этом важно избегать зависимости от локали окружения сервера:

process.env.LANG = "en-US";

или явная передача локали как параметра.

Эволюция архитектуры после внедрения Intl

После замены legacy-решений на Intl API структура системы обычно становится более предсказуемой:

  • исчезает дублирование форматирования;
  • уменьшается количество локализационных багов;
  • упрощается поддержка новых языков;
  • бизнес-логика отделяется от представления данных;
  • уменьшается зависимость от внешних библиотек.

Переход на стандартизированный API приводит к унификации поведения между браузером и сервером, снижая количество edge-case сценариев, связанных с форматированием данных в разных окружениях.