CI/CD для интернационализированных приложений

Интернационализация в JavaScript опирается на встроенный стандарт ECMAScript Internationalization API (ECMA-402), который реализует единый слой форматирования дат, чисел, списков и текста с учётом локали. При переходе к промышленным процессам разработки этот слой перестаёт быть исключительно прикладным и становится частью CI/CD-инфраструктуры: поведение Intl напрямую влияет на корректность интерфейсов, тестов и релизов.

Ключевая особенность заключается в том, что Intl — это не просто набор функций форматирования, а интерфейс к системным ICU-данным (International Components for Unicode), которые могут различаться между средами выполнения. Именно это делает интернационализацию частью цепочки доставки, а не только логики приложения.


Модель локалей и детерминированность поведения

Любая интеграция интернационализации в CI/CD начинается с понимания детерминированности локалей. В Intl локаль задаётся строкой BCP 47:

  • en-US
  • ru-RU
  • ar-EG
  • zh-Hans-CN

Поведение форматтеров зависит от:

  • версии ICU в Node.js или браузере
  • системных региональных настроек (в редких конфигурациях)
  • доступных коллационных правил
  • поддержки новых локалей

Это приводит к потенциальной проблеме: одинаковый код может давать разные строки на разных агентах сборки.

Для устранения вариативности в CI принято фиксировать локаль:

const formatter = new Intl.DateTimeFormat('ru-RU', {
  dateStyle: 'short',
  timeStyle: 'short'
});

или принудительно использовать нейтральные локали в тестах:

const nf = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });

Форматирование как контракт в тестировании

В CI/CD форматирование становится контрактом, который должен быть стабилен.

Числа и валюты

const price = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR'
}).format(1234.56);

Тесты фиксируют строковое представление:

expect(price).toBe('1.234,56 €');

Проблема возникает при смене ICU версии: даже пробелы и символы неразрывного пробела могут измениться.


Даты и время

const dt = new Intl.DateTimeFormat('en-GB', {
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric'
}).format(new Date('2024-05-01'));

В CI важно исключить флейки, связанные с временными зонами:

  • фиксация TZ=UTC
  • использование фиксированных дат
  • запрет new Date() без параметров в тестах

Snapshot-тестирование и его ограничения

Snapshot-тесты часто используются для UI, где Intl играет ключевую роль. Однако их стабильность зависит от:

  • версии ICU
  • окружения выполнения
  • локальных патчей Node.js

Пример проблемы:

// ожидание
"1,234.50"

// фактический результат в другой версии ICU
"1.234,50"

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

const normalize = (str) =>
  str.replace(/\u00A0/g, ' ').trim();

или тестируют не строку, а структуру:

const parts = new Intl.NumberFormat('en-US').formatToParts(1234.5);

formatToParts как инструмент стабильности

Метод formatToParts позволяет разложить форматированное значение на атомарные элементы:

[
  { type: 'integer', value: '1' },
  { type: 'group', value: ',' },
  { type: 'integer', value: '234' },
  { type: 'decimal', value: '.' },
  { type: 'fraction', value: '50' }
]

В CI это снижает зависимость от локализованных символов:

  • разделители групп
  • десятичные символы
  • пробелы

Тесты начинают проверять структуру, а не строку.


Проверка поддержки локалей в pipeline

Разные окружения могут поддерживать разный набор локалей. В Node.js это зависит от сборки ICU:

console.log(Intl.DateTimeFormat.supportedLocalesOf(['ru', 'fr', 'ja']));

В CI полезно вводить этап валидации:

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

Это предотвращает ситуацию, когда продакшн поддерживает ru-RU, а тестовый раннер — нет.


Псевдолокализация как этап CI

Псевдолокализация используется для выявления проблем интерфейса без перевода:

  • удлинение строк
  • замена символов
  • добавление диакритики

Пример трансформации:

Settings → Šęťťïñğš

Это помогает обнаружить:

  • обрезание текста
  • переполнение контейнеров
  • некорректные padding/margin
  • отсутствие поддержки RTL

В CI это может быть отдельный шаг сборки UI.


RTL и направление текста

Intl напрямую не управляет RTL, но локаль влияет на выбор направления:

const locale = 'ar-EG';

В CI проверяется:

  • наличие dir="rtl"
  • корректность CSS logical properties
  • отсутствие фиксированных left/right значений

Тесты часто используют headless браузер:

expect(document.documentElement.dir).toBe('rtl');

Collator и сортировка в данных

Intl.Collator определяет порядок сортировки строк:

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

В CI это критично для:

  • списков пользователей
  • таблиц поиска
  • автокомплита

Проблема: разные ICU дают разный порядок сортировки.

Поэтому тесты часто фиксируют:

const sorted = ['ä', 'a', 'z'].sort(collator.compare);

Extract/Translate pipeline и Intl-совместимость

В CI/CD интернационализация часто связана с этапом извлечения строк:

  • ключи локализации
  • ICU MessageFormat строки
  • JSON-файлы переводов

Пример:

{
  "cart.items": "{count, plural, one {# item} other {# items}}"
}

На этапе CI проверяется:

  • корректность ICU синтаксиса
  • совпадение плейсхолдеров
  • отсутствие лишних аргументов

Валидация ICU MessageFormat

Ошибки в ICU-строках часто проявляются только в рантайме. Поэтому добавляется линтинг:

i18n-lint translations/

Проверки включают:

  • синтаксис plural rules
  • наличие обязательных вариантов one, other
  • отсутствие неиспользуемых аргументов

Версионирование ICU как фактор релиза

Node.js и браузеры используют разные версии ICU. Это влияет на:

  • формат дат
  • правила сортировки
  • поддерживаемые локали

В CI/CD это учитывается через:

  • фиксированные Docker-образы
  • pinning версии Node.js
  • проверку process.versions.icu
console.log(process.versions.icu);

Feature flags и интернационализация

Некоторые локализационные функции зависят от фич-флагов:

  • расширенные правила plural
  • новые локали
  • улучшенные форматы дат

CI пайплайн часто разделяет:

  • стабильные локали (production-ready)
  • экспериментальные локали (canary)

Тестирование edge cases в Intl

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

  • очень большие числа
  • отрицательные значения
  • нулевые значения
  • даты до 1970 года
  • высокоточные таймстампы
new Intl.NumberFormat('en-US').format(Number.MAX_SAFE_INTEGER);

Кэширование и производительность форматтеров

Создание Intl объектов дорогостоящее. В CI/CD проверяется:

  • отсутствие повторного создания форматтеров в циклах
  • использование singleton-инстансов
  • корректность кеширования
const nf = new Intl.NumberFormat('en-US');

Линтеры могут запрещать:

array.map(x => new Intl.NumberFormat().format(x));

Runtime vs Build-time интернационализация

Два подхода влияют на архитектуру пайплайна:

Runtime:

  • форматирование происходит в браузере/Node.js
  • зависит от ICU окружения

Build-time:

  • строки генерируются на этапе сборки
  • результат фиксируется в бандле

CI/CD pipeline должен явно разделять эти режимы, иначе возможны расхождения между staging и production.


Контроль регрессий локализации

Любое изменение UI может сломать локализацию:

  • обрезание строк
  • смена порядка параметров
  • потеря plural форм

Для этого применяются:

  • визуальные regression tests (Playwright, Cypress)
  • snapshot локализованных экранов
  • матрицы локалей

Матрица локалей в CI

Типичный подход:

  • минимум: en, ru
  • расширенный набор: en, ru, de, fr, ja, ar

Каждый билд прогоняется по матрице:

  • форматирование чисел
  • даты
  • UI текст

Это увеличивает стоимость CI, но снижает риск релизных дефектов.


Интеграция Intl в монорепозитории

В монорепозиториях интернационализация часто централизуется:

  • shared intl-utils пакет
  • единый слой форматтеров
  • единые тесты локалей

CI проверяет:

  • отсутствие дублирования переводов
  • согласованность ключей между пакетами
  • совместимость версий ICU между сервисами

Логирование и локаль в продакшене

CI/CD должен учитывать, что форматирование логов тоже может зависеть от локали:

console.log(new Intl.DateTimeFormat().format(new Date()));

В продакшене это может усложнять:

  • агрегацию логов
  • поиск ошибок
  • парсинг метрик

Поэтому часто применяется правило: логирование — только в en-US или ISO-форматах, а Intl используется только для UI-слоя.