Обработка отсутствующих локалей

При работе с Intl API часто возникает ситуация, когда запрошенная локаль не поддерживается текущей средой выполнения. Это особенно заметно:

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

Например:

new Intl.DateTimeFormat("fr-CA")

Если окружение не поддерживает fr-CA, движок пытается подобрать наиболее подходящий вариант автоматически.


Механизм fallback в Intl API

Большинство конструкторов Intl используют механизм автоматического отката локали (locale fallback).

Пример:

const formatter = new Intl.NumberFormat("de-AT")

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

Если de-AT поддерживается:

de-AT

Если нет, возможны варианты:

de

или:

en-US

В зависимости от доступных данных.


Иерархия поиска локали

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

Для:

zh-Hant-TW

движок проверяет:

  1. zh-Hant-TW
  2. zh-Hant
  3. zh
  4. локаль по умолчанию среды

Аналогично:

pt-BR

может откатиться к:

pt

Проверка поддерживаемых локалей

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

Проверка одной локали

console.log(
  Intl.DateTimeFormat.supportedLocalesOf(["fr-CA"])
)

Результат:

["fr-CA"]

или:

[]

Проверка нескольких локалей

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

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

Результат:

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

Несуществующая локаль xx-YY будет отброшена.


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

Все основные конструкторы Intl принимают массив локалей в порядке приоритета.

Пример:

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

Движок выбирает первую доступную локаль.

Такой подход особенно полезен:

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

Практический fallback

const locales = [
  "kk-KZ",
  "ru-RU",
  "en-US"
]

const formatter = new Intl.NumberFormat(locales)

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

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

kk-KZ

или:

ru-RU

или:

en-US

resolvedOptions()

Метод resolvedOptions() показывает фактически используемую локаль.

const formatter = new Intl.DateTimeFormat(
  ["fr-CA", "fr", "en"]
)

console.log(
  formatter.resolvedOptions()
)

Результат:

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

Это особенно важно для:

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

localeMatcher

Опция localeMatcher определяет алгоритм поиска подходящей локали.

Поддерживаются два значения:

  • "lookup"
  • "best fit"

lookup

Строгий пошаговый поиск.

const formatter = new Intl.NumberFormat(
  ["en-GB"],
  {
    localeMatcher: "lookup"
  }
)

Используется RFC-совместимый механизм поиска.


best fit

Более гибкий алгоритм.

const formatter = new Intl.NumberFormat(
  ["en-GB"],
  {
    localeMatcher: "best fit"
  }
)

Движок может использовать внутренние эвристики.

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


Разница между lookup и best fit

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

Может вернуть:

en

А best fit иногда подбирает локаль точнее в зависимости от платформы.

Поведение может отличаться между:

  • V8;
  • SpiderMonkey;
  • JavaScriptCore.

Локаль по умолчанию

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

const formatter = new Intl.DateTimeFormat(
  ["xx-YY"]
)

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

Например:

en-US

Получение локали среды

console.log(
  Intl.DateTimeFormat().resolvedOptions().locale
)

Пример результата:

ru-RU

Обработка ошибок локалей

Некорректная строка локали вызывает RangeError.

new Intl.NumberFormat("invalid_locale")

Ошибка:

RangeError

Проверка локали перед использованием

Через try/catch

function isValidLocale(locale) {
  try {
    new Intl.NumberFormat(locale)

    return true
  } catch {
    return false
  }
}

console.log(
  isValidLocale("ru-RU")
)

Через Intl.Locale

function isValidLocale(locale) {
  try {
    new Intl.Locale(locale)

    return true
  } catch {
    return false
  }
}

Intl.Locale

Класс Intl.Locale помогает анализировать и нормализовать локали.

const locale = new Intl.Locale("fr-ca")

console.log(locale.toString())

Результат:

fr-CA

Нормализация локалей

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

console.log(locale.baseName)

Результат:

en-US

Работа с региональными вариантами

Некоторые локали имеют несколько региональных модификаций.

Примеры:

en-US
en-GB
en-AU

Форматирование может отличаться:

const us = new Intl.DateTimeFormat("en-US")
const gb = new Intl.DateTimeFormat("en-GB")

const date = new Date()

console.log(us.format(date))
console.log(gb.format(date))

Частичный fallback

Если отсутствует региональная версия:

es-MX

движок может использовать:

es

Это позволяет сохранять язык интерфейса даже без точной региональной локали.


Unsupported locale и Unicode extension

Локаль может существовать, но отдельные расширения — нет.

Пример:

new Intl.DateTimeFormat(
  "en-US-u-ca-islamic"
)

Если календарь islamic не поддерживается, движок выберет доступный.

Проверка:

const formatter = new Intl.DateTimeFormat(
  "en-US-u-ca-islamic"
)

console.log(
  formatter.resolvedOptions()
)

Проверка поддерживаемых календарей

Intl.supportedValuesOf("calendar")

Пример результата:

[
  "gregory",
  "buddhist",
  "islamic"
]

Проверка систем нумерации

Intl.supportedValuesOf("numberingSystem")

Например:

[
  "latn",
  "arab",
  "thai"
]

Проверка часовых поясов

Intl.supportedValuesOf("timeZone")

Graceful degradation

Корректная деградация — важная часть интернационализации.

Пример безопасной конфигурации:

const formatter = new Intl.DateTimeFormat(
  [
    "kk-KZ",
    "ru-RU",
    "en-US"
  ],
  {
    dateStyle: "long"
  }
)

Даже если казахская локаль недоступна, приложение продолжит работать.


Ручной fallback

Иногда требуется собственная логика выбора локалей.

const preferred = [
  "kk-KZ",
  "ru-RU",
  "en-US"
]

const supported =
  Intl.DateTimeFormat.supportedLocalesOf(
    preferred
  )

const locale =
  supported[0] || "en-US"

const formatter =
  new Intl.DateTimeFormat(locale)

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

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

const locale =
  localStorage.getItem("locale")

Далее:

const available = [
  "ru-RU",
  "en-US",
  "de-DE"
]

const supported =
  Intl.DateTimeFormat.supportedLocalesOf([
    locale
  ])

const finalLocale =
  supported.length
    ? supported[0]
    : "en-US"

Accept-Language

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

console.log(navigator.languages)

Пример:

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

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

const formatter =
  new Intl.NumberFormat(
    navigator.languages
  )

Серверный fallback

В Node.js часто используется заголовок:

Accept-Language

Пример:

fr-CA,fr;q=0.9,en;q=0.8

После парсинга:

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

Этот массив можно передать напрямую в Intl.


Особенности Node.js

Node.js может поставляться:

  • с полной ICU-базой;
  • с минимальной ICU-конфигурацией.

Минимальная сборка поддерживает ограниченное число локалей.

Проверка:

console.log(
  Intl.DateTimeFormat.supportedLocalesOf([
    "ru-RU",
    "zh-CN",
    "ar-EG"
  ])
)

Full ICU в Node.js

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

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

или специальные сборки Node.js с Full ICU.


Различия браузеров

Поддержка локалей зависит от:

  • версии браузера;
  • платформы;
  • ICU-данных;
  • ОС пользователя.

Например:

  • Chrome и Edge используют ICU;
  • Firefox использует собственную реализацию;
  • Safari может поддерживать меньше локалей.

Intl API никогда не возвращает ошибку из-за отсутствующей локали

Это важная особенность API.

Ошибка возникает только:

  • при синтаксически некорректной локали;
  • при невалидных параметрах.

Но отсутствие поддержки локали приводит к fallback, а не к исключению.


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

const formatter =
  new Intl.NumberFormat("abc-XYZ")

Если строка синтаксически допустима, но локаль неизвестна:

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

Возможен fallback:

en-US

Canonical locale identifiers

Intl.getCanonicalLocales() нормализует локали.

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

Результат:

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

Удаление дубликатов

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

Результат:

[
  "en-US"
]

Стратегия устойчивой интернационализации

Надёжная схема работы с локалями обычно включает:

  1. Получение пользовательских предпочтений.
  2. Нормализацию локалей.
  3. Проверку поддержки.
  4. Выбор fallback-цепочки.
  5. Использование безопасной локали по умолчанию.
  6. Анализ resolvedOptions().

Пример полноценной системы fallback

function resolveLocale(locales) {
  const canonical =
    Intl.getCanonicalLocales(locales)

  const supported =
    Intl.DateTimeFormat
      .supportedLocalesOf(canonical)

  return supported[0] || "en-US"
}

const locale = resolveLocale([
  "kk-KZ",
  "ru-RU",
  "en-US"
])

const formatter =
  new Intl.DateTimeFormat(locale, {
    dateStyle: "full"
  })

console.log(
  formatter.format(new Date())
)