Объект Intl входит в стандарт ECMAScript
Internationalization API и поддерживается большинством современных
браузеров. Однако уровень поддержки зависит от конкретного класса,
версии браузера и используемого движка JavaScript.
Базовая интернационализация появилась значительно раньше расширенных возможностей форматирования. Поэтому в старых браузерах часто доступны только:
Intl.NumberFormatIntl.DateTimeFormatIntl.CollatorБолее новые API могут отсутствовать полностью или поддерживаться частично:
Intl.RelativeTimeFormatIntl.ListFormatIntl.DisplayNamesIntl.LocaleIntl.SegmenterIntl.DurationFormatНаличие самого API проверяется через глобальный объект
Intl:
if (typeof Intl !== "undefined") {
console.log("Intl поддерживается");
}
Проверка конкретного конструктора:
if (Intl.RelativeTimeFormat) {
console.log("RelativeTimeFormat доступен");
}
Проверка поддержки локали:
const supported = Intl.DateTimeFormat.supportedLocalesOf([
"ru",
"fr",
"ja"
]);
console.log(supported);
Результат содержит только поддерживаемые локали.
Движок V8 обеспечивает практически полную поддержку современного Intl API.
Поддерживаются:
Новые возможности появляются достаточно быстро после утверждения спецификации TC39.
Firefox использует движок SpiderMonkey и традиционно обладает хорошей поддержкой интернационализации.
Особенности:
Intl.PluralRules;Safari использует JavaScriptCore.
Исторически именно Safari чаще других браузеров отставал в реализации новых возможностей Intl API.
Наиболее частые проблемы старых версий Safari:
Intl.RelativeTimeFormat;Intl.ListFormat;Особенно заметны различия в Safari iOS старых поколений.
Современный Edge основан на Chromium, поэтому поддержка практически идентична Google Chrome.
Старый EdgeHTML имел заметные ограничения:
NumberFormat;Node.js также использует Intl API, но поддержка зависит от сборки ICU.
Существует несколько режимов:
| Режим | Описание |
|---|---|
| none | Intl полностью отключён |
| small-icu | только английская локаль |
| full-icu | полная поддержка локалей |
Проверка текущей локали:
console.log(
Intl.DateTimeFormat().resolvedOptions().locale
);
Проверка поддержки русского языка:
console.log(
Intl.DateTimeFormat.supportedLocalesOf(["ru"])
);
Большинство реализаций Intl основано на библиотеке ICU (International Components for Unicode).
ICU предоставляет:
Без ICU полноценная интернационализация невозможна.
Даже при наличии одинакового API результаты форматирования могут отличаться.
Пример:
new Intl.NumberFormat("fr-FR").format(1000);
В одном браузере:
1 000
В другом:
1 000
Разница связана с:
IE поддерживает только ограниченную часть Intl API.
Типичные ограничения:
Пример неподдерживаемого API:
new Intl.RelativeTimeFormat("ru");
В IE вызовет ошибку:
Intl.RelativeTimeFormat is undefined
Старые Android Browser имели крайне ограниченную поддержку интернационализации.
Проблемы:
Intl;Проверка возможностей должна строиться не на User-Agent, а на наличии API.
Неправильно:
if (navigator.userAgent.includes("Chrome")) {
// ...
}
Правильно:
if (Intl.ListFormat) {
// ...
}
Такой подход:
Полифил — это реализация отсутствующего API средствами JavaScript.
Полифилы позволяют:
Полифил нужен, если:
Один из наиболее популярных наборов полифилов.
Поддерживает:
Intl.NumberFormatIntl.DateTimeFormatIntl.RelativeTimeFormatIntl.ListFormatIntl.DisplayNamesIntl.PluralRulesIntl.LocaleОсобенности:
Старый полифил для базового Intl API.
Обычно используется для:
Недостатки:
Сервис динамической загрузки полифилов.
Позволяет подключать только необходимые возможности:
<script src="https://polyfill.io/v3/polyfill.min.js?features=Intl"></script>
Преимущества:
Установка:
npm install @formatjs/intl-relativetimeformat
Подключение:
import "@formatjs/intl-relativetimeformat/polyfill";
import "@formatjs/intl-relativetimeformat/locale-data/ru";
<script src="https://unpkg.com/@formatjs/intl-relativetimeformat/polyfill.js"></script>
Наиболее эффективный подход — загружать полифил только при отсутствии API.
Пример:
async function loadPolyfill() {
if (!Intl.RelativeTimeFormat) {
await import(
"@formatjs/intl-relativetimeformat/polyfill"
);
await import(
"@formatjs/intl-relativetimeformat/locale-data/ru"
);
}
}
Полифилы часто имеют большой размер.
Dynamic import позволяет:
Пример:
if (!Intl.ListFormat) {
import("./polyfills/list-format.js");
}
Intl-полифилы могут занимать значительный объём из-за locale data.
При использовании множества языков размер может превышать сотни килобайт.
Основные источники объёма:
Часто нет необходимости подключать все локали.
Неэффективно:
import "@formatjs/intl-relativetimeformat/locale-data/*";
Лучше:
import "@formatjs/intl-relativetimeformat/locale-data/ru";
import "@formatjs/intl-relativetimeformat/locale-data/en";
Babel может автоматически подключать необходимые полифилы.
Пример конфигурации:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
Однако core-js покрывает только часть Intl API.
Для расширенной интернационализации обычно требуются отдельные пакеты FormatJS.
Intl-полифилы часто выносят в отдельный chunk.
Пример:
if (!Intl.DisplayNames) {
import(
/* webpackChunkName: "intl-displaynames" */
"./polyfills/displaynames"
);
}
Преимущества:
Часовые пояса — одна из наиболее сложных частей Intl.
Старые браузеры могут:
Проверка:
console.log(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
try {
new Intl.DateTimeFormat("ru", {
timeZone: "Asia/Almaty"
});
console.log("Поддерживается");
} catch {
console.log("Не поддерживается");
}
Некоторые старые реализации Intl неверно обрабатывают сложные языки.
Особенно это касается:
Пример:
new Intl.PluralRules("ru").select(5);
Корректный результат:
many
Иногда браузер поддерживает API лишь частично.
Пример:
Intl.DateTimeFormat
может существовать, но не поддерживать:
dateStyle;timeStyle;calendar;numberingSystem.Поэтому важно проверять не только наличие конструктора, но и конкретных возможностей.
const supportsDateStyle = (() => {
try {
new Intl.DateTimeFormat("ru", {
dateStyle: "long"
});
return true;
} catch {
return false;
}
})();
Если современное API недоступно, приложение должно продолжать работать.
Пример:
function formatDate(date) {
if (Intl.DateTimeFormat) {
return new Intl.DateTimeFormat("ru").format(date);
}
return date.toLocaleDateString();
}
Современные возможности подключаются только там, где они доступны.
Базовая функциональность остаётся работоспособной во всех окружениях.
Такой подход:
На мобильных устройствах особенно важны:
Старые Android WebView могут иметь крайне ограниченную поддержку Intl.
Особенно проблемны:
Интернационализацию необходимо тестировать в разных окружениях.
Минимальный набор:
Пример проверки:
describe("Intl support", () => {
test("RelativeTimeFormat exists", () => {
expect(Intl.RelativeTimeFormat)
.toBeDefined();
});
});
Intl API зависит от ICU и локалей, поэтому snapshot-тесты могут различаться между средами.
Например:
expect(
new Intl.NumberFormat("fr").format(1000)
).toMatchSnapshot();
Может давать разные результаты на CI и локальной машине.
Современные инструменты сборки используют Browserlist.
Пример:
> 0.5%
last 2 versions
not dead
На основе Browserlist:
Современная концепция Baseline позволяет ориентироваться на набор возможностей, гарантированно поддерживаемых большинством браузеров.
Для Intl это особенно важно, поскольку многие API были стандартизированы сравнительно недавно.
На практике чаще всего используются следующие подходы:
На практике чаще всего требуют полифилов:
| API | Причина |
|---|---|
Intl.RelativeTimeFormat |
позднее появление |
Intl.DisplayNames |
слабая поддержка Safari |
Intl.ListFormat |
отсутствует в старых браузерах |
Intl.Segmenter |
очень новая спецификация |
Intl.DurationFormat |
ограниченная поддержка |
const missingFeatures = [
"RelativeTimeFormat",
"ListFormat",
"DisplayNames"
].filter(name => !(name in Intl));
console.log(missingFeatures);
Все полифилы подключаются сразу.
Плюсы:
Минусы:
Полифилы загружаются только при необходимости.
Плюсы:
Минусы:
Сервер определяет браузер и отправляет нужный набор полифилов.
Подход используется в:
Некоторые возможности Intl находятся на стадии proposal.
Поэтому:
async function setupIntl() {
if (!Intl.RelativeTimeFormat) {
await import(
"@formatjs/intl-relativetimeformat/polyfill"
);
await import(
"@formatjs/intl-relativetimeformat/locale-data/ru"
);
}
if (!Intl.ListFormat) {
await import(
"@formatjs/intl-listformat/polyfill"
);
await import(
"@formatjs/intl-listformat/locale-data/ru"
);
}
}
Некоторые проекты подключают полифилы всегда, даже в современных браузерах.
Причины:
Недостаток — увеличение размера приложения.
Современная стратегия:
Именно этот подход считается наиболее эффективным для современных веб-приложений.