Когда нужны полифиллы

Библиотека FormatJS активно использует стандарт ECMAScript Internationalization API (Intl). Все механизмы форматирования дат, чисел, валют, относительного времени и сообщений опираются именно на него.

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

  • старых версий Safari;
  • Internet Explorer;
  • старых Android WebView;
  • некоторых версий Node.js;
  • встроенных браузеров мобильных приложений;
  • минимальных JavaScript-движков без ICU-данных.

В таких случаях FormatJS не может корректно работать без дополнительных полифиллов.


Что такое полифилл

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

Если окружение не поддерживает определённый объект Intl, полифилл добавляет недостающий функционал.

Например:

Intl.RelativeTimeFormat

Если браузер не знает такой конструктор, код:

new Intl.RelativeTimeFormat()

завершится ошибкой:

Intl.RelativeTimeFormat is not a constructor

После подключения полифилла API становится доступным:

import '@formatjs/intl-relativetimeformat/polyfill'

Какие возможности чаще всего требуют полифиллов

Intl.PluralRules

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

Пример:

new Intl.PluralRules('ru').select(5)

Результат:

many

Необходим для ICU MessageFormat:

{count, plural,
  one {# файл}
  few {# файла}
  many {# файлов}
}

Без PluralRules pluralization работать не будет.


Intl.RelativeTimeFormat

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

Пример:

new Intl.RelativeTimeFormat('ru').format(-3, 'day')

Результат:

3 дня назад

Активно применяется в интерфейсах:

  • чаты;
  • уведомления;
  • социальные сети;
  • таймлайны;
  • системы логирования.

Intl.DateTimeFormat

Форматирование дат и времени.

Пример:

new Intl.DateTimeFormat('ru-RU', {
  dateStyle: 'long'
}).format(new Date())

Некоторые старые браузеры поддерживают только базовую версию API и не умеют:

  • dateStyle;
  • timeStyle;
  • formatRange;
  • календарные системы;
  • тайм-зоны.

Intl.NumberFormat

Форматирование чисел и валют.

Пример:

new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'KZT'
}).format(1000)

Результат:

1 000,00 ₸

Старые движки могут не поддерживать:

  • компактную запись;
  • единицы измерения;
  • диапазоны;
  • некоторые валюты;
  • локализационные данные.

Intl.DisplayNames

Используется для локализованных названий языков, регионов и валют.

Пример:

new Intl.DisplayNames(['ru'], {
  type: 'region'
}).of('KZ')

Результат:

Казахстан

Поддержка появилась сравнительно недавно, поэтому полифилл требуется часто.


Intl.ListFormat

Форматирование списков.

Пример:

new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
}).format(['React', 'Vue', 'Angular'])

Результат:

React, Vue и Angular

Почему полифиллы в FormatJS разделены по пакетам

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

Например:

@formatjs/intl-pluralrules
@formatjs/intl-relativetimeformat
@formatjs/intl-listformat

Такой подход позволяет:

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

Установка полифиллов

Установка PluralRules

npm install @formatjs/intl-pluralrules

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

import '@formatjs/intl-pluralrules/polyfill'

Локали:

import '@formatjs/intl-pluralrules/locale-data/ru'
import '@formatjs/intl-pluralrules/locale-data/en'

Установка RelativeTimeFormat

npm install @formatjs/intl-relativetimeformat

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

import '@formatjs/intl-relativetimeformat/polyfill'

Локали:

import '@formatjs/intl-relativetimeformat/locale-data/ru'

Установка DisplayNames

npm install @formatjs/intl-displaynames

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

import '@formatjs/intl-displaynames/polyfill'
import '@formatjs/intl-displaynames/locale-data/ru'

Почему важны locale-data

Сам полифилл содержит только реализацию API.

Локализационные данные подключаются отдельно.

Например:

import '@formatjs/intl-relativetimeformat/locale-data/ru'

Без locale-data API может существовать, но не знать конкретную локаль.

Типичная ошибка:

Missing locale data for locale: "ru"

Проверка поддержки перед подключением

Подключать полифилл всегда необязательно. Обычно выполняется проверка поддержки.

Пример:

if (!Intl.RelativeTimeFormat) {
  await import('@formatjs/intl-relativetimeformat/polyfill')
}

После этого загружаются locale-data:

await import('@formatjs/intl-relativetimeformat/locale-data/ru')

Такой подход уменьшает размер первоначальной загрузки.


Conditional Polyfills

Наиболее эффективная стратегия — динамическая загрузка.

Пример:

async function setupIntl() {
  if (!Intl.PluralRules) {
    await import('@formatjs/intl-pluralrules/polyfill')
    await import('@formatjs/intl-pluralrules/locale-data/ru')
  }

  if (!Intl.RelativeTimeFormat) {
    await import('@formatjs/intl-relativetimeformat/polyfill')
    await import('@formatjs/intl-relativetimeformat/locale-data/ru')
  }
}

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

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

Использование polyfill-force

Некоторые браузеры имеют частичную или некорректную реализацию Intl.

В таких случаях обычной проверки недостаточно.

FormatJS предоставляет режим принудительной замены:

import '@formatjs/intl-relativetimeformat/polyfill-force'

Этот вариант:

  • игнорирует встроенную реализацию;
  • заменяет её полностью;
  • гарантирует одинаковое поведение.

Когда нужен polyfill-force

Некорректная реализация браузера

Иногда API существует, но работает с ошибками.

Например:

  • неправильные plural rules;
  • ошибки тайм-зон;
  • отсутствие некоторых опций;
  • несовместимость ICU-форматов.

Старый Safari

Некоторые версии Safari имеют неполную поддержку:

  • Intl.DisplayNames;
  • Intl.ListFormat;
  • Intl.RelativeTimeFormat.

Встроенные WebView

Android WebView может содержать урезанный Intl.


Полифиллы и Node.js

В Node.js проблема обычно связана с ICU-данными.

Проверка:

Intl.DateTimeFormat.supportedLocalesOf(['ru'])

Если массив пустой:

[]

значит текущая сборка Node.js не содержит полных данных локализации.


Full ICU в Node.js

Для полноценной интернационализации нужен Full ICU.

Проверка:

node -p process.versions.icu

Установка full-icu

npm install full-icu

Запуск:

NODE_ICU_DATA=node_modules/full-icu node app.js

React-приложения и полифиллы

В проектах с React полифиллы обычно подключаются до рендера приложения.

Пример:

import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/ru'

import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/ru'

import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))

Отдельный bootstrap-файл

Распространённый подход — выделение отдельного файла инициализации.

intl-setup.js

import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/ru'

import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/ru'

index.js

import './intl-setup'
import './app'

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

  • централизованная настройка;
  • удобство поддержки;
  • изоляция инфраструктурного кода.

Полифиллы и серверный рендеринг

При SSR важно, чтобы:

  • сервер;
  • браузер;
  • hydration

использовали одинаковые правила форматирования.

Иначе возможны ошибки:

Text content did not match

Проблема различий локалей

Например:

На сервере:

1,000.50

В браузере:

1 000,50

Это приводит к несовпадению HTML.


Решение для SSR

Необходимо:

  1. Подключать одинаковые полифиллы.
  2. Использовать одинаковые locale-data.
  3. Проверять ICU в Node.js.
  4. Синхронизировать locale между сервером и клиентом.

Browserlist и необходимость полифиллов

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

Пример:

> 0.5%
last 2 versions
not dead

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


Как определить необходимость полифилла

Через Can I Use

Поддержка API:

  • Intl.RelativeTimeFormat
  • Intl.DisplayNames
  • Intl.ListFormat

существенно различается между браузерами.


Через telemetry

Практика крупных проектов:

  • анализ user-agent;
  • сбор статистики браузеров;
  • постепенное удаление старых полифиллов.

Стоимость полифиллов

Полифиллы увеличивают:

  • размер JavaScript;
  • время парсинга;
  • memory footprint;
  • startup time.

Особенно тяжёлыми бывают locale-data.


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

Плохой вариант:

import '@formatjs/intl-relativetimeformat/locale-data/*'

Хороший вариант:

import '@formatjs/intl-relativetimeformat/locale-data/ru'
import '@formatjs/intl-relativetimeformat/locale-data/en'

Lazy Loading локалей

При переключении языка locale-data можно загружать динамически.

Пример:

async function loadLocale(locale) {
  await import(
    `@formatjs/intl-relativetimeformat/locale-data/${locale}`
  )
}

Автоматическая загрузка полифиллов

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

@formatjs/intl

Он помогает централизовать работу с интернационализацией.

Однако в production-проектах чаще используется ручное управление для оптимизации bundle size.


Типичные ошибки

Отсутствуют locale-data

Ошибка:

Missing locale data

Причина:

import '@formatjs/intl-pluralrules/polyfill'

без:

import '@formatjs/intl-pluralrules/locale-data/ru'

Полифилл загружен слишком поздно

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

renderApp()

import('@formatjs/intl-relativetimeformat/polyfill')

Код приложения уже успеет выполниться раньше загрузки API.


Разные локали

Например:

locale = 'ru'

но загружены только:

locale-data/en

Смешивание native и polyfill поведения

Некоторые браузеры используют встроенный API, а другие — полифилл.

Результат:

  • различия форматирования;
  • проблемы тестирования;
  • нестабильные snapshot-тесты.

Тестирование полифиллов

В тестовой среде важно эмулировать старые браузеры.

Часто используется:

delete Intl.RelativeTimeFormat

После чего подключается полифилл.


Jest и FormatJS

Пример setup-файла:

import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/ru'

import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/ru'

Полифиллы в Vite

Пример:

// main.js
import './intl-setup'

Vite корректно работает с dynamic import, поэтому conditional polyfills реализуются особенно удобно.


Полифиллы в Webpack

Иногда требуется ручное разделение chunk.

Пример:

if (!Intl.ListFormat) {
  import(
    /* webpackChunkName: "intl-listformat" */
    '@formatjs/intl-listformat/polyfill'
  )
}

Современная стратегия использования полифиллов

Наиболее распространённая схема:

  1. Проверка наличия API.
  2. Динамическая загрузка polyfill.
  3. Динамическая загрузка locale-data.
  4. Отдельные chunks.
  5. Минимальный набор локалей.
  6. Full ICU для SSR.
  7. polyfill-force только при необходимости.

Такой подход обеспечивает:

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