Как настроить локали с помощью расширений 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));
// Вывод: "٢٠ رمضان ١٤٤٦ هـ"

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

Буддийский календарь широко используется в Таиланде. Он отсчитывает годы от рождения Будды в 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")));
// Вывод: "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));
// Вывод: "١٢٣٬٤٥٦"

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

Тайские цифры предоставляют ещё один пример:

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

console.log(formatter.format(123456));
// Вывод: "๑๒๓,๔๕๖"

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

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

Ключ 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));
// Вывод: "12:30 AM"

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

console.log(new Intl.DateTimeFormat(europe23Hour, { hour: "numeric", minute: "numeric" }).format(date));
// Вывод: "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)));
// Вывод: ["Meyer", "Möller", "Mueller", "Müller"]

console.log(names.sort((a, b) => phonebook.compare(a, b)));
// Вывод: ["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(), при использовании с компараторами.

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

Ключ 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)));
// Результат: ["APPLE", "Apple", "apple", "banana"]

console.log(words.sort((a, b) => lowerFirst.compare(a, b)));
// Результат: ["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)));
// Результат: ["item1", "item10", "item2", "item20"]

console.log(items.sort((a, b) => numeric.compare(a, b)));
// Результат: ["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());
// Вывод: "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);

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

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

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

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

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

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

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"));
// Вывод: { 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));
// Вывод использует исламский календарь, арабско-индийские цифры и 12-часовой формат времени

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

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

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

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

Расширения Unicode решают реальные проблемы в интернационализированных приложениях. Понимание распространённых сценариев использования помогает эффективно применять расширения.

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

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

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"

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

Используйте расширения для колlation, чтобы реализовать сортировку, специфичную для локали:

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"));
// Вывод: "١٢٣٬٤٥٦"

console.log(formatTraditionalNumber(123456, "th-TH", "thai"));
// Вывод: "๑๒๓,๔๕๖"

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

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

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

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

const locale = new Intl.Locale("zh-CN-u-ca-chinese");
console.log(locale.calendar);
// Если браузер поддерживает китайский календарь: "chinese"
// Если браузер не поддерживает: undefined

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

Резюме

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

Ключевые концепции:

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

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