JavaScript Intl API интернационализация (I18n)

Английский — один из самых распространенных языков в мире, каждый седьмой житель планеты говорит на нем. Это первый (родной) язык для 379 миллионов человек, но 917 миллионов говорят на китайском, 460 миллионов — на испанском и 341 миллион — на хинди.

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

JavaScript Internationalization API (API интернационализации), также известный как i18n, позволяет разрабатывать веб-страницы и приложения таким образом, чтобы их можно было легко адаптировать для пользователей, говорящих на разных языках. В этой статье будут рассмотрены различные методы, предлагаемые API и то, как эти возможности можно использовать в коде, чтобы шире охватить международную аудиторию.

Интернационализация (I18n) может быть сложной задачей

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

  • Испанский: nombre, email, fecha
  • Французский: nom, e-mail, date
  • Немецкий: name, email, datum

Система интернационализации и локализации Gettext существует уже несколько десятилетий, а библиотеки доступны для большинства языков программирования.

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

<label for="name">{{ NAME }}</label>

Здесь NAME динамически заменяется на «Name», когда пользователь в качестве основного языка выбрал английский. К сожалению, именно здесь начинаются проблемы для пользовательского интерфейса:

  1. Могут быть разные варианты для одного и того же языка. Например, испанский, на котором говорят в Испании, не идентичен тому, на котором говорят в Южной Америке.
  2. В некоторых языках слова могут быть значительно длиннее на других. Например, «email» на русском языке будет выглядеть, как «электронное письмо».
  3. Текст не всегда ориентирован слева направо. Для некоторых языков слова пишутся справа налево, например: арабский, иврит, курдский и идиш. Ещё написание может быть сверху вниз, например в китайском, корейском, японском и тайваньском языках.

Многие проблемы можно решить, если свести текст к минимуму и использовать CSS-свойства, такие как direction (направление), writing-mode (режим письма) и логические размеры для макета.

Терминологическая путаница

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

Рассмотрим дату, которая выглядит, как «12/03/24». Она может быть прочитана так:

  • «3 December 2024» для жителей США, использующих формат MDY
  • «12 March 2024» для жителей Европы, Южной Америки и Азии, использующих формат DMY, и
  • «24 March 2012» для жителей Канады, Китая, Японии и Венгрии, которые считают более практичным формат YMD.

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

Число «1,000» может читаться как:

  • «Одна тысяча» в США, Великобритании, Канаде, Китае и Японии, а также
  • «Один (целый, ноль тысячных)» в Испании, Франции, Германии и России, где десятичная дробь числа отделена запятой.

Ситуация может осложняться даже в одном только английском языке. Термин «1,000 meters» означает:

  • 1 километр (или 0,62 мили) для жителей США
  • набор из тысячи измерительных приборов в Великобритании, Канаде и Австралии!

JavaScript Intl API

Малоизвестный JavaScript-объект Intl в большинстве современных браузеров и сред выполнения реализует API интернационализации ECMAScript. Поддержка в целом неплохая, и даже в IE11 есть много полезных методов. Для старых браузеров есть полифилл, а поддержку API можно определить так:

if (window.Intl) {
  // Intl поддерживается
}

Сам API немного необычный. Он предоставляет несколько конструкторов объектов для дат, времени, чисел и списков, которым передаются языковой стандарт и необязательный объект, содержащий параметры конфигурации. Например, вот объект DateTime, для английского (США):

const dateFormatter = new Intl.DateTimeFormat('en-US');

Этот объект можно использовать любое количество раз для вызова различных методов, которым передается значение Date() (или ES6 Temporal, если доступно). Метод format() используется чаще всего, например:

const valentinesDay = dateFormatter.format( new Date('2022-02-14') );
// возвращает US формат "2/14/2022"

const starwarsDay = dateFormatter.format( new Date('2022-05-04') );
// возвращает US формат "5/4/2022"

То же самое можно сделать, создав объект Intl:

const starwarsDay = new Intl.DateTimeFormat('en-US')
    .format(new Date('2022-05-04'));

Помимо метода format(), некоторые объекты поддерживают ещё:

  • formatToParts(), который возвращает массив объектов, содержащих отформатированные строки, например {type: 'weekday', value: 'Monday'}
  • resolvedOptions(), который возвращает новый объект со свойствами, отражающими используемый языковой стандарт и параметры форматирования, например dateFormatter.resolvedOptions().locale

Определение локалей

Все объекты Intl требуют передавать в качестве аргумента locale (языковой стандарт). Это строка, которая идентифицирует:

  • языковой подтэг
  • подтег скрипта (необязательно)
  • подтег региона или страны (необязательно)
  • один или несколько вариантов подтегов (необязательно)
  • одна или несколько последовательностей расширения BCP 47 (необязательно)
  • приватная последовательность расширения (необязательно)

Часто, достаточно языка и региона. Например, ru-RU, fr-FR и т. д.

Помимо использования строки, объект Intl.locale может использоваться для создания таких локалей, как английский (США) с 12-часовым форматом времени:

const us = new Intl.Locale('en', {
  region: 'US', hourCycle: 'h12', calendar: 'gregory'
});

Это можно использовать в другом конструкторе Intl, например:

new Intl.DateTimeFormat(us, { timeStyle: 'medium' })
  .format( new Date('2022-05-04T13:00:00') );
// "1:00:00 PM"

Если языковой стандарт (locale) не определен, используются настройки языка и региона устройства, например:

new Intl.DateTimeFormat().format(new Date('2022-05-04'));

Pltcm будет возвращаться 5/4/2022 на устройстве с настройками для США и 04/05/2022 на устройстве с настройками для Великобритании.

Даты и время

Следующий инструмент показывает примеры дат и времени, отформатированных с помощью Intl.DateTimeFormat():

See this code i18n date and time formatting tool on x.xhtml.ru.

Конструктору передаются языковой стандарт (locale) и объект с параметрами. У этого объекта есть много различных свойств, хотя обычно редко требуется больше, чем dateStyle и/или timeStyle:

свойствоописание
dateStyleстиль даты: "full" "long" "medium" "short"
timeStyleстиль времени: "full" "long" "medium" "short"
calendarопции включают: "chinese" "gregory" "hebrew" "indian" "islamic" и т.д.
dayPeriodвыражения периода: "narrow" "short" "long"
numberingSystemсистема нумерации: "arab" "beng" "fullwide" "latn" и т.д.
localeMatcherалгоритм распознавания локали: "lookup" "best fit"
timeZoneчасовой пояс: "America/New_York" "Europe/Paris" etc.
hour12true для 12-часового отображения времени
hourCycleчасовой цикл: "h11" "h12" "h23" "h24"
formatMatcherалгоритм распознавания формата: "basic" "best fit"
weekdayформат дней недели: "long" "short" "narrow"
eraформат эры: "long" "short" "narrow"
yearформат года: "numeric" "2-digit"
monthформат времени: "numeric" "2-digit" "long" "short" "narrow"
dayформат дня месяца: "numeric" "2-digit"
hourформат часов: "numeric" "2-digit"
minuteформат минут: "numeric" "2-digit"
secondформат секунд: "numeric" "2-digit"
timeZoneNameлюбое из: "long" "short"

Примеры:

// Japanese короткая дата без времени: "2022/05/04"
new Intl.DateTimeFormat("ja-JP", { dateStyle: "short" })
  .format( new Date("2022-05-04T13:00") );

// US короткая дата и время: "5/4/22, 1:00 PM"
new Intl.DateTimeFormat("en-US", { dateStyle: "short", timeStyle: "short" })
  .format( new Date("2022-05-04T13:00") );

// UK длинная дата, короткое время: "4 May 2022 at 13:00"
new Intl.DateTimeFormat("en-GB", { dateStyle: "long", timeStyle: "short" })
  .format( new Date("2022-05-04T13:00") );

// Spanish дата и время полные
// (в зависимости от локали вашей системы)
// "miércoles, 4 de mayo de 2022, 13:00:00 (hora de verano británica)"
new Intl.DateTimeFormat("es-ES", { dateStyle: "full", timeStyle: "full" })
  .format( new Date("2022-05-04T13:00") );

Диапазоны дат

Метод formatRange() принимает две даты и форматирует период наиболее кратким образом в зависимости от языкового стандарта и параметров, например:

// результат: "4 May 2022, 13:00–14:00"
new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeStyle: "short" })
  .formatRange(new Date("2022-05-04T13:00"), new Date("2022-05-04T14:00"))

Этот метод имеет ограниченную поддержку в браузерах, но был реализован в Chrome 76.

Относительные периоды

Объект Intl.RelativeTimeFormat() может отображать периоды, относящиеся к этому моменту времени. У объекта c параметрами меньше опций:

свойствоописание
localeMatcherалгоритм распознавания локали: "lookup" "best fit"
numericодно из "always", т.е. "1 день назад" или "auto", т.е. "yesterday"
styleформат: "long" "short" "narrow"

В метод format() передается числовое значение и единица измерения: "year" (год), "quarter" (квартал), "month" (месяц), "week" (неделя), "day" (день), "hour" (час), "minute" (минута), или "second" (секунда). Примеры:

// US 1 день назад, numeric: "1 day ago"
new Intl.RelativeTimeFormat("en-US")
  .format( -1, "day" );

// US на 1 день, auto: "tomorrow" (завтра)
new Intl.RelativeTimeFormat("en-US", { numeric: "auto" })
  .format( -1, "day" );

// German, следующий месяц auto: "nächsten Monat" (в следующем месяце)
new Intl.RelativeTimeFormat("de-DE", { numeric: "auto" })
  .format( 1, "month" );

Числа, валюты, проценты и единицы измерения

Следующий пример показывает варианты использования Intl.NumberFormat() для форматирования чисел, валют, процентов и единиц измерения:

See this code i18n number and currency formatting tool on x.xhtml.ru.

Конструктору передаются языковой стандарт и объект с параметрами:

свойствоописание
numberingSystemсистема нумерации: "arab" "beng" "deva" "fullwide" "latn" и т.д.
notationтип: "standard" "scientific" "engineering" "compact"
styleформатирование: "decimal" "currency" "percent" "unit" — this determines which other options can be set
currencyкод валюты: "USD" "EUR" "GBP" etc.
currencyDisplayформат валюты: "symbol" "narrowSymbol" "code" "name"
currencySignдля отрицательных значений валюты, "standard" знак «минус» или "accounting" для скобок
unitunit type: "centimeter" "inch" "hour" и т.д.
unitDisplayформат единиц изм.: "long" "short" "narrow"
useGroupingfalse убирает разделитель тысячных
minimumIntegerDigitsминимальное количество целых цифр
minimumFractionDigitsминимальное количество разрядов дробной части
maximumFractionDigitsмаксимальное количество разрядов дробной части
minimumSignificantDigitsминимальное количество значащих цифр
maximumSignificantDigitsмаксимальное количество значащих цифр

Примеры:

// US округление до 2 десятичных: "12,345.68"
new Intl.NumberFormat("en-US", { maximumSignificantDigits: 2 })
  .format( 12345.6789 );

// French округление до 3 десятичных знаков: "12 345,689"
new Intl.NumberFormat("fr-FR", { maximumSignificantDigits: 3 })
  .format( 12345.6789 );

// US компактное число, 0 десятичных знаков: "12K"
new Intl.NumberFormat("en-US", { notation: "compact", maximumSignificantDigits: 0 })
  .format( 12345.6789 );

// Spanish запись валюты амер. долл.: "12.345,68 US$"
new Intl.NumberFormat("es-ES", {
  style: "currency",
  currency: "USD",
  currencyDisplay: "symbol"
})
  .format( 12345.6789 );

// UK метры в длинном формате без десятичн.: "12,346 metres"
new Intl.NumberFormat("en-GB", {
  maximumSignificantDigits: 0,
  style: "unit",
  unit: "meter",
  unitDisplay: "long"
})
  .format( 12345.6789 );

Списки

Объект Intl.ListFormat() может форматировать массив элементов в чувствительный к языку список. В английском языке это обычно требует наличия «and» (и) или «or» (или) перед последним элементом.

Объект c параметрами может устанавливать следующие свойства:

свойствоописание
typeформат вывода: "conjunction" для «и«-списков, "disjunction" для «или«-списков
styleформатирование: "long" "short" "narrow"

Примеры:

const browsers = ['Chrome', 'Firefox', 'Edge', 'Safari'];

// US English: "Chrome, Firefox, Edge, and Safari"
new Intl.ListFormat("en-US", { type: "conjunction" }).format(browsers);

// US English: "Chrome, Firefox, Edge, or Safari"
new Intl.ListFormat("en-US", { type: "disjunction" }).format(browsers);

// French: "Chrome, Firefox, Edge, et Safari"
new Intl.ListFormat("fr-FR", { type: "conjunction" }).format(browsers);

// French: "Chrome, Firefox, Edge, ou Safari"
new Intl.ListFormat("fr-FR", { type: "disjunction" }).format(browsers);

Множественное число

Немного причудливый объект Intl.PluralRules() включает правила языка с учетом множественного значения числа, lkz форматирования количества элементов. Объект с параметрами может установить для свойства type одно из следующих значений:

  • cardinal: количество вещей (по умолчанию), или
  • ordinal: рейтинг вещей, как 1st, 2nd, или 3rd в английском

Метод select() возвращает строку на английском языке, представляющую категорию множественного значения числа: zero, one, two, few, many, или other.

Примеры:

// US English zero cardinal: "other"
new Intl.PluralRules("en-US", { type: "cardinal" }).select(0);

// US English zero ordinal: "other"
new Intl.PluralRules("en-US", { type: "ordinal" }).select(0);

// US English 1 cardinal: "one"
new Intl.PluralRules("en-US", { type: "cardinal" }).select(1);

// US English 1 ordinal: "one"
new Intl.PluralRules("en-US", { type: "ordinal" }).select(1);

// US English 2 cardinal: "other"
new Intl.PluralRules("en-US", { type: "cardinal" }).select(2);

// US English 2 ordinal: "two"
new Intl.PluralRules("en-US", { type: "ordinal" }).select(2);

// US English 3 cardinal: "other"
new Intl.PluralRules("en-US", { type: "cardinal" }).select(3);

// US English 3 ordinal: "few"
new Intl.PluralRules("en-US", { type: "ordinal" }).select(3);

Сравнение строк

Наконец, объект Intl.Collator() позволяет сравнивать строки с учетом языка. Его объект c параметрами может устанавливать следующие свойства:

свойствоописание
collationсопоставление вариантов для определенных языков
numerictrue для числового сопоставления, где ‘1 < 2 < 10’
caseFirstрегистр первого символа "upper" или "lower"
usageлюбая строка "sort" (по умолчанию) или "search"
sensitivity"base" "accent" "case" "variant"
ignorePunctuationtrue игнорировать пунктуацию

Метод compare() сравнивает две строки, например:

// German: возвращает 1
new Intl.Collator('de').compare('z', 'ä');

Заключение

Отображение информации с учетом формата языковых настроек устройства пользователя должно быть простым, если для отображения данных используется JavaScript. Например, следующий код определяет функцию dateFormat(), которая использует краткий формат даты Intl или возвращается к YYYY-MM-DD, если он не поддерживается:

// функция форматирования даты
const dateFormat = (Intl && Intl.DateTimeFormat ?
  date => new Intl.DateTimeFormat({ dateStyle: 'short' }).format(date) :
  date => date.toISOString().slice(0, 10)
);

// вставляет сегодняшнюю дату в DOM-элемент #today
document.getElementById('today').textContent = dateFormat( new Date() );

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