Браузерная поддержка и полифилы

Объект Intl входит в стандарт ECMAScript Internationalization API и поддерживается большинством современных браузеров. Однако уровень поддержки зависит от конкретного класса, версии браузера и используемого движка JavaScript.

Базовая интернационализация появилась значительно раньше расширенных возможностей форматирования. Поэтому в старых браузерах часто доступны только:

  • Intl.NumberFormat
  • Intl.DateTimeFormat
  • Intl.Collator

Более новые API могут отсутствовать полностью или поддерживаться частично:

  • Intl.RelativeTimeFormat
  • Intl.ListFormat
  • Intl.DisplayNames
  • Intl.Locale
  • Intl.Segmenter
  • Intl.DurationFormat

Проверка поддержки Intl

Наличие самого 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);

Результат содержит только поддерживаемые локали.


Поддержка в современных браузерах

Google Chrome

Движок V8 обеспечивает практически полную поддержку современного Intl API.

Поддерживаются:

  • форматирование дат и чисел;
  • множественные календари;
  • plural rules;
  • relative time;
  • list formatting;
  • locale negotiation;
  • segmenter API.

Новые возможности появляются достаточно быстро после утверждения спецификации TC39.


Mozilla Firefox

Firefox использует движок SpiderMonkey и традиционно обладает хорошей поддержкой интернационализации.

Особенности:

  • качественная работа с ICU;
  • широкая поддержка локалей;
  • стабильная реализация Intl.PluralRules;
  • ранняя поддержка некоторых экспериментальных возможностей.

Safari

Safari использует JavaScriptCore.

Исторически именно Safari чаще других браузеров отставал в реализации новых возможностей Intl API.

Наиболее частые проблемы старых версий Safari:

  • отсутствие Intl.RelativeTimeFormat;
  • неполная поддержка Intl.ListFormat;
  • ошибки в форматировании некоторых локалей;
  • ограниченное количество ICU-данных.

Особенно заметны различия в Safari iOS старых поколений.


Microsoft Edge

Современный Edge основан на Chromium, поэтому поддержка практически идентична Google Chrome.

Старый EdgeHTML имел заметные ограничения:

  • частичную поддержку локалей;
  • проблемы с NumberFormat;
  • отсутствие новых API.

Поддержка в Node.js

Node.js также использует Intl API, но поддержка зависит от сборки ICU.

Существует несколько режимов:

Режим Описание
none Intl полностью отключён
small-icu только английская локаль
full-icu полная поддержка локалей

Проверка текущей локали:

console.log(
    Intl.DateTimeFormat().resolvedOptions().locale
);

Проверка поддержки русского языка:

console.log(
    Intl.DateTimeFormat.supportedLocalesOf(["ru"])
);

ICU и его роль

Большинство реализаций Intl основано на библиотеке ICU (International Components for Unicode).

ICU предоставляет:

  • данные локалей;
  • правила форматирования;
  • правила сортировки;
  • plural rules;
  • информацию о календарях;
  • сведения о часовых поясах.

Без ICU полноценная интернационализация невозможна.


Различия между браузерами

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

Пример:

new Intl.NumberFormat("fr-FR").format(1000);

В одном браузере:

1 000

В другом:

1 000

Разница связана с:

  • версиями ICU;
  • Unicode CLDR;
  • реализацией движка;
  • обновлением языковых данных.

Проблемы старых браузеров

Internet Explorer

IE поддерживает только ограниченную часть Intl API.

Типичные ограничения:

  • отсутствие большинства современных классов;
  • неполные locale data;
  • ошибки форматирования;
  • проблемы с timezone.

Пример неподдерживаемого API:

new Intl.RelativeTimeFormat("ru");

В IE вызовет ошибку:

Intl.RelativeTimeFormat is undefined

Android Browser

Старые Android Browser имели крайне ограниченную поддержку интернационализации.

Проблемы:

  • отсутствие Intl;
  • неполный ICU;
  • нестабильная работа локалей.

Feature Detection вместо User-Agent

Проверка возможностей должна строиться не на User-Agent, а на наличии API.

Неправильно:

if (navigator.userAgent.includes("Chrome")) {
    // ...
}

Правильно:

if (Intl.ListFormat) {
    // ...
}

Такой подход:

  • устойчив к изменениям браузеров;
  • корректно работает в embedded runtime;
  • совместим с future-proof архитектурой.

Полифилы Intl API

Полифил — это реализация отсутствующего API средствами JavaScript.

Полифилы позволяют:

  • использовать современные возможности в старых браузерах;
  • унифицировать поведение;
  • уменьшать различия между платформами.

Когда необходим полифил

Полифил нужен, если:

  • целевая аудитория использует старые браузеры;
  • требуется поддержка IE11;
  • необходимо единообразное форматирование;
  • используется новый Intl API.

Основные полифилы

FormatJS

Один из наиболее популярных наборов полифилов.

Поддерживает:

  • Intl.NumberFormat
  • Intl.DateTimeFormat
  • Intl.RelativeTimeFormat
  • Intl.ListFormat
  • Intl.DisplayNames
  • Intl.PluralRules
  • Intl.Locale

Особенности:

  • модульная архитектура;
  • поддержка tree-shaking;
  • совместимость с React Intl;
  • актуальные ICU-данные.

Intl.js

Старый полифил для базового Intl API.

Обычно используется для:

  • Internet Explorer;
  • старых мобильных браузеров.

Недостатки:

  • большой размер;
  • устаревшая архитектура;
  • ограниченная поддержка новых API.

Polyfill.io

Сервис динамической загрузки полифилов.

Позволяет подключать только необходимые возможности:

<script src="https://polyfill.io/v3/polyfill.min.js?features=Intl"></script>

Преимущества:

  • автоматическое определение браузера;
  • уменьшение объёма загрузки;
  • CDN-кэширование.

Подключение полифилов

Через npm

Установка:

npm install @formatjs/intl-relativetimeformat

Подключение:

import "@formatjs/intl-relativetimeformat/polyfill";
import "@formatjs/intl-relativetimeformat/locale-data/ru";

Через CDN

<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 и lazy loading

Полифилы часто имеют большой размер.

Dynamic import позволяет:

  • уменьшить initial bundle;
  • загружать код по требованию;
  • ускорить старт приложения.

Пример:

if (!Intl.ListFormat) {
    import("./polyfills/list-format.js");
}

Размер полифилов

Intl-полифилы могут занимать значительный объём из-за locale data.

При использовании множества языков размер может превышать сотни килобайт.

Основные источники объёма:

  • ICU data;
  • plural rules;
  • timezone data;
  • calendars.

Оптимизация locale data

Часто нет необходимости подключать все локали.

Неэффективно:

import "@formatjs/intl-relativetimeformat/locale-data/*";

Лучше:

import "@formatjs/intl-relativetimeformat/locale-data/ru";
import "@formatjs/intl-relativetimeformat/locale-data/en";

Babel и автоматическое подключение полифилов

Babel может автоматически подключать необходимые полифилы.

Пример конфигурации:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "usage",
                "corejs": 3
            }
        ]
    ]
}

Однако core-js покрывает только часть Intl API.

Для расширенной интернационализации обычно требуются отдельные пакеты FormatJS.


Webpack и разделение бандлов

Intl-полифилы часто выносят в отдельный chunk.

Пример:

if (!Intl.DisplayNames) {
    import(
        /* webpackChunkName: "intl-displaynames" */
        "./polyfills/displaynames"
    );
}

Преимущества:

  • кеширование;
  • уменьшение главного bundle;
  • независимое обновление.

Проблемы timezone

Часовые пояса — одна из наиболее сложных частей Intl.

Старые браузеры могут:

  • использовать устаревшую базу timezone;
  • не поддерживать отдельные зоны;
  • неверно учитывать DST.

Проверка:

console.log(
    Intl.DateTimeFormat().resolvedOptions().timeZone
);

Проверка поддержки часового пояса

try {
    new Intl.DateTimeFormat("ru", {
        timeZone: "Asia/Almaty"
    });

    console.log("Поддерживается");
} catch {
    console.log("Не поддерживается");
}

Проблемы plural rules

Некоторые старые реализации Intl неверно обрабатывают сложные языки.

Особенно это касается:

  • русского;
  • арабского;
  • польского;
  • украинского.

Пример:

new Intl.PluralRules("ru").select(5);

Корректный результат:

many

Частичная поддержка API

Иногда браузер поддерживает API лишь частично.

Пример:

Intl.DateTimeFormat

может существовать, но не поддерживать:

  • dateStyle;
  • timeStyle;
  • calendar;
  • numberingSystem.

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


Проверка options support

const supportsDateStyle = (() => {
    try {
        new Intl.DateTimeFormat("ru", {
            dateStyle: "long"
        });

        return true;
    } catch {
        return false;
    }
})();

Graceful Degradation

Если современное API недоступно, приложение должно продолжать работать.

Пример:

function formatDate(date) {
    if (Intl.DateTimeFormat) {
        return new Intl.DateTimeFormat("ru").format(date);
    }

    return date.toLocaleDateString();
}

Progressive Enhancement

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

Базовая функциональность остаётся работоспособной во всех окружениях.

Такой подход:

  • снижает количество ошибок;
  • уменьшает зависимость от полифилов;
  • улучшает производительность.

Совместимость мобильных браузеров

На мобильных устройствах особенно важны:

  • размер полифилов;
  • скорость инициализации;
  • объём locale data.

Старые Android WebView могут иметь крайне ограниченную поддержку Intl.

Особенно проблемны:

  • встроенные браузеры приложений;
  • старые Smart TV;
  • embedded webview.

Тестирование Intl API

Интернационализацию необходимо тестировать в разных окружениях.

Минимальный набор:

  • Chrome;
  • Firefox;
  • Safari;
  • мобильный Safari;
  • Android Chrome;
  • Node.js.

Автоматическое тестирование

Пример проверки:

describe("Intl support", () => {
    test("RelativeTimeFormat exists", () => {
        expect(Intl.RelativeTimeFormat)
            .toBeDefined();
    });
});

Snapshot-тесты форматирования

Intl API зависит от ICU и локалей, поэтому snapshot-тесты могут различаться между средами.

Например:

expect(
    new Intl.NumberFormat("fr").format(1000)
).toMatchSnapshot();

Может давать разные результаты на CI и локальной машине.


Browserlist и целевые браузеры

Современные инструменты сборки используют Browserlist.

Пример:

> 0.5%
last 2 versions
not dead

На основе Browserlist:

  • Babel определяет необходимые полифилы;
  • bundler оптимизирует код;
  • build-система выбирает target environments.

Baseline и Intl

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

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


Современные рекомендации

На практике чаще всего используются следующие подходы:

Для современных приложений

  • reliance на native Intl;
  • selective polyfills;
  • dynamic imports.

Для enterprise-проектов

  • полные ICU data;
  • поддержка старых браузеров;
  • централизованная система локализации.

Для мобильных приложений

  • минимальный набор locale data;
  • lazy loading;
  • сокращение размера бандла.

Наиболее проблемные API

На практике чаще всего требуют полифилов:

API Причина
Intl.RelativeTimeFormat позднее появление
Intl.DisplayNames слабая поддержка Safari
Intl.ListFormat отсутствует в старых браузерах
Intl.Segmenter очень новая спецификация
Intl.DurationFormat ограниченная поддержка

Проверка нескольких API одновременно

const missingFeatures = [
    "RelativeTimeFormat",
    "ListFormat",
    "DisplayNames"
].filter(name => !(name in Intl));

console.log(missingFeatures);

Стратегии загрузки полифилов

Полная загрузка

Все полифилы подключаются сразу.

Плюсы:

  • простая архитектура;
  • предсказуемость.

Минусы:

  • большой bundle;
  • медленный старт.

Условная загрузка

Полифилы загружаются только при необходимости.

Плюсы:

  • оптимальная производительность;
  • меньше сетевого трафика.

Минусы:

  • более сложная архитектура;
  • необходимость feature detection.

Server-side polyfill injection

Сервер определяет браузер и отправляет нужный набор полифилов.

Подход используется в:

  • enterprise-системах;
  • крупных SPA;
  • CDN-инфраструктуре.

Intl API и ECMAScript proposals

Некоторые возможности 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"
        );
    }
}

Подход polyfill-first

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

Причины:

  • единообразное поведение;
  • одинаковые ICU-данные;
  • воспроизводимость тестов.

Недостаток — увеличение размера приложения.


Подход native-first

Современная стратегия:

  1. использовать встроенный Intl;
  2. проверять поддержку;
  3. догружать только отсутствующие возможности.

Именно этот подход считается наиболее эффективным для современных веб-приложений.