25 октября 2023 г.

Intl: интернационализация в JavaScript

Общая проблема строк, дат, чисел в JavaScript – они «не в курсе» языка и особенностей стран, где находится посетитель.

В частности:

Строки
При сравнении сравниваются коды символов, а это неправильно, к примеру, в русском языке оказывается, что "ё" > "я" и "а" > "Я", хотя всем известно, что я – последняя буква алфавита и это она должна быть больше любой другой.
Даты
В разных странах принята разная запись дат. Где-то пишут 31.12.2014 (Россия), а где-то 12/31/2014 (США), где-то иначе.
Числа
В одних странах выводятся цифрами, в других – иероглифами, длинные числа разделяются где-то пробелом, где-то запятой.

Все современные браузеры, кроме IE10 (но есть библиотеки и для него) поддерживают стандарт ECMA 402, предназначенный решить эти проблемы навсегда.

Основные объекты

Intl.Collator
Умеет правильно сравнивать и сортировать строки.
Intl.DateTimeFormat
Умеет форматировать дату и время в соответствии с нужным языком.
Intl.NumberFormat
Умеет форматировать числа в соответствии с нужным языком.

Локаль

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

Локаль описывается строкой из трёх компонентов, которые разделяются дефисом:

  1. Код языка.
  2. Код способа записи.
  3. Код страны.

На практике не всегда указаны три, обычно меньше:

  1. ru – русский язык, без уточнений.
  2. en-GB – английский язык, используемый в Англии (GB).
  3. en-US – английский язык, используемый в США (US).
  4. zh-Hans-CN – китайский язык (zh), записываемый упрощённой иероглифической письменностью (Hans), используемый в Китае.

Также через суффикс -u-* можно указать расширения локалей, например "th-TH-u-nu-thai" – тайский язык (th), используемый в Таиланде (TH), с записью чисел тайскими буквами (๐, ๑, ๒, ๓, ๔, ๕, ๖, ๗, ๘, ๙) .

Стандарт, который описывает локали – RFC 5646, языки описаны в IANA language registry.

Все методы принимают локаль в виде строки или массива, содержащего несколько локалей в порядке предпочтения.

Если локаль не указана или undefined – берётся локаль по умолчанию, установленная в окружении (браузере).

Подбор локали localeMatcher

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

У него два значения:

  • "lookup" – означает простейший порядок поиска путём обрезания суффикса, например zh-Hans-CNzh-Hanszh → локаль по умолчанию.
  • "best fit" – использует встроенные алгоритмы и предпочтения браузера (или другого окружения) для выбора подходящей локали.

По умолчанию стоит "best fit".

Если локалей несколько, например ["zh-Hans-CN", "ru-RU"] то localeMatcher пытается подобрать наиболее подходящую локаль для первой из списка (китайская), если не получается – переходит ко второй (русской) и так далее. Если ни одной не нашёл, например на компьютере не совсем поддерживается ни китайский ни русский, то используется локаль по умолчанию.

Как правило, "best fit" является здесь наилучшим выбором.

Строки, Intl.Collator

Синтаксис:

// создание
let collator = new Intl.Collator([locales, [options]])

Параметры:

locales

Локаль, одна или массив в порядке предпочтения.

options

Объект с дополнительными настройками:

  • localeMatcher – алгоритм выбора подходящей локали.

  • usage – цель сравнения: сортировка "sort" или поиск "search", по умолчанию "sort".

  • sensitivity – чувствительность: какие различия в символах учитывать, а какие – нет, варианты:

    • base – учитывать только разные символы, без диакритических знаков и регистра, например: а ≠ б, е = ё, а = А.
    • accent – учитывать символы и диакритические знаки, например: а ≠ б, е ≠ ё, а = А.
    • case – учитывать символы и регистр, например: а ≠ б, е = ё, а ≠ А.
    • variant – учитывать всё: символ, диакритические знаки, регистр, например: а ≠ б, е ≠ ё, а ≠ А, используется по умолчанию.
  • ignorePunctuation – игнорировать знаки пунктуации: true/false, по умолчанию false.

  • numeric – использовать ли численное сравнение: true/false, если true, то будет 12 > 2, иначе 12 < 2.

  • caseFirst – в сортировке должны идти первыми прописные или строчные буквы, варианты: "upper" (прописные), "lower" (строчные) или "false" (стандартное для локали, также является значением по умолчанию). Не поддерживается IE11.

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

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

let result = collator.compare(str1, str2);

Результат compare имеет значение 1 (больше), 0 (равно) или -1 (меньше).

Например:

let collator = new Intl.Collator();

alert( "ёжик" > "яблоко" ); // true (ёжик больше, что неверно)
alert( collator.compare("ёжик", "яблоко") ); // -1 (ёжик меньше, верно)

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

let collator1 = new Intl.Collator();
alert( collator1.compare("ЁжиК", "ёжик") ); // 1, разные

let collator2 = new Intl.Collator(undefined, {
  sensitivity: "accent"
});
alert( collator2.compare("ЁжиК", "ёжик") ); // 0, одинаковые

Даты, Intl.DateTimeFormat

Синтаксис:

// создание
let formatter = new Intl.DateTimeFormat([locales, [options]])

Первый аргумент – такой же, как и в Collator, а в объекте options мы можем определить, какие именно части даты показывать (часы, месяц, год…) и в каком формате.

Полный список свойств options:

Свойство Описание Возможные значения По умолчанию
localeMatcher Алгоритм подбора локали lookup, best fit best fit
formatMatcher Алгоритм подбора формата basic, best fit best fit
hour12 Включать ли время в 12-часовом формате true -- 12-часовой формат, false -- 24-часовой
timeZone Временная зона Временная зона, например Europe/Moscow UTC
weekday День недели narrow, short, long
era Эра narrow, short, long
year Год 2-digit, numeric undefined или numeric
month Месяц 2-digit, numeric, narrow, short, long undefined или numeric
day День 2-digit, numeric undefined или numeric
hour Час 2-digit, numeric
minute Минуты 2-digit, numeric
second Секунды 2-digit, numeric
timeZoneName Название таймзоны (нет в IE11) short, long

Все локали обязаны поддерживать следующие наборы настроек:

  • weekday, year, month, day, hour, minute, second
  • weekday, year, month, day
  • year, month, day
  • year, month
  • month, day
  • hour, minute, second

Если указанный формат не поддерживается, то настройка formatMatcher задаёт алгоритм подбора наиболее близкого формата: basic – по стандартным правилам и best fit – по умолчанию, на усмотрение окружения (браузера).

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

let dateString = formatter.format(date);

Например:

let date = new Date(2014, 11, 31, 12, 30, 0);

let formatter1 = new Intl.DateTimeFormat("ru");
alert( formatter1.format(date) ); // 31.12.2014

let formatter2 = new Intl.DateTimeFormat("en-US");
alert( formatter2.format(date) ); // 12/31/2014

Длинная дата, с настройками:

let date = new Date(2014, 11, 31, 12, 30, 0);

let formatter = new Intl.DateTimeFormat("ru", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
});

alert( formatter.format(date) ); // среда, 31 декабря 2014 г.

Только время:

let date = new Date(2014, 11, 31, 12, 30, 0);

let formatter = new Intl.DateTimeFormat("ru", {
  hour: "numeric",
  minute: "numeric",
  second: "numeric"
});

alert( formatter.format(date) ); // 12:30:00

Числа, Intl.NumberFormat

Форматтер Intl.NumberFormat умеет красиво форматировать не только числа, но и валюту, а также проценты.

Синтаксис:

let formatter = new Intl.NumberFormat([locales[, options]]);

formatter.format(number); // форматирование

Параметры, как и раньше – локаль и опции.

Список опций:

Свойство Описание Возможные значения По умолчанию
localeMatcher Алгоритм подбора локали lookup, best fit best fit
style Стиль форматирования decimal, percent, currency decimal
currency Алфавитный код валюты См. Список кодов валюты, например USD
currencyDisplay Показывать валюту в виде кода, локализованного символа или локализованного названия code, symbol, name symbol
useGrouping Разделять ли цифры на группы true, false true
minimumIntegerDigits Минимальное количество цифр целой части от 1 до 21 21
minimumFractionDigits Минимальное количество десятичных цифр от 0 до 20 для чисел и процентов 0, для валюты зависит от кода.
maximumFractionDigits Максимальное количество десятичных цифр от minimumFractionDigits до 20. для чисел max(minimumFractionDigits, 3), для процентов 0, для валюты зависит от кода.
minimumSignificantDigits Минимальное количество значимых цифр от 1 до 21 1
maximumSignificantDigits Максимальное количество значимых цифр от minimumSignificantDigits до 21 21

Пример без опций:

let formatter = new Intl.NumberFormat("ru");
alert( formatter.format(1234567890.123) ); // 1 234 567 890,123

С ограничением значимых цифр (важны только первые 3):

let formatter = new Intl.NumberFormat("ru", {
  maximumSignificantDigits: 3
});
alert( formatter.format(1234567890.123) ); // 1 230 000 000

С опциями для валюты:

let formatter = new Intl.NumberFormat("ru", {
  style: "currency",
  currency: "GBP"
});

alert( formatter.format(1234.5) ); // 1 234,5 £

С двумя цифрами после запятой:

let formatter = new Intl.NumberFormat("ru", {
  style: "currency",
  currency: "GBP",
  minimumFractionDigits: 2
});

alert( formatter.format(1234.5) ); // 1 234,50 £

Методы в Date, String, Number

Методы форматирования также поддерживаются в обычных строках, датах, числах:

String.prototype.localeCompare(that [, locales [, options]])

Сравнивает строку с другой, с учётом локали, например:

let str = "ёжик";

alert( str.localeCompare("яблоко", "ru") ); // -1
Date.prototype.toLocaleString([locales [, options]])

Форматирует дату в соответствии с локалью, например:

let date = new Date(2014, 11, 31, 12, 0);

alert( date.toLocaleString("ru", { year: 'numeric', month: 'long' }) ); // Декабрь 2014
Date.prototype.toLocaleDateString([locales [, options]])

То же, что и выше, но опции по умолчанию включают в себя год, месяц, день

Date.prototype.toLocaleTimeString([locales [, options]])

То же, что и выше, но опции по умолчанию включают в себя часы, минуты, секунды

Number.prototype.toLocaleString([locales [, options]])

Форматирует число, используя опции Intl.NumberFormat.

Все эти методы при запуске создают соответствующий объект Intl.* и передают ему опции, можно рассматривать их как укороченные варианты вызова.

Старые IE

В IE10 рекомендуется использовать полифил, например библиотеку https://github.com/andyearnshaw/Intl.js.

Задачи

важность: 5

Используя Intl.Collator, отсортируйте массив:

let animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"];

// ... ваш код ...

alert( animals ); // АИСТ,ёж,енот,ехидна,тигр,ЯК

В этом примере порядок сортировки не должен зависеть от регистра.

Что касается буквы "ё", то мы следуем обычным правилам сортировки буквы ё, по которым «е» и «ё» считаются одной и той же буквой, за исключением случая, когда два слова отличаются только в позиции буквы «е» / «ё» – тогда слово с «е» ставится первым.

Здесь подойдут стандартные параметры сравнения:

let animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"];

let collator = new Intl.Collator();
animals.sort((a, b) => collator.compare(a, b));

alert( animals ); // АИСТ,ёж,енот,ехидна,тигр,ЯК

А вот, что было бы при обычном вызове sort():

let animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"];

alert( animals.sort() ); // АИСТ,ЯК,енот,ехидна,тигр,ёж
Карта учебника

Комментарии

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.