Fallback и цепочки локалей

Любой объект из семейства Intl работает с локалями — специальными идентификаторами языка и региональных настроек. Наиболее распространённый формат локали:

ru-RU
en-US
de-DE
fr-CA

Структура состоит из нескольких частей:

  • язык (ru, en, de);
  • регион (RU, US, DE);
  • дополнительные расширения и параметры.

Пример:

new Intl.DateTimeFormat("ru-RU")

В этом случае движок пытается найти точное соответствие локали ru-RU. Если поддержка отсутствует, запускается механизм fallback — последовательного поиска подходящей альтернативы.


Что такое fallback локалей

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

Пример:

new Intl.NumberFormat("fr-CA")

Если движок не поддерживает канадский французский (fr-CA), он может:

  1. попробовать обычный французский (fr);
  2. затем перейти к локали по умолчанию среды выполнения.

Fallback особенно важен:

  • в браузерах;
  • на мобильных устройствах;
  • в embedded-средах;
  • в Node.js со срезанными ICU-данными;
  • в международных приложениях.

Базовый алгоритм fallback

При поиске подходящей локали Intl использует цепочку деградации.

Например:

zh-Hant-TW

Возможная последовательность:

zh-Hant-TW
zh-Hant
zh
default locale

Каждый следующий шаг делает локаль менее специфичной.


Пример реального fallback

const formatter = new Intl.DateTimeFormat(
  ["ban", "id"]
)

console.log(formatter.resolvedOptions().locale)

Результат:

id

Разбор:

  • ban — балийский язык;
  • если он не поддерживается движком;
  • используется id — индонезийский.

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


Цепочки локалей

Intl API принимает:

  • одну локаль;
  • массив локалей.

Пример:

const locales = [
  "fr-CA",
  "fr-FR",
  "en-US"
]

const formatter =
  new Intl.NumberFormat(locales)

console.log(
  formatter.resolvedOptions().locale
)

Алгоритм:

  1. поиск fr-CA;
  2. затем fr-FR;
  3. затем en-US;
  4. затем системная локаль.

Это называется locale negotiation — согласование локали.


Метод supportedLocalesOf

Метод позволяет проверить, какие локали реально поддерживаются.

Пример:

const locales = [
  "ru-RU",
  "fr-CA",
  "ban",
  "xx-YY"
]

console.log(
  Intl.DateTimeFormat.supportedLocalesOf(
    locales
  )
)

Возможный результат:

["ru-RU", "fr-CA"]

Неподдерживаемые локали автоматически исключаются.


Проверка fallback через resolvedOptions

Метод resolvedOptions() показывает итоговую локаль после всех fallback-переходов.

Пример:

const formatter =
  new Intl.DateTimeFormat(
    ["ban", "id"]
  )

console.log(
  formatter.resolvedOptions()
)

Результат:

{
  locale: "id",
  calendar: "gregory",
  numberingSystem: "latn",
  timeZone: "UTC"
}

Это главный способ диагностики поведения Intl API.


Разница между locale fallback и language fallback

Важно различать:

  • деградацию региона;
  • деградацию языка.

Fallback региона

fr-CA → fr

Язык сохраняется, меняется только региональная специфика.


Fallback языка

ban → id

Происходит переход к другому языку из цепочки локалей.


Lookup matcher

Intl поддерживает два алгоритма поиска локали:

  • lookup;
  • best fit.

По умолчанию используется best fit.

Пример:

new Intl.DateTimeFormat(
  ["en-GB"],
  {
    localeMatcher: "lookup"
  }
)

Алгоритм lookup

lookup использует строгую последовательную деградацию.

Например:

zh-Hant-TW
↓
zh-Hant
↓
zh

Без эвристик и дополнительных сопоставлений.


Алгоритм best fit

best fit зависит от реализации движка.

Он может:

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

Например:

en-AU

может быть сопоставлен с:

en-GB

или:

en-US

в зависимости от платформы.


Использование localeMatcher

Пример сравнения:

const lookup =
  new Intl.DateTimeFormat(
    ["en-XX"],
    {
      localeMatcher: "lookup"
    }
  )

const bestFit =
  new Intl.DateTimeFormat(
    ["en-XX"],
    {
      localeMatcher: "best fit"
    }
  )

console.log(
  lookup.resolvedOptions().locale
)

console.log(
  bestFit.resolvedOptions().locale
)

Результаты могут различаться в разных средах.


Canonicalization локалей

Перед fallback локали нормализуются.

Пример:

console.log(
  Intl.getCanonicalLocales(
    ["EN-us"]
  )
)

Результат:

["en-US"]

Intl автоматически:

  • исправляет регистр;
  • нормализует формат;
  • устраняет устаревшие обозначения.

Устаревшие коды локалей

Некоторые языковые коды заменяются современными.

Пример:

console.log(
  Intl.getCanonicalLocales("iw")
)

Результат:

["he"]

Здесь:

  • iw — старый код иврита;
  • he — современный код.

Unicode extension и fallback

Локали могут содержать Unicode-расширения.

Пример:

ja-JP-u-ca-japanese

Здесь:

  • u — Unicode extension;
  • ca — calendar;
  • japanese — японский календарь.

Fallback расширений

Если расширение не поддерживается, Intl пытается сохранить основную локаль.

Пример:

const formatter =
  new Intl.DateTimeFormat(
    "ru-RU-u-ca-unknown"
  )

console.log(
  formatter.resolvedOptions()
)

Расширение будет проигнорировано, но ru-RU сохранится.


Приоритет options над Unicode extension

Опции конструктора имеют больший приоритет.

Пример:

const formatter =
  new Intl.DateTimeFormat(
    "ja-JP-u-ca-japanese",
    {
      calendar: "gregory"
    }
  )

console.log(
  formatter.resolvedOptions().calendar
)

Результат:

gregory

Несмотря на расширение локали.


Поведение при полностью неизвестной локали

Пример:

const formatter =
  new Intl.NumberFormat(
    "xxx-YYY"
  )

console.log(
  formatter.resolvedOptions().locale
)

Обычно движок использует:

  • системную локаль;
  • либо дефолт ICU.

Например:

en-US

Использование navigator.languages

В браузере можно получить предпочтительные локали пользователя.

Пример:

console.log(
  navigator.languages
)

Возможный результат:

[
  "fr-CA",
  "fr",
  "en-US",
  "en"
]

Такой массив идеально подходит для Intl fallback.


Типичная схема локализации приложения

Пример:

const userLocales =
  navigator.languages

const formatter =
  new Intl.DateTimeFormat(
    [
      ...userLocales,
      "en-US"
    ]
  )

Здесь:

  1. используются предпочтения пользователя;
  2. затем резервный английский.

Fallback в разных объектах Intl

Механизм одинаков для большинства API:

  • Intl.DateTimeFormat;
  • Intl.NumberFormat;
  • Intl.Collator;
  • Intl.RelativeTimeFormat;
  • Intl.ListFormat;
  • Intl.DisplayNames;
  • Intl.PluralRules.

Пример с Intl.RelativeTimeFormat

const rtf =
  new Intl.RelativeTimeFormat(
    ["xx", "ru"]
  )

console.log(
  rtf.format(-1, "day")
)

Результат:

1 день назад

Пример с Intl.ListFormat

const formatter =
  new Intl.ListFormat(
    ["unknown", "en"],
    {
      style: "long",
      type: "conjunction"
    }
  )

console.log(
  formatter.format([
    "A",
    "B",
    "C"
  ])
)

Результат:

A, B, and C

Node.js и ICU

В Node.js fallback зависит от ICU-данных.

Некоторые сборки содержат:

  • только английскую локаль;
  • сокращённый набор языков.

Проверка:

console.log(
  Intl.DateTimeFormat.supportedLocalesOf([
    "ru",
    "fr",
    "de",
    "ja"
  ])
)

Полный ICU в Node.js

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

node --icu-data-dir=...

или сборки Node.js с full-icu.

Без полного ICU fallback может слишком быстро переходить к английскому.


Ошибки при проектировании fallback

Отсутствие резервной локали

Плохо:

new Intl.NumberFormat(userLocale)

Лучше:

new Intl.NumberFormat([
  userLocale,
  "en-US"
])

Использование несуществующих локалей

Плохо:

"ru_RU"

Правильно:

"ru-RU"

Intl использует BCP 47.


Игнорирование resolvedOptions

Без проверки невозможно понять:

  • какая локаль реально выбрана;
  • какие параметры применились;
  • какие расширения были проигнорированы.

BCP 47 и fallback

Intl API основан на стандарте BCP 47.

Локаль может включать:

language-script-region-variant

Пример:

sr-Cyrl-RS

Где:

  • sr — сербский;
  • Cyrl — кириллица;
  • RS — Сербия.

Fallback для script subtags

Если конкретный script не поддерживается:

sr-Cyrl-RS
↓
sr-Cyrl
↓
sr

Script играет важную роль для:

  • китайского;
  • сербского;
  • узбекского;
  • азербайджанского.

Китайские локали и fallback

Пример:

zh-Hans-CN
  • Hans — упрощённое письмо;
  • CN — Китай.

Fallback:

zh-Hans-CN
↓
zh-Hans
↓
zh

Для традиционного письма:

zh-Hant-TW

Практика построения цепочек

Типичная цепочка:

const locales = [
  "fr-CA",
  "fr",
  "en-US"
]

Логика:

  1. канадский французский;
  2. любой французский;
  3. английский как универсальный fallback.

Корпоративные стратегии fallback

Крупные приложения часто используют:

user locale
↓
language locale
↓
regional corporate locale
↓
English
↓
system locale

Это обеспечивает:

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

Intl.Locale

Объект Intl.Locale помогает анализировать локали.

Пример:

const locale =
  new Intl.Locale(
    "sr-Cyrl-RS"
  )

console.log(locale.language)
console.log(locale.script)
console.log(locale.region)

Результат:

sr
Cyrl
RS

Модификация локали

const locale =
  new Intl.Locale(
    "en-US"
  )

const british =
  locale.maximize()

console.log(british.toString())

Методы:

  • maximize();
  • minimize().

Они работают через ICU-данные и помогают при locale negotiation.


maximize() и fallback

const locale =
  new Intl.Locale("en")

console.log(
  locale.maximize().toString()
)

Возможный результат:

en-Latn-US

Метод добавляет недостающие части локали.


minimize() и сокращение локали

const locale =
  new Intl.Locale(
    "en-Latn-US"
  )

console.log(
  locale.minimize().toString()
)

Результат:

en

Это полезно для построения компактных fallback-цепочек.


Локализация и пользовательские настройки

Частая схема:

const locales = [
  user.selectedLocale,
  ...navigator.languages,
  "en-US"
]

Такой подход:

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

Intl API не переводит текст

Fallback касается только:

  • форматирования;
  • правил сортировки;
  • дат;
  • чисел;
  • списков;
  • plural rules.

Перевод интерфейса должен реализовываться отдельно:

  • i18next;
  • FormatJS;
  • Lingui;
  • собственные словари.

Связь fallback и ICU

Intl API в большинстве движков основан на ICU.

ICU отвечает за:

  • locale matching;
  • fallback;
  • pluralization;
  • calendars;
  • numbering systems.

Поведение разных платформ может немного отличаться именно из-за версии ICU.


Особенности браузерной совместимости

Разные браузеры могут:

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

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

  • в старых Safari;
  • в Android WebView;
  • в embedded Chromium.

Диагностика поддержки локалей

Практический способ:

function supports(locale) {
  return Intl.DateTimeFormat
    .supportedLocalesOf(locale)
    .length > 0
}

console.log(supports("ru"))
console.log(supports("ban"))

Рекомендации по проектированию fallback

Использовать массив локалей

["fr-CA", "fr", "en"]

Всегда задавать резервный язык

Чаще всего:

en-US

Проверять итоговую локаль

resolvedOptions().locale

Избегать жёстких предположений

Нельзя гарантировать:

  • одинаковый fallback;
  • одинаковый best fit;
  • одинаковую ICU-базу.

Использовать canonical locales

Intl.getCanonicalLocales()

Тестировать Node.js отдельно

Из-за ICU-сборок поведение может отличаться от браузеров.