Конфликты локалей

В экосистеме FormatJS локаль представляет собой не просто строковый идентификатор вида en или ru-RU, а набор правил форматирования, включающий:

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

Каждая локаль в контексте Intl и FormatJS опирается на CLDR-данные (Common Locale Data Repository), где даже близкие языки могут иметь несовместимые поведенческие различия. Это становится источником конфликтов при объединении или переключении локалей в одном приложении.


Иерархия локалей и точка возникновения конфликтов

Стандартная модель локалей основана на иерархии:

  • en
  • en-GB
  • en-US
  • en-AU

Аналогично для других языков:

  • ru
  • ru-RU
  • ru-BY

Конфликты возникают, когда система одновременно использует несколько уровней этой иерархии, особенно при:

  • частичной загрузке переводов;
  • смешивании fallback-цепочек;
  • объединении сообщений из разных источников.

FormatJS не выполняет автоматическое «слияние смысла» между локалями — каждая запись рассматривается как независимая сущность.


Перекрытие сообщений в словарях

Наиболее распространённый тип конфликта связан с одинаковыми ключами сообщений в разных локалях.

Пример структуры:

const messages_en = {
  title: "Settings",
  save: "Save"
};

const messages_ru = {
  title: "Настройки",
  save: "Сохранить"
};

Проблема возникает при неправильной сборке бандла, когда:

  • словари объединяются в один объект;
  • происходит поверхностное слияние (Object.assign);
  • используется общий namespace без разделения локалей.

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


Конфликты fallback-цепочек

FormatJS использует цепочку fallback-локалей, если сообщение отсутствует в текущем наборе.

Типичная логика:

ru-RU → ru → en

Конфликт возникает при:

  • наличии частичных переводов в нескольких уровнях;
  • несовместимости структуры сообщений;
  • неодинаковом ICU-синтаксисе между fallback-локалями.

Например:

// ru-RU
"cart.items": "{count} товар"

// ru
"cart.items": "{count} товаров"

// en
"cart.items": "{count} items"

Если одна из локалей содержит ошибочный ICU-формат, fallback может привести к неожиданному рендеру или падению парсера intl-messageformat.


Несовместимость ICU-синтаксиса между локалями

FormatJS использует ICU MessageFormat, который строго парсится.

Конфликтные ситуации:

  • различие в именах параметров;
  • отсутствие обязательных веток plural;
  • различие в форматировании дат;
  • неправильное экранирование скобок.

Пример некорректного расхождения:

// en
"messages": "{count, plural, one {# item} other {# items}}"

// ru (ошибка структуры)
"messages": "{count, plural, one {# товар} few {# товара}}"

Для русского языка требуется полный набор категорий one/few/many/other, и отсутствие одной ветки приводит к логической ошибке форматирования.


Конфликты при загрузке локалей в IntlProvider

В React-интеграции FormatJS через IntlProvider локаль и сообщения передаются как единый контекст.

<IntlProvider locale="ru" messages={messages_ru}>
  <App />
</IntlProvider>

Проблемные сценарии:

  • асинхронная загрузка сообщений после первого рендера;
  • переключение локали без очистки старого message cache;
  • использование одного провайдера для разных доменов сообщений.

Особенно критичен случай, когда locale обновляется, но messages остаются от предыдущей локали — возникает несоответствие языка и контента.


SSR и гидратационные расхождения

При серверном рендеринге конфликт локалей проявляется как несоответствие HTML между сервером и клиентом.

Причины:

  • сервер использует ru-RU, клиент — en-US;
  • различие в настройках браузера и серверного определения локали;
  • разная версия словарей на сервере и в клиентском бандле.

Типичный эффект — предупреждения о hydration mismatch и перерисовка части интерфейса.


Конфликты числового и датового форматирования

FormatJS опирается на Intl.NumberFormat и Intl.DateTimeFormat, поведение которых зависит от локали.

Примеры различий:

  • 1,000.50 (en-US)
  • 1 000,50 (ru-RU)

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

  • число форматируется в одной локали, а сообщение — в другой;
  • используется кешированный formatter от предыдущей локали;
  • сервер и клиент применяют разные региональные настройки.

Слияние словарей и стратегические ошибки

При масштабировании приложений часто применяется объединение сообщений из разных модулей:

const messages = {
  ...commonMessages,
  ...pageMessages,
  ...widgetMessages
};

Конфликты возникают при:

  • одинаковых ключах в разных модулях;
  • различной структуре ICU-строк под один ключ;
  • переопределении базовых переводов модульными.

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


Конфликты кодировки локалей и нормализации тегов

Locale identifier может быть записан в разных формах:

  • ru-RU
  • ru-ru
  • RU-ru

Хотя стандарт требует BCP 47 нормализации, на практике источники локалей могут быть неоднородны. FormatJS и Intl выполняют частичную нормализацию, но:

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

Конфликты plural rules между близкими локалями

Даже внутри одного языка возможны различия:

  • sr vs sr-Latn
  • pt vs pt-BR
  • zh vs zh-Hans / zh-Hant

Plural rules и форматы чисел могут отличаться, несмотря на общий язык.

Ошибка возникает, когда:

  • сообщения пишутся под одну локаль, но используются в другой;
  • ICU-ветки не соответствуют правилам конкретной локали;
  • fallback приводит к некорректному выбору формы.

Кэширование форматтеров и устаревшие локали

FormatJS активно использует кэширование форматтеров (Intl.NumberFormat, Intl.DateTimeFormat).

Конфликтный сценарий:

  • локаль меняется во время жизни приложения;
  • старые форматтеры продолжают использоваться;
  • UI отображает смесь старых и новых правил форматирования.

Это особенно заметно при:

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

Расхождение между ключами локалей и API сообщений

В крупных системах сообщения часто приходят из:

  • CMS;
  • backend API;
  • статических JSON-файлов;
  • модулей фронтенда.

Конфликты возникают, когда:

  • разные источники используют один ключ с разным смыслом;
  • backend возвращает уже локализованную строку;
  • frontend пытается повторно применить ICU-форматирование.

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


Конфликты при динамическом импортировании локалей

При ленивой загрузке:

import(`./locales/${locale}.json`)

возможны ситуации:

  • race condition при быстром переключении локалей;
  • загрузка частично устаревшего словаря;
  • параллельное обновление состояния сообщений.

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