Intl: мощный браузерный API, который вы, возможно, не используете
- пятница, 24 апреля 2026 г. в 00:00:35
Вероятно, вы когда-нибудь использовали Moment.js, date-fns, Luxon или numeral.js. Разработчики годами полагаются на эти библиотеки для форматирования дат, чисел и валют. Это очень полезные библиотеки, но у них есть и свои недостатки: они увеличивают размер сборки на несколько килобайт и требуют разбора кода на стороне клиента.
Библиотека | Размер |
|---|---|
Moment.js | 295 кБ |
date-fns | 77 кБ |
luxon.js | 82 кБ |
numeral.js | 11 кБ |
Intl API широко доступен (за исключением Intl.DurationFormat, который работает во всех современных браузерах, но существует недостаточно долго, чтобы считаться «широко доступным») и может удовлетворить почти все требования к форматированию непосредственно в браузере, без загрузки кода и без необходимости его парсинга. Он также учитывает языковые предпочтения пользователей, поэтому даты и числа можно форматировать так, как им удобно, без дополнительных усилий.
Прим. пер.: в статье много прикольных интерактивных примеров, которые нельзя перенести на Хабр, поэтому я создал небольшую демку с аналогичными примерами (открывается только через ВПН, РКН зачем-то блочит Netlify). Код демо на GitHub (локали и их названия определены в файле src/constants.ts). Большинство сниппетов кода из статьи можно выполнять прямо в консоли инструментов разработчика в браузере.
Возможно, вы думаете: «Мой сайт только на одном языке. Зачем мне нужен Intl?»
Это самая распространенная причина, по которой разработчики игнорируют Intl. Но Intl не столько про перевод (на другой язык) пользовательского интерфейса, сколько о правильном форматировании данных для пользователей.
Рассмотрим дату 09/12/2025. В США она читается как двенадцатое сентября. Почти во всем остальном мире она читается как девятое декабря. Это довольно большая разница, и если небрежно (или не) отформатированная дата приводит к тому, что кто-то пропускает встречу или просто путается, это очень плохо отражается на авторитете сайта.
Intl форматирует даты, числа, валюты, списки и текст в соответствии с локалью пользователя. Даже если наш сайт только на английском языке, у пользователей из разных англоязычных стран все равно разные правила форматирования. Intl обрабатывает все это за нас, поэтому нам не нужно об этом беспокоиться.
0 Кб в сборке: он встроен в браузер
широко доступен: работает во всех современных браузерах
учитывает локали: знает не только языки, но и особенности каждой страны
производительность: нативные реализации значительно быстрее, чем пользовательские библиотеки
API, которые мы рассмотрим в статье:
Intl.DateTimeFormat: форматирование дат и времени
Intl.RelativeTimeFormat: «вчера», «через 3 дня» и т.д.
Intl.DurationFormat: «2 часа 5 минут 30 секунд» и т.п.
Intl.NumberFormat: форматирование чисел, валют и единиц измерения
Intl.ListFormat: «яблоки, апельсины и бананы» и т.п.
Intl.PluralRules: правильное определение формы множественного числа
Intl.Segmenter: правильное разделение текста на слова, предложения или графемы
Intl.Collator: сортировка строк с учетом локали
В дальнейшем в этой статье мы рассмотрим множество примеров, но распространенное заблуждение, или, возможно, желание, заключается в том, что Intl обрабатывает данные. К сожалению, Intl — это API для форматирования, а не для вычислений или измерений. Он просто принимает объекты (такие как даты, числа и массивы) и преобразует их в строки.
Например, если мы хотим отформатировать время относительно текущего, чтобы показать, когда была опубликована запись в блоге, по сравнению с текущим временем, нам нужно самостоятельно рассчитать разницу, а затем передать ее в функцию Intl.RelativeTimeFormat. Пример того, как это сделать, ищите в разделе, посвященном этой функции.
Далее в статье вы также увидите, что Intl может форматировать числа с единицами измерения (например, градусы Цельсия или мили), но не имеет представления о том, что эти единицы означают. Поэтому, хотя он и знает, как добавить символ °C после числа, когда мы запрашиваем градусы Цельсия, он не знает, как преобразовать градусы Фаренгейта в градусы Цельсия. Он просто принимает число и возвращает строку с указанием единицы измерения.
Все функции Intl имеют, примерно, одну сигнатуру:
Определяем локаль.
Определяем настройки.
Создаем форматтер.
Используем его для обработки данных.
Пример общей настройки Intl.DateTimeFormat:
const locale = 'en-US'; const options = { dateStyle: 'full', timeStyle: 'short', }; const formatter = new Intl.DateTimeFormat(locale, options);
После создания, форматтеру передается значение для форматирования:
const publishedAt = new Date('2026-04-07T16:45:00Z'); formatter.format(publishedAt); // → "Tuesday, April 7, 2026 at 4:45 PM" formatter.format(new Date('2026-06-01T09:00:00Z')); // → "Monday, June 1, 2026 at 9:00 AM"
Такой же шаблон прослеживается в большинстве функций Intl:
new Intl.DateTimeFormat(locale, options).format(date); new Intl.NumberFormat(locale, options).format(number); new Intl.ListFormat(locale, options).format(items); new Intl.PluralRules(locale, options).select(number); new Intl.Collator(locale, options).compare(a, b);
Аргументы конструктора также являются консистентными:
Строка локали, такая как en-US, nl-NL, or fr-FR. При отсутствии браузер использует системную локаль. Можно явно указать undefined для использования дефолтной локали при необходимости передачи настроек.
Объект настроек, определяющий результат. Доступные настройки зависят от форматтера, но шаблон настроек одинаковый.
Создание форматтера — самая затратная часть, поскольку она загружает данные локали и настраивает внутренние структуры. Вызов функций .format(), .select() или .compare() после этого обходится дешевле. Это означает, что при обработке большого количества значений форматтер следует создавать один раз и использовать повторно.
// Хорошо: создаем один раз, повторно используем много раз. const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'short', }); dates.map((date) => formatter.format(date)); // Плохо: создаем новый форматтер каждый раз. dates.map((date) => new Intl.DateTimeFormat('en-US', { dateStyle: 'short' }).format(date));
Это особенно важно для Intl.Collator, позволяющего сортировать строки с учетом локали. Сортировка может вызывать функцию сравнения много раз, поэтому определенно стоит создать сортировщик один раз и использовать его повторно.
Для указания локали можно использовать любой допустимый языковой тег BCP 47, например ‘en’, ‘en-US’, ‘nl-NL’ или ‘fr-FR’. Также можно указать список локалей, и браузер выберет первую из них, которую поддерживает.
Поддержка локалей различается в зависимости от комбинации браузера и операционной системы. Браузер знает о тысячах локалей, но фактически располагает данными (например, переведенными единицами измерения или форматами дат) только для нескольких сотен из них. Передавая список локалей, мы можем предусмотреть резервные варианты на случай, если у браузера нет данных для первой из них. Например, если мы укажем ['en-US', 'en-GB'], браузер будет использовать американский английский, если он доступен, или переключится на британский английский.
Если локаль не указана, форматтер использует локаль браузера. Мы можем явно указать undefined для достижения того же эффекта. Это означает, что мы можем создать форматтер, который автоматически адаптируется к локали пользователя без дополнительных усилий. В качестве альтернативы можно явно получить локали пользователя из navigator.language или navigator.languages и передать их форматтеру:
// Устанавливаем определенную локаль const formatter = new Intl.DateTimeFormat('nl-NL'); // Передаем список локалей, браузер возьмет первую из поддерживаемых const formatter = new Intl.DateTimeFormat(['en-US', 'en-GB']); // Используем браузерную локаль const formatter = new Intl.DateTimeFormat(); // Используем браузерную локаль с настройками const formatter = new Intl.DateTimeFormat(undefined, options); // Используем локаль пользователя const formatter = new Intl.DateTimeFormat(navigator.language); // Используем полный список языковых предпочтений пользователя // (браузер возьмет первую локаль из поддерживаемых) const formatter = new Intl.DateTimeFormat(navigator.languages);
navigator.languages — это массив, подобный ['en-US', 'en', 'nl'], поэтому он работает как список локалей, который можно передать непосредственно в форматтер.
В чем разница между языком и локалью? Локаль — это язык, на котором говорят в конкретной стране. Например, и в США, и в Великобритании говорят на английском, но у них разные правила форматирования дат и чисел. Поэтому у нас есть
en-USдля американского английского иen-GBдля британского. Если мы укажем простоen, браузер выберет локаль по умолчанию для этого языка, которая обычно является наиболее распространенной.
Начнем с API с наиболее понятными названиями: тех, что используются для форматирования дат и времени.
Intl.DateTimeFormat — это API для форматирования дат и времени:

В простейшей форме это выглядит так:
const formatter = new Intl.DateTimeFormat(); formatter.format(new Date()); // → "20.04.2026" (в ru-RU)
Стандартный вывод очень лаконичен. В большинстве случаев требуется указать стиль вывода. Самый простой способ — использовать dateStyle и timeStyle:
const formatter = new Intl.DateTimeFormat('fr-FR', { dateStyle: 'full', // 'full' | 'long' | 'medium' | 'short' timeStyle: 'short', // 'full' | 'long' | 'medium' | 'short' }); formatter.format(new Date()); // → "mercredi 8 avril 2026 à 09:19"
Нам не нужно знать, что «mercredi» по-французски означает среду, или что «avril» означает апрель, или иметь какие-то массивы дней и месяцев.
Если требуется больший контроль, можно точно указать, какие части даты/времени отображать и как их форматировать:

В выводе будут содержаться только отдельные части. Смешивание dateStyle/timeStyle с параметрами отдельных частей не допускается; можно использовать либо один, либо другой вариант, но не оба одновременно.
Есть еще несколько опций, которые стоит упомянуть:
const options = { hour12: true | false, // 12/24-часовой формат timeZone: 'UTC' | 'Europe/Amsterdam' | ..., // временная зона timeZoneName: 'short' | 'long', // "CET", "центральноевропейское время" и др. dayPeriod: 'narrow' | 'short' | 'long', // "утро", "AM" и др. };
Все форматтеры Intl, имеющие метод .format(), также имеют метод .formatToParts(). Вместо возврата одной большой строки, он возвращает массив объектов { type, value }. Это упрощает оборачивание отдельных частей в разметку для стилизации, например, выделение дня жирным шрифтом или добавление всплывающей подсказки к названию часового пояса:
const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeStyle: 'short', }); formatter.formatToParts(new Date('2026-04-07T16:45:00Z')); // → [ // { type: 'month', value: 'April' }, // { type: 'literal', value: ' ' }, // { type: 'day', value: '7' }, // { type: 'literal', value: ', ' }, // { type: 'year', value: '2026' }, // { type: 'literal', value: ' at ' }, // { type: 'hour', value: '4' }, // { type: 'literal', value: ':' }, // { type: 'minute', value: '45' }, // { type: 'literal', value: ' ' }, // { type: 'dayPeriod', value: 'PM' }, // ]
Intl.RelativeTimeFormat возвращает удобные для человека строки относительного времени, такие как «вчера» и «через 3 дня»:

В отличие от DateTimeFormat, он работает с числом и единицей измерения, а не с Date:
const relative = new Intl.RelativeTimeFormat('nl-NL', { numeric: 'auto', // 'auto' | 'always' }); relative.format(-3, 'month'); // → "3 maanden geleden" relative.format(1, 'day'); // → "morgen" relative.format(-1, 'day'); // → "gisteren"
С numeric: 'auto' форматтер использует такие слова, как «завтра» и «вчера», когда может. С numeric: 'always' он всегда использует число: «через 1 день», «1 день назад».
Поскольку разницу необходимо вычислять самостоятельно, вот вспомогательный шаблон для вычисления количества дней между двумя датами:
const date = new Date('2026-04-01T00:00:00Z'); const diffInMs = date - new Date(); const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24)); const relative = new Intl.RelativeTimeFormat('ru-RU', { numeric: 'auto' }); relative.format(diffInDays, 'day'); // → "через 11 дней" (зависит от того, когда вы читаете эту статью)
Даты измеряются в миллисекундах. Поэтому их можно преобразовать, разделив на нужную единицу измерения. Вместо очень большого числа, например, 86400000 миллисекунд в сутках, код будет проще читаться, если мы будем делать это пошагово: 1000 миллисекунд в секунде, 60 секунд в минуте, 60 минут в часе и 24 часа в сутках.
RelativeTimeFormat также имеет функцию .formatToParts(), которая разбивает вывод на части 'integer', 'literal' и 'unit'. Это позволяет выделить каждую часть и стилизовать ее отдельно:
const relative = new Intl.RelativeTimeFormat('en-US', { numeric: 'always' }); relative.formatToParts(-3, 'month'); // → [ // { type: 'integer', value: '3', unit: 'month' }, // { type: 'literal', value: ' months ago' }, // ]
Intl.DurationFormat форматирует продолжительность времени:

Это позволяет, например, выбрать между выводом в формате 2:05:30 или 2 часа, 5 минут, 30 секунд:
const duration = new Intl.DurationFormat('en-US', { style: 'long', // 'long' | 'short' | 'narrow' | 'digital' }); duration.format({ hours: 2, minutes: 5, seconds: 30 }); // → "2 hours, 5 minutes, 30 seconds" const clock = new Intl.DurationFormat('en-US', { style: 'digital' }); clock.format({ hours: 2, minutes: 5, seconds: 30 }); // → "2:05:30"
В то время как другие форматеры даты и времени используют параметры для управления отображением отдельных частей, DurationFormat по умолчанию отображает только те части, которые ему переданы. Таким образом, если переданы часы, минуты и секунды, он отобразит все три. Если переданы только минуты и секунды, он отобразит только их.
Однако можно принудительно отображать необходимые части через настройки *Display:
const options = { yearsDisplay: 'auto' | 'always', monthsDisplay: 'auto' | 'always', daysDisplay: 'auto' | 'always', hoursDisplay: 'auto' | 'always', minutesDisplay: 'auto' | 'always', secondsDisplay: 'auto' | 'always', millisecondsDisplay: 'auto' | 'always', microsecondsDisplay: 'auto' | 'always', nanosecondsDisplay: 'auto' | 'always', };
always означает принудительное отображение.
Дефолтным значением этих параметров почти всегда является auto, однако при установке style в значение digital, значением по умолчанию для часов, минут и секунд будет always.
Также можно задавать гораздо более точные значения длительности, вплоть до наносекунд, что полезно, например, для отображения результатов измерения производительности:
const perf = new Intl.DurationFormat('en-US', { style: 'narrow' }); perf.format({ milliseconds: 423, microseconds: 195 }); // → "423ms 195μs"
Intl.NumberFormat форматирует простые числа, валюты и единицы измерения:

Этот API решает разные задачи, которые лучше разделять.
Для обычных чисел, как правило, используется стиль decimal. Он обеспечивает группировку и десятичные разделители с учетом локали без дополнительных усилий:
const formatter = new Intl.NumberFormat('nl-NL'); formatter.format(1000000); // → "1.000.000" formatter.format(123456.789); // → "123.456,789"
Даже для простых чисел локаль имеет значение: в голландском языке для обозначения тысяч используется ., а для обозначения десятичных дробей — ,, в то время как в американском английском — наоборот.
Для отображения процентов используется style: 'percent':
const pct = new Intl.NumberFormat('fr-FR', { style: 'percent' }); pct.format(0.1); // → "10 %" pct.format(0.753); // → "75 %"
Универсальный форматтер чисел также поддерживает различные обозначения, что позволяет задавать компактные числа, например, 1M:
const compact = new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short', }); compact.format(1007800); // → "1M" compact.format(1534); // → "1.5K" // Для длинной версии используется 'long' вместо 'short' const compactLong = new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'long', }); compactLong.format(1007800); // → "1 million" compactLong.format(1534); // → "1.5 thousand"
Обратите внимание, как в компактной записи число округляется, чтобы соответствовать формату. 1 007 800 становится "1M", а 1 534 становится "1,5K". Для контроля округления можно использовать параметры дробных цифр или переключиться на значащие цифры.
Количество десятичных знаков можно регулировать с помощью параметров отображения дробных цифр или переключаться на значащие цифры для научных или измерительных данных:
// Всегда отображать ровно 2 десятичных числа const price = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); price.format(9.9); // → "9.90" price.format(9.999); // → "10.00" // До 3 значащих цифр const approx = new Intl.NumberFormat('en-US', { maximumSignificantDigits: 3, }); approx.format(123456); // → "123,000" approx.format(0.001234); // → "0.00123"
Важной особенностью Intl.NumberFormat является форматирование валют, которое даже в простейшем виде имеет немало различий между странами, например, стоит ли символ до или после числа, и неправильные настройки в UI легко могут создать впечатление непрофессионализма или ненадежности сайта:

Форматирование валюты — это не просто добавление символа перед числом. Положение символа, разделитель тысяч, десятичный разделитель и правила округления различаются в зависимости от локали, даже при использовании одной и той же валюты (см. разницу в форматировании евро в Нидерландах и Франции). Intl.NumberFormat обрабатывает все это за нас, не требуя от нас знания специфики разных локалей:
const moneyNL = new Intl.NumberFormat('nl-NL', { style: 'currency', currency: 'EUR', }); const moneyFr = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', }); moneyNL.format(123456.789); // → "€ 123.456,79" moneyFr.format(123456.789); // → "123 456,79 €"
Мы также можем настроить способ отображения самой валюты, например, показывать код валюты вместо символа или даже полное локализованное название валюты:
const options = { style: 'currency', currency: 'USD', currencyDisplay: 'name', // 'symbol' | 'narrowSymbol' | 'code' | 'name' currencySign: 'accounting', // 'standard' (по умолчанию) | 'accounting' }; new Intl.NumberFormat('nl-NL', options).format(123456.789); // → "123.456,79 Amerikaanse dollar"
Разница между symbol и narrowSymbol заключается в том, что narrowSymbol может быть «некорректным». Например, символом для долларов США является US$, потому что доллары используются и в других странах, и необходимо различать доллары США и, скажем, канадские доллары, которые обозначаются как CA$. narrowSymbol для обоих — просто $, который более компактен и подходит для работы в одной стране, но может вызывать путаницу при работе с международной аудиторией.
currencySign определяет способ отображения отрицательных денежных значений. Обычные люди ожидают знак минус перед числом, но бухгалтеры, будучи нонконформистами, предпочитают заключать отрицательные числа в скобки:
const standard = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', currencySign: 'standard', }); const accounting = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', currencySign: 'accounting', }); standard.format(-123.45); // → "-$123.45" accounting.format(-123.45); // → "($123.45)"
Стиль unit — одна из наименее используемых частей Intl.NumberFormat. Она форматирует число с указанием физической единицы измерения с учетом локали:

Поддерживаемые единицы измерения: длина, масса, температура, скорость, объем, размер цифрового блока и время. Эти значения можно получить, вызвав метод Intl.supportedValuesOf("unit").
const boiling = new Intl.NumberFormat('nl-NL', { style: 'unit', unit: 'celsius', unitDisplay: 'short', // 'short' | 'long' | 'narrow' }); boiling.format(100); // → "100°C" new Intl.NumberFormat('nl-NL', { style: 'unit', unit: 'celsius', unitDisplay: 'long', }).format(100); // → "100 graden Celsius"
Стиль unit также позволяет объединять любые две единицы измерения, когда необходимо отобразить, например, «мили в час» или «километры в час». Для этого используется нотация unit1-per-unit2:
const speed = new Intl.NumberFormat('nl-NL', { style: 'unit', unit: 'kilometer-per-hour', unitDisplay: 'short', // 'short' | 'long' | 'narrow' }); speed.format(100); // → "100 km/u" new Intl.NumberFormat('nl-NL', { style: 'unit', unit: 'kilometer-per-hour', unitDisplay: 'long', }).format(100); // → "100 kilometer per uur"
NumberFormat также имеет метод .formatToParts(). Это особенно полезно для валют, где может потребоваться выделить различные части числа и стилизовать их:
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }); money.formatToParts(1234.56); // → [ // { type: 'currency', value: '$' }, // { type: 'integer', value: '1' }, // { type: 'group', value: ',' }, // { type: 'integer', value: '234' }, // { type: 'decimal', value: '.' }, // { type: 'fraction', value: '56' }, // ]
Для работы с текстом на естественном языке существуют два API: один для форматирования списков, а другой для поиска форм множественного числа.
Intl.ListFormat преобразует массивы в списки на естественном языке:

При необходимости преобразования массива в удобочитаемый список используйте ListFormat вместо Array.join(', ). Списки, соединенные запятыми, обычно, отличаются от списков на естественном языке. При перечислении вещей мы не говорим «а, б, в», мы говорим «а, б и в». ListFormat обрабатывает это автоматически, нам не нужно использовать Array.reduce, например, и отслеживать последний элемент массива:
// ✗ Неестественный язык ['Apples', 'Oranges', 'Bananas'].join(', '); // → "Apples, Oranges, Bananas" // ✓ Естественный язык const formatter = new Intl.ListFormat('en-US', { type: 'conjunction' }); formatter.format(['Apples', 'Oranges', 'Bananas']); // → "Apples, Oranges, and Bananas"
Каждый тип списков предназначен для разных типов текста в UI.
Используйте conjunction, когда элементы связаны друг с другом, и подразумевается союз «и»:
new Intl.ListFormat('en', { type: 'conjunction' }).format(['Cowboy', 'Hat', 'Horse']); // → "Cowboy, Hat, and Horse"
Используйте disjunction, когда элементы являются альтернативами друг друга, и подразумевается союз «или»:
new Intl.ListFormat('en', { type: 'disjunction' }).format(['Cowboy', 'Hat', 'Horse']); // → "Cowboy, Hat, or Horse"
Используйте unit, когда нужен компактный список без явного союза:
new Intl.ListFormat('en', { type: 'unit' }).format(['Cowboy', 'Hat', 'Horse']); // → "Cowboy, Hat, Horse"
Параметр style ( 'long', 'short', 'narrow') влияет на способ отображения соединительных слов. В английском языке short применяет амперсанд: «Cowboy, Hat, & Horse».
В американском английском (en-US) conjunction применяет оксфордскую запятую («…Hat, and Horse»). Переключитесь на британский английский (en-GB), и оксфордская запятая исчезнет.
Intl.PluralRules указывает, какую форму множественного числа использовать для данного числа. В английском языке форма множественного числа — это «s» при переходе от «1 bird» к «2 birds», но в других языках это может быть сложнее.

Особенность PluralRules заключается в том, что, в отличие от других API форматирования, он фактически не выполняет форматирование, а лишь предоставляет согласованную классификацию форм множественного числа в языке.
Таким образом, для слова bird/birds будет указано, что для 1 птицы форма множественного числа — «one», а для 0, 2, 3 и т.д. — «other». Затем эту информацию можно использовать для отображения правильной формы слова в UI: в случае «one» ничего не добавляется, а в случае «other» добавляется «s»:
const plurals = new Intl.PluralRules('en-US'); plurals.select(0); // → "other" plurals.select(1); // → "one" plurals.select(2); // → "other" const count = 5; const label = `${count} bird${plurals.select(count) === 'other' ? 's' : ''}`; // → "5 birds"
См. пример использования результата PluralRules в русской локали в демо.
cardinal — это стандартная форма множественного числа, и по сути оно означает «считать». В английском языке всего две формы кардинального множественного числа: 'one' (для числа 1) и 'other' (для всего остального, включая 0). В других языках их больше. Например, в арабском языке шесть форм множественного числа: zero, one, two, few, many и other.
Существует также ordinal, используемый для ранжирования или упорядочивания вещей: «1-й», «2-й», «3-й», «4-й» и т.д. В английском языке формы множественного числа для порядковых числительных — one (для 1), two (для 2), few (для 3) и other (для всего остального). Это можно использовать для получения правильного суффикса для любого числа:
const rules = new Intl.PluralRules('en-US', { type: 'ordinal' }); const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' }; const ordinal = (n) => `${n}${suffixes[rules.select(n)]}`; ordinal(1); // → "1st" ordinal(2); // → "2nd" ordinal(3); // → "3rd" ordinal(4); // → "4th" ordinal(21); // → "21st"
Как вы можете видеть из этих двух примеров, наша задача — указать правильный суффикс или, в случае других языков, правильную словоформу.
Например, число 3 по-голландски — «drie», а «3rd» — «derde». Не так просто добавить суффикс, чтобы преобразовать «drie» в «derde», поэтому необходимо указывать полные формы слов для каждой категории множественного числа.
Intl.Segmenter разбивает текст на осмысленные сегменты:

Деление предложений на части может казаться простым, но быстро становится очень сложным, если учитывать все нюансы естественного языка. Например, как посчитать количество слов в предложении? Можно попробовать сделать это так:
'How many words are in this sentence?'.split(' ').length; // → 7 ✓
Разделение слов по пробелу с последующим вызовом геттера .length хорошо работает для английского и других западных языков, но оно основано на предположении, что слова разделяются пробелами. Это не универсальное правило в естественном языке. Например, здесь оно дает сбой:
'これは日本語のテキストです'.split(' ').length; // → 1 ✗
В японском языке между словами не используются пробелы. Поэтому разделение по пробелу не сработает.
Даже String.length для получения количества символов не всегда работает, как ожидается. Какова длина этого эмодзи?
'👨👩👧👦'.length; // → 11
Не 1, а 11! Дело в том, что .length считает не видимые символы, а кодовые единицы. 👨👩👧👦 представляет собой лигатуру из четырех отдельных эмодзи, соединенных невидимыми символами нулевой ширины. Таким образом, мы видим один эмодзи, а JavaScript видит одиннадцать кодовых единиц и .length возвращает 11.
Распознавание предложений тоже непростая задача. Поскольку форматированные числа, сокращения и усечения содержат знаки препинания, разделение по точке не всегда работает корректно:
`This article could be 100.000 words long if you don't look out. Do you really need that many examples?`.split('.').length; // → 3 ✗ // (разделение по точке происходит также в "100.000")
Разумеется, некоторые предложения могут заканчиваться вопросительным или восклицательным знаком, поэтому разделение предложений по точке в принципе не является надежным способом выделения предложений.
Intl.Segmenter корректно обрабатывает все эти кейсы и имеет три разных уровня детализации, так что мы можем получать символы, слова или предложения из текстовой строки.
'grapheme' (по умолчанию) — символы, воспринимаемые пользователем (то, что мы видим как один символ, даже если он состоит из нескольких кодовых точек):
const graphemes = new Intl.Segmenter('en', { granularity: 'grapheme' }); Array.from(graphemes.segment('👨👩👧👦')).length; // → 1 ✓ Array.from(graphemes.segment('café')).length; // → 4 ✓
'word' — слова, а также несловесные сегменты, такие как пробелы и знаки препинания. Каждый сегмент имеет логическое значение isWordLike, которое помогает отфильтровать несловесные сегменты:
const words = new Intl.Segmenter('ja-JP', { granularity: 'word' }); Array.from(words.segment('これは日本語のテキストです')).filter((s) => s.isWordLike).length; // → 6 ✓ (браузер знает границы японских слов)
'sentence' — полные предложения с сокращениями и числами:
const sentences = new Intl.Segmenter('en', { granularity: 'sentence' }); Array.from(sentences.segment('I went to the dr. smith yesterday. How are you?')).map((s) => s.segment); // → [ // "I went to the dr. smith yesterday. ", // "How are you?" // ]
Одно замечание: сегментатор обрабатывает dr. как сокращение, если за ним следует строчная буква, но если следующее слово написано с заглавной буквы (как в случае с именем собственным), оно может рассматриваться как граница предложения. Сегментатор умный, но не идеальный.
Метод segment() возвращает итерируемый объект, поэтому мы оборачиваем его в Array.from(). Каждый элемент представляет собой объект, содержащий как минимум:
segment: текст сегмента
index: где начинается сегмент в исходной строке
input: исходная строка
Для детализации 'word' элементы также содержат логическое свойство:
isWordLike: true, если сегмент является словом, а не пробелом или знаком препинания
См. статью, посвященную Segmenter API.
Intl.Collator сортирует строки с учетом локали:

Встроенная функция Array.sort() сортирует строки по их кодовым точкам Unicode, что не соответствует алфавитному порядку, ожидаемому людьми во многих языках:
['ñ', 'n', 'o'].sort(); // → ['n', 'o', 'ñ'] ✗ (ñ имеет более высокую кодовую точку, сортируется после z) const collator = new Intl.Collator('es'); // испанский ['ñ', 'n', 'o'].sort(collator.compare); // → ['n', 'ñ', 'o'] ✓ (ñ правильно размещается между n и o в испанском)
Это важно везде, где отображается индекс, каталог или любой другой отсортированный список.
Возможно, вы знаете, что существует метод String.prototype.localeCompare(), который также выполняет сравнение строк с учетом локали. Внутри он также использует Intl.Collator, но при каждом его вызове создается и уничтожается новый экземпляр Collator. Это довольно медленно (помните, создание форматтера — самая затратная часть), поэтому лучше настроить Collator один раз, а затем повторно использовать его метод compare:
// Работает, но медленно items.sort((a, b) => a.localeCompare(b)); // Лучше: создаем экземпляр и повторно используем его const collator = new Intl.Collator('en'); items.sort(collator.compare);
Intl.Collator имеет несколько полезных настроек. Самая важная из них — sensitivity (чувствительность), которая определяет, какие различия в символах учитываются:
// 'base': учитываются только основные различия символов ('a' ≠ 'b', но 'a' = 'á' = 'A') // 'accent': различаются основные буквы и диакритические знаки, но регистр не учитывается // 'case': различаются основные буквы и регистр, диакритические знаки не учитываются // 'variant': все различия принимаются во внимание (дефолтное значение для сортировки) const collator = new Intl.Collator('en', { sensitivity: 'base' }); collator.compare('a', 'á'); // → 0 (считаются одинаковыми)
Другие полезные опции:
usage: 'sort' (по умолчанию) или 'search'. Используйте 'sort' для сортировки списков. Для фильтрации списков на основе логики, специфичной для языка, можно использовать search и проверить, является ли результат 0, для поиска совпадения. Для сортировщика cafe, café и CAFE считаются равными с sensitivity: 'base', поэтому использование search сделает поиск более естественным для пользователей в языках с диакритическими знаками и различиями в регистре
caseFirst: 'upper', 'lower' или 'false' (по умолчанию). Определяет, какой регистр будет первым в сортировке, если разница в регистре имеет значение
ignorePunctuation: true или false (по умолчанию). Если true, различия в пунктуации игнорируются при сравнении
collation: запрашивает определенный вариант сортировки. Распространенные значения:
'phonebk': сортировка в стиле телефонной книги, используемая в немецком языке, где ä сортируется как ae, ö как oe, ü как ue
'pinyin': порядок пиньинь для латинских и CJK символов, распространенный в китайском языке
'stroke': порядок количества черт в иероглифах CJK, используемый в традиционном китайском языке
'trad': традиционный порядок, например, обработка букв ch и ll как отдельных букв в испанском языке
'emoji': рекомендуемый порядок расположения символов эмодзи
'standard': стандартный порядок символов в локали (по умолчанию)
Полный список поддерживаемых значений можно получить с помощью метода Intl.supportedValuesOf('collation').
Настройка numeric заслуживает отдельного внимания. По умолчанию, сортировка строк выполняется посимвольно. Это означает, что 10 располагается перед 9, поскольку 1 меньше 9. Это почти никогда не соответствует нашим ожиданиям при сортировке чего-либо, содержащего числа.
const files = ['chapter10.txt', 'chapter9.txt', 'chapter2.txt', 'chapter1.txt']; // Дефолтная посимвольная сортировка files.sort(); // → ['chapter1.txt', 'chapter10.txt', 'chapter2.txt', 'chapter9.txt'] ✗ // Числовая сортировка const collator = new Intl.Collator('en', { numeric: true }); files.sort(collator.compare); // → ['chapter1.txt', 'chapter2.txt', 'chapter9.txt', 'chapter10.txt'] ✓
Это может быть очень полезным с названиями файлов, номерами версий и любым списком, в котором пользователи используют числа в названиях: image1, image2, image10 и т.п. Без numeric: true image10 будет размещена сразу после image1, перед image2. С numeric: true сортировка будет правильной.
На первый взгляд может показаться, что Intl — это просто набор несвязанных API. Он работает с датами, валютами, а также сортирует списки и считает слова? Вот это API! Если же смотреть только на названия конструкторов, может показаться, что запомнить все это очень сложно.
Но Intl основан на одной ключевой идее: у нас есть определенный тип данных, и мы хотим легко отформатировать его на естественном языке. Выбираем локаль. Выбираем несколько параметров. Создаем форматтер, компаратор или сегментатор. Затем используем его повторно с нашими данными. Независимо от того, что форматируется: даты, числа, валюты, списки, множественное число, слова или отсортированные строки, структура API остается, примерно, одинаковой.
Понимание этой общей основы значительно упрощает понимание и использование всего API, особенно при решении задач форматирования строк. Это означает меньше написанного вручную кода форматирования, меньше зависимостей и меньше скрытых ошибок, когда значение кажется нам правильным, но сбивает с толку или является неправильным для пользователя в другой локали.
Браузер уже знает, как все это работает, и проделал за нас всю сложную работу. Intl предоставляет возможность напрямую использовать эти знания.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
