Как настраивать локали с помощью расширений Unicode

Добавляйте системы календарей, форматы чисел и предпочтения отображения времени в идентификаторы локалей

Введение

Идентификатор локали, такой как en-US, сообщает JavaScript, какой язык и регион использовать для форматирования. Однако он не указывает, какую календарную систему применять, какой формат чисел отображать или показывать ли время в 12- или 24-часовом формате. Эти параметры зависят от выбора пользователя, а не только от местоположения.

Расширения Unicode решают эту проблему. Они позволяют добавлять параметры форматирования прямо в идентификатор локали. Вместо того чтобы указывать отдельные настройки для каждого форматтера, вы один раз задаёте предпочтения в строке локали.

В этом руководстве объясняется, как работают расширения Unicode, какие типы расширений доступны и когда их использовать в вашем коде для интернационализации.

Что такое расширения Unicode

Расширения Unicode — это дополнительные теги, которые вы добавляете к идентификаторам локалей для указания параметров форматирования. Они следуют стандартному формату, определённому в BCP 47 — той же спецификации, что описывает идентификаторы локалей.

Расширение начинается с -u-, за которым следуют пары ключ-значение. u обозначает Unicode. Каждый ключ состоит из двух букв, а значения зависят от типа ключа.

const locale = "en-US-u-ca-gregory-hc-h12";

Этот идентификатор локали указывает американский английский с григорианским календарём и 12-часовым форматом времени.

Как добавить расширения к строкам локалей

Расширения размещаются в конце идентификатора локали, после компонентов языка, письма и региона. Маркер -u- отделяет основную часть идентификатора от расширений.

Базовая структура выглядит так:

language-region-u-key-value-key-value

Каждая пара ключ-значение задаёт одно форматирование. В одной строке локали можно указать несколько пар ключ-значение.

const japanese = new Intl.Locale("ja-JP-u-ca-japanese-nu-jpan");
console.log(japanese.calendar); // "japanese"
console.log(japanese.numberingSystem); // "jpan"

Порядок пар ключ-значение не имеет значения. И "en-u-ca-gregory-nu-latn", и "en-u-nu-latn-ca-gregory" — это корректные и равнозначные варианты.

Расширения календаря

Ключ ca определяет, какая календарная система будет использоваться для форматирования дат. В разных культурах применяются разные календари, и некоторые пользователи предпочитают не григорианские календари по религиозным или культурным причинам.

Наиболее распространённые значения календарей:

  • gregory — григорианский календарь
  • buddhist — буддийский календарь
  • islamic — исламский календарь
  • hebrew — еврейский календарь
  • chinese — китайский календарь
  • japanese — японский императорский календарь
const islamicLocale = new Intl.Locale("ar-SA-u-ca-islamic");
const date = new Date("2025-03-15");
const formatter = new Intl.DateTimeFormat(islamicLocale, {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.format(date));
// Output: "٢٠ رمضان ١٤٤٦ هـ"

Это форматирует дату по исламскому календарю. Та же григорианская дата будет другим годом, месяцем и днём в исламской системе.

Буддийский календарь часто используется в Таиланде. Он отсчитывает годы с рождения Будды в 543 году до н.э., поэтому буддийский год на 543 года больше григорианского.

const buddhistLocale = new Intl.Locale("th-TH-u-ca-buddhist");
const formatter = new Intl.DateTimeFormat(buddhistLocale, {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.format(new Date("2025-03-15")));
// Output: "15 มีนาคม 2568"

2025 год по григорианскому календарю — это 2568 год по буддийскому календарю.

Расширения системы счисления

Ключ nu определяет, какая система счисления будет использоваться для отображения чисел. Хотя в большинстве локалей применяются западные арабские цифры (0-9), во многих регионах есть свои традиционные системы.

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

  • latn — западные арабские цифры (0-9)
  • arab — арабско-индийские цифры
  • hanidec — китайские десятичные цифры
  • deva — деванагари
  • thai — тайские цифры
const arabicLocale = new Intl.Locale("ar-EG-u-nu-arab");
const number = 123456;
const formatter = new Intl.NumberFormat(arabicLocale);

console.log(formatter.format(number));
// Output: "١٢٣٬٤٥٦"

Арабско-индийские цифры выглядят иначе, чем западные, но обозначают те же значения. Число 123456 записывается как ١٢٣٬٤٥٦.

Тайские цифры — ещё один пример:

const thaiLocale = new Intl.Locale("th-TH-u-nu-thai");
const formatter = new Intl.NumberFormat(thaiLocale);

console.log(formatter.format(123456));
// Output: "๑๒๓,๔๕๖"

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

Расширения часового формата

Ключ hc определяет, как отображается время. В некоторых регионах предпочитают 12-часовой формат с AM и PM, в других — 24-часовой. Часовой формат также влияет на то, как отображается полночь.

Доступны четыре варианта часового формата:

  • h12 использует часы 1–12, полночь — 12:00 AM
  • h11 использует часы 0–11, полночь — 0:00 AM
  • h23 использует часы 0–23, полночь — 0:00
  • h24 использует часы 1–24, полночь — 24:00

Значения h12 и h11 обозначают 12-часовой формат, а h23 и h24 — 24-часовой. Разница в том, начинается ли отсчёт с 0 или с 1.

const us12Hour = new Intl.Locale("en-US-u-hc-h12");
const japan11Hour = new Intl.Locale("ja-JP-u-hc-h11");
const europe23Hour = new Intl.Locale("en-GB-u-hc-h23");

const date = new Date("2025-03-15T00:30:00");

console.log(new Intl.DateTimeFormat(us12Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Output: "12:30 AM"

console.log(new Intl.DateTimeFormat(japan11Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Output: "0:30 AM"

console.log(new Intl.DateTimeFormat(europe23Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Output: "00:30"

В формате h12 полночь отображается как 12:30 AM, а в h11 — как 0:30 AM. В h23 это будет 00:30 без AM или PM.

В большинстве приложений используется либо h12, либо h23. Формат h11 в основном применяется в Японии, а h24 почти не используется.

Расширения сортировки

Ключ co определяет правила сортировки строк. Сортировка определяет порядок символов при упорядочивании текста. В разных языках и регионах свои правила сортировки.

К распространённым значениям сортировки относятся:

  • standard — стандартная сортировка Unicode
  • phonebk — телефонная сортировка (немецкий)
  • pinyin — сортировка по пиньиню (китайский)
  • stroke — сортировка по количеству черт (китайский)

Телефонная сортировка по-немецки обрабатывает умлауты иначе, чем стандартная сортировка. В телефонном порядке ä преобразуется в ae, ö — в oe, а ü — в ue для целей сортировки.

const names = ["Müller", "Meyer", "Möller", "Mueller"];

const standard = new Intl.Collator("de-DE");
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");

console.log(names.sort((a, b) => standard.compare(a, b)));
// Output: ["Meyer", "Möller", "Mueller", "Müller"]

console.log(names.sort((a, b) => phonebook.compare(a, b)));
// Output: ["Meyer", "Möller", "Mueller", "Müller"]

В китайском языке есть несколько систем сортировки. Сортировка по пиньиню упорядочивает по произношению, а сортировка по количеству черт — по числу штрихов в иероглифе.

const pinyinCollator = new Intl.Collator("zh-CN-u-co-pinyin");
const strokeCollator = new Intl.Collator("zh-CN-u-co-stroke");

Расширения сортировки влияют только на API Intl.Collator и методы вроде Array.prototype.sort(), если они используются с collators.

Расширения сортировки по регистру

Ключ kf определяет, какие буквы — заглавные или строчные — будут идти первыми при сортировке. Это зависит от языка и задачи.

Доступно три значения:

  • upper — сначала заглавные буквы
  • lower — сначала строчные буквы
  • false — использовать сортировку по умолчанию для выбранного языка
const words = ["apple", "Apple", "APPLE", "banana"];

const upperFirst = new Intl.Collator("en-US-u-kf-upper");
const lowerFirst = new Intl.Collator("en-US-u-kf-lower");

console.log(words.sort((a, b) => upperFirst.compare(a, b)));
// Output: ["APPLE", "Apple", "apple", "banana"]

console.log(words.sort((a, b) => lowerFirst.compare(a, b)));
// Output: ["apple", "Apple", "APPLE", "banana"]

Сортировка по регистру влияет на порядок, если слова отличаются только регистром. Это определяет вторичный порядок после сравнения основных символов.

Расширения числовой сортировки

Ключ kn включает числовую сортировку, при которой числовые последовательности сортируются по их числовому значению, а не по лексикографическому порядку. Без числовой сортировки "10" будет идти перед "2", потому что "1" стоит раньше "2" в порядке символов.

Числовая сортировка поддерживает два значения:

  • true — включить числовую сортировку
  • false — отключить числовую сортировку (по умолчанию)
const items = ["item1", "item10", "item2", "item20"];

const standard = new Intl.Collator("en-US");
const numeric = new Intl.Collator("en-US-u-kn-true");

console.log(items.sort((a, b) => standard.compare(a, b)));
// Output: ["item1", "item10", "item2", "item20"]

console.log(items.sort((a, b) => numeric.compare(a, b)));
// Output: ["item1", "item2", "item10", "item20"]

Когда включена числовая сортировка, "item2" правильно располагается перед "item10", потому что 2 меньше 10. Это обеспечивает ожидаемый порядок сортировки строк с числами.

Числовая сортировка полезна для сортировки имён файлов, номеров версий, адресов и любого текста с числами внутри.

Использование объектов опций вместо строк расширений

Вместо кодирования расширений в строке локали, вы можете передавать их как опции в конструктор Intl.Locale. Такой подход отделяет базовую локаль от предпочтений форматирования.

const locale = new Intl.Locale("ja-JP", {
  calendar: "japanese",
  numberingSystem: "jpan",
  hourCycle: "h11"
});

console.log(locale.toString());
// Output: "ja-JP-u-ca-japanese-hc-h11-nu-jpan"

Конструктор автоматически преобразует опции в теги расширений. Оба подхода создают одинаковые объекты локалей.

Подход с объектом опций даёт несколько преимуществ. Он делает код более читаемым за счёт использования полных имён свойств вместо двухбуквенных кодов. Также так проще динамически формировать локали из конфигурационных данных.

const userPreferences = {
  language: "ar",
  region: "SA",
  calendar: "islamic",
  numberingSystem: "arab"
};

const locale = new Intl.Locale(`${userPreferences.language}-${userPreferences.region}`, {
  calendar: userPreferences.calendar,
  numberingSystem: userPreferences.numberingSystem
});

Вы также можете передавать опции напрямую в конструкторы форматтеров:

const formatter = new Intl.DateTimeFormat("th-TH", {
  calendar: "buddhist",
  numberingSystem: "thai",
  year: "numeric",
  month: "long",
  day: "numeric"
});

Это объединяет параметры форматирования для локали и параметры отображения в одном вызове конструктора.

Когда использовать расширения и когда опции форматтера

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

Используйте расширения в строке локали, если предпочтения форматирования являются неотъемлемой частью локали пользователя. Например, если тайский пользователь всегда хочет видеть буддийский календарь и тайские цифры, укажите эти предпочтения в идентификаторе локали.

const userLocale = "th-TH-u-ca-buddhist-nu-thai";

Это позволяет передавать локаль любому форматтеру без повторения настроек:

const dateFormatter = new Intl.DateTimeFormat(userLocale);
const numberFormatter = new Intl.NumberFormat(userLocale);

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

Используйте параметры форматирования, когда предпочтения форматирования специфичны для одного случая. Например, если вы хотите показывать исламский календарь в одной части приложения, а григорианский — в другой, передайте параметр calendar в нужный форматтер.

const islamicFormatter = new Intl.DateTimeFormat("ar-SA", {
  calendar: "islamic"
});

const gregorianFormatter = new Intl.DateTimeFormat("ar-SA", {
  calendar: "gregory"
});

Один и тот же идентификатор локали может давать разное форматирование в зависимости от параметра calendar.

Расширения в строке локали работают как значения по умолчанию. Параметры форматтера переопределяют эти значения, если они указаны. Это позволяет использовать пользовательские настройки как базу и при этом настраивать отдельные форматтеры.

const locale = "en-US-u-hc-h23";
const formatter12Hour = new Intl.DateTimeFormat(locale, {
  hourCycle: "h12"
});

Пользователь предпочитает 24-часовой формат времени, но этот конкретный форматтер переопределяет это и показывает 12-часовой формат.

Чтение значений расширений из локалей

Объект Intl.Locale предоставляет значения расширений как свойства. Вы можете читать эти свойства, чтобы проверить или узнать настройки форматирования локали.

const locale = new Intl.Locale("ar-SA-u-ca-islamic-nu-arab-hc-h12");

console.log(locale.calendar); // "islamic"
console.log(locale.numberingSystem); // "arab"
console.log(locale.hourCycle); // "h12"

Эти свойства возвращают значения расширений, если они есть, или undefined, если расширение не указано.

Эти свойства можно использовать для создания интерфейсов настройки или проверки пользовательских предпочтений:

function describeLocalePreferences(localeString) {
  const locale = new Intl.Locale(localeString);

  return {
    language: locale.language,
    region: locale.region,
    calendar: locale.calendar || "default",
    numberingSystem: locale.numberingSystem || "default",
    hourCycle: locale.hourCycle || "default"
  };
}

console.log(describeLocalePreferences("th-TH-u-ca-buddhist-nu-thai"));
// Output: { language: "th", region: "TH", calendar: "buddhist", numberingSystem: "thai", hourCycle: "default" }

Свойства collation, caseFirst и numeric соответствуют ключам расширений co, kf и kn:

const locale = new Intl.Locale("de-DE-u-co-phonebk-kf-upper-kn-true");

console.log(locale.collation); // "phonebk"
console.log(locale.caseFirst); // "upper"
console.log(locale.numeric); // true

Обратите внимание, что свойство numeric возвращает булево значение, а не строку. Значение true означает, что числовая сортировка включена.

Комбинирование нескольких расширений

Вы можете объединять несколько расширений в одном идентификаторе локали. Это позволяет указать все предпочтения форматирования сразу.

const locale = new Intl.Locale("ar-SA-u-ca-islamic-nu-arab-hc-h12-co-standard");

const dateFormatter = new Intl.DateTimeFormat(locale, {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const date = new Date("2025-03-15T14:30:00");
console.log(dateFormatter.format(date));
// Output uses Islamic calendar, Arabic-Indic numerals, and 12-hour time

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

const locale = new Intl.Locale("en-US-u-hc-h23-hc-h12");
console.log(locale.hourCycle); // "h12"

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

Практические примеры использования

Юникод-расширения решают реальные задачи в интернационализированных приложениях. Понимание типовых сценариев поможет эффективно применять расширения.

Хранение пользовательских предпочтений

Храните настройки форматирования пользователя в одной строке локали вместо нескольких полей конфигурации:

function saveUserPreferences(userId, localeString) {
  const locale = new Intl.Locale(localeString);

  return {
    userId,
    language: locale.language,
    region: locale.region,
    localeString: locale.toString(),
    preferences: {
      calendar: locale.calendar,
      numberingSystem: locale.numberingSystem,
      hourCycle: locale.hourCycle
    }
  };
}

const preferences = saveUserPreferences(123, "ar-SA-u-ca-islamic-nu-arab-hc-h12");

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

Создание селекторов локалей

Дайте пользователям возможность выбирать параметры форматирования через интерфейс, формируя строки локалей с расширениями:

function buildLocaleFromUserInput(language, region, preferences) {
  const options = {};

  if (preferences.calendar) {
    options.calendar = preferences.calendar;
  }

  if (preferences.numberingSystem) {
    options.numberingSystem = preferences.numberingSystem;
  }

  if (preferences.hourCycle) {
    options.hourCycle = preferences.hourCycle;
  }

  const locale = new Intl.Locale(`${language}-${region}`, options);
  return locale.toString();
}

const userLocale = buildLocaleFromUserInput("th", "TH", {
  calendar: "buddhist",
  numberingSystem: "thai",
  hourCycle: "h23"
});

console.log(userLocale);
// Output: "th-TH-u-ca-buddhist-hc-h23-nu-thai"

Поддержка религиозных календарей

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

function createReligiousCalendarFormatter(religion, baseLocale) {
  const calendars = {
    jewish: "hebrew",
    muslim: "islamic",
    buddhist: "buddhist"
  };

  const calendar = calendars[religion];
  if (!calendar) {
    return new Intl.DateTimeFormat(baseLocale);
  }

  const locale = new Intl.Locale(baseLocale, { calendar });
  return new Intl.DateTimeFormat(locale, {
    year: "numeric",
    month: "long",
    day: "numeric"
  });
}

const jewishFormatter = createReligiousCalendarFormatter("jewish", "en-US");
console.log(jewishFormatter.format(new Date("2025-03-15")));
// Output: "15 Adar II 5785"

Сортировка по пользовательским правилам

Используйте расширения колляции для реализации сортировки с учётом локали:

function sortNames(names, locale, collationType) {
  const localeWithCollation = new Intl.Locale(locale, {
    collation: collationType
  });

  const collator = new Intl.Collator(localeWithCollation);
  return names.sort((a, b) => collator.compare(a, b));
}

const germanNames = ["Müller", "Meyer", "Möller", "Mueller"];
const sorted = sortNames(germanNames, "de-DE", "phonebk");
console.log(sorted);

Отображение традиционных цифр

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

function formatTraditionalNumber(number, locale, numberingSystem) {
  const localeWithNumbering = new Intl.Locale(locale, {
    numberingSystem
  });

  return new Intl.NumberFormat(localeWithNumbering).format(number);
}

console.log(formatTraditionalNumber(123456, "ar-EG", "arab"));
// Output: "١٢٣٬٤٥٦"

console.log(formatTraditionalNumber(123456, "th-TH", "thai"));
// Output: "๑๒๓,๔๕๖"

Поддержка браузерами

Юникод-расширения работают во всех современных браузерах. Chrome, Firefox, Safari и Edge поддерживают синтаксис расширений в идентификаторах локалей и соответствующие свойства в объектах Intl.Locale.

Доступность конкретных значений расширений зависит от реализации браузера. Все браузеры поддерживают распространённые значения, такие как gregory для календаря, latn для системы счисления и h12 или h23 для 12- или 24-часового формата. Менее распространённые значения, например, традиционные китайские календари или системы счисления для языков меньшинств, могут не поддерживаться во всех браузерах.

При использовании менее распространённых значений расширений тестируйте идентификаторы локалей в целевых браузерах. Используйте свойства Intl.Locale, чтобы проверить, распознал ли браузер ваши значения расширений:

const locale = new Intl.Locale("zh-CN-u-ca-chinese");
console.log(locale.calendar);
// If browser supports Chinese calendar: "chinese"
// If browser does not support it: undefined

Node.js поддерживает расширения Unicode начиная с версии 12, а полный набор свойств доступен с версии 18 и выше.

Кратко

Расширения Unicode позволяют добавлять настройки форматирования к идентификаторам локали. Вместо того чтобы настраивать каждый форматтер отдельно, вы указываете предпочтения один раз в строке локали.

Основные понятия:

  • Расширения начинаются с -u-, за которым следуют пары ключ-значение
  • Ключ ca определяет календарную систему
  • Ключ nu определяет систему счисления
  • Ключ hc определяет формат часового цикла
  • Ключ co определяет правила сортировки (колляции)
  • Ключ kf определяет порядок с учётом регистра
  • Ключ kn включает числовую колляцию
  • Можно использовать строки расширения или объекты с опциями
  • Расширения работают как значения по умолчанию, которые можно переопределить через опции форматтера
  • Объект Intl.Locale предоставляет расширения как свойства

Используйте расширения Unicode для хранения пользовательских настроек, поддержки культурных календарей, отображения традиционных чисел и реализации сортировки с учётом локали. Это стандартный способ кастомизации форматирования в JavaScript для интернационализации.