Отладка проблем с локалями

Международные API в JavaScript опираются на стандарты BCP 47, ICU и данные CLDR, из-за чего поведение форматирования зависит не только от кода приложения, но и от окружения выполнения. Различия между браузерами, версиями Node.js, операционными системами и установленными языковыми пакетами часто становятся причиной несоответствий в форматировании дат, чисел и текста.

Основные объекты Intl API: Intl.DateTimeFormat, Intl.NumberFormat, Intl.Collator, Intl.RelativeTimeFormat, Intl.PluralRules, Intl.ListFormat — используют механизм выбора локали и её «деградации» (locale fallback), который является ключевой зоной возникновения проблем.


Механизм выбора локали и цепочка fallback

Выбор локали происходит через алгоритм разрешения BCP 47 тегов. Например, строка ru-KZ, ru-RU, ru рассматриваются как отдельные кандидаты с постепенным упрощением.

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

const fmt = new Intl.DateTimeFormat("ru-KZ");

движок:

  • проверяет полную локаль ru-KZ
  • при отсутствии поддержки переходит к ru
  • затем к локали по умолчанию окружения

Результат можно исследовать через:

fmt.resolvedOptions();

Ключевые поля:

  • locale — фактически выбранная локаль
  • calendar — используемый календарь
  • numberingSystem — система счисления
  • timeZone — временная зона (для DateTimeFormat)

Расхождения между ожидаемой и фактической локалью часто возникают из-за того, что движок «упрощает» запрос до более общего варианта.


Диагностика через resolvedOptions

resolvedOptions() является основным инструментом диагностики поведения Intl.

const nf = new Intl.NumberFormat("kk-KZ");
nf.resolvedOptions();

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

  • локаль заменена на соседнюю (например, ru-KZru-RU)
  • изменена система счисления на латинскую вместо арабской
  • временная зона отличается от ожидаемой
  • календарь переключён на григорианский независимо от запроса

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


Проблемы несовпадения локалей между средами

Различия между средами выполнения являются одной из основных причин неконсистентного форматирования.

Браузеры

  • используют ICU, встроенный в движок (V8, SpiderMonkey, JavaScriptCore)
  • обновляются вместе с браузером
  • поддерживают расширенные локали

Node.js

  • зависит от сборки ICU

  • возможны режимы:

    • full ICU
    • small ICU
    • system ICU

В режиме small ICU часть локалей отсутствует, что приводит к неожиданным fallback-цепочкам.

Пример:

Intl.NumberFormat("zh-Hant-HK")

в одной среде может давать традиционные китайские настройки, в другой — упрощённый китайский.


Влияние системных языковых пакетов

Node.js может использовать системный ICU, где набор локалей ограничен установленными пакетами ОС.

В Linux-средах часто встречается ситуация:

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

Результат — автоматический переход к en-US.


Проверка доступных локалей

Метод Intl.supportedLocalesOf используется для диагностики доступности локалей:

Intl.supportedLocalesOf(["ru-KZ", "kk-KZ", "fr-FR"]);

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

Пустой результат указывает на отсутствие поддержки, а не на ошибку синтаксиса.


Проблемы с временными зонами

Intl.DateTimeFormat чувствителен к таймзоне, которая может отличаться между:

  • сервером
  • клиентом
  • контейнером Docker
  • CI окружением
new Intl.DateTimeFormat("ru-RU", {
  timeZone: "Asia/Almaty"
});

Типичные проблемы:

  • отсутствие таймзоны в базе ICU
  • различие между IANA timezone database версиями
  • игнорирование параметра timeZone при fallback

Диагностика:

new Intl.DateTimeFormat().resolvedOptions().timeZone;

Коллизии BCP 47 и расширений

BCP 47 теги могут содержать расширения:

  • -u-nu (система счисления)
  • -u-ca (календарь)
  • -u-hc (часовой формат)

Пример:

new Intl.DateTimeFormat("ar-EG-u-nu-latn");

Проблемы возникают при:

  • игнорировании расширений движком
  • частичной поддержке Unicode extensions
  • нормализации тега до базовой локали

Результатом может стать потеря ожидаемой системы счисления или формата времени.


Несоответствие форматирования чисел

Intl.NumberFormat зависит от:

  • локали
  • numbering system
  • default options ICU
new Intl.NumberFormat("de-DE").format(1234567.89);

Ожидается: 1.234.567,89

Однако возможны отклонения:

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

Причина часто связана с fallback локали или ограниченной ICU сборкой.


Проблемы коллатора (сортировка строк)

Intl.Collator зависит от локали сильнее остальных API.

["ä", "a", "z"].sort(new Intl.Collator("de").compare);

Типовые ошибки:

  • сортировка как ASCII вместо локализованной
  • игнорирование диакритики
  • различие между usage: "sort" и usage: "search"

Диагностика:

const col = new Intl.Collator("de", { sensitivity: "base" });
col.resolvedOptions();

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

Intl объекты кэшируются внутри движка, но неправильное переиспользование приводит к ошибкам логики:

const fmt = new Intl.DateTimeFormat("ru-RU");

function format(date, locale) {
  return fmt.format(date); // локаль игнорируется
}

Проблема проявляется как:

  • игнорирование динамической локали
  • «залипание» на первой инициализации
  • неконсистентное поведение в многопоточных сервисах

Особенности сериализации и логирования

Intl объекты не сериализуются напрямую:

JSON.stringify(new Intl.NumberFormat("ru-RU"));

Результат:

{}

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

formatter.resolvedOptions();

Логирование без resolvedOptions часто скрывает реальную локаль, выбранную движком.


Различия между ICU версиями

ICU библиотека определяет:

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

Разные версии ICU приводят к:

  • изменению дефолтного форматирования дат
  • появлению или исчезновению локалей
  • изменению правил округления чисел

В Node.js это особенно заметно при переходе между версиями runtime.


Ошибки из-за некорректных BCP 47 тегов

Некорректные теги автоматически приводятся к валидным:

new Intl.DateTimeFormat("russian");

Результат:

  • fallback к en-US

Корректный формат:

  • ru
  • ru-RU

Диагностический признак — неожиданный locale в resolvedOptions.


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

При SSR часто возникает расхождение между сервером и клиентом:

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

Это приводит к:

  • hydration mismatch
  • различиям в датах
  • различиям в числах

Типичный источник:

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

без явного указания локали.


Поведение default locale

Если локаль не указана:

new Intl.NumberFormat();

используется:

Intl.DateTimeFormat().resolvedOptions().locale

или системная локаль окружения.

Проблемы возникают при:

  • изменении LANG в системе
  • запуске в Docker
  • CI средах с en-US по умолчанию

Методы диагностики проблем локалей

Комплексная диагностика включает:

  • проверку resolvedOptions
  • проверку supportedLocalesOf
  • сравнение ICU версий
  • тестирование в разных окружениях

Пример диагностического набора:

function debugLocale(locale) {
  const dt = new Intl.DateTimeFormat(locale);
  const nf = new Intl.NumberFormat(locale);
  const col = new Intl.Collator(locale);

  return {
    date: dt.resolvedOptions(),
    number: nf.resolvedOptions(),
    collator: col.resolvedOptions()
  };
}

Частые источники несоответствий

  • неполная ICU сборка
  • fallback локали до en
  • игнорирование BCP 47 расширений
  • различие сервер/клиент
  • различие версий Node.js
  • отсутствие таймзон в окружении
  • использование системной локали без фиксации

Поведение при неизвестных локалях

Если локаль не распознана:

  • происходит silent fallback
  • ошибки не выбрасываются
  • результат становится валидным, но неожиданным

Пример:

new Intl.NumberFormat("xx-YY");

Результат:

  • переход к дефолтной локали

Диагностика возможна только через resolvedOptions.


Наблюдение за изменением локали

Intl API не предоставляет событий изменения локали. Любые изменения окружения не отражаются автоматически.

Следовательно:

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