Библиотека FormatJS активно использует стандарт
ECMAScript Internationalization API (Intl).
Все механизмы форматирования дат, чисел, валют, относительного времени и
сообщений опираются именно на него.
Современные браузеры поддерживают большую часть Intl,
однако в старых версиях браузеров и некоторых окружениях возможности API
могут отсутствовать полностью или частично. Особенно это касается:
В таких случаях 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/intl-pluralrules
@formatjs/intl-relativetimeformat
@formatjs/intl-listformat
Такой подход позволяет:
PluralRulesnpm install @formatjs/intl-pluralrules
Подключение:
import '@formatjs/intl-pluralrules/polyfill'
Локали:
import '@formatjs/intl-pluralrules/locale-data/ru'
import '@formatjs/intl-pluralrules/locale-data/en'
RelativeTimeFormatnpm install @formatjs/intl-relativetimeformat
Подключение:
import '@formatjs/intl-relativetimeformat/polyfill'
Локали:
import '@formatjs/intl-relativetimeformat/locale-data/ru'
DisplayNamesnpm install @formatjs/intl-displaynames
Подключение:
import '@formatjs/intl-displaynames/polyfill'
import '@formatjs/intl-displaynames/locale-data/ru'
Сам полифилл содержит только реализацию 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')
Такой подход уменьшает размер первоначальной загрузки.
Наиболее эффективная стратегия — динамическая загрузка.
Пример:
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')
}
}
Преимущества:
Некоторые браузеры имеют частичную или некорректную реализацию
Intl.
В таких случаях обычной проверки недостаточно.
FormatJS предоставляет режим принудительной замены:
import '@formatjs/intl-relativetimeformat/polyfill-force'
Этот вариант:
polyfill-forceИногда API существует, но работает с ошибками.
Например:
Некоторые версии Safari имеют неполную поддержку:
Intl.DisplayNames;Intl.ListFormat;Intl.RelativeTimeFormat.Android WebView может содержать урезанный Intl.
В Node.js проблема обычно связана с ICU-данными.
Проверка:
Intl.DateTimeFormat.supportedLocalesOf(['ru'])
Если массив пустой:
[]
значит текущая сборка Node.js не содержит полных данных локализации.
Для полноценной интернационализации нужен Full ICU.
Проверка:
node -p process.versions.icu
npm install full-icu
Запуск:
NODE_ICU_DATA=node_modules/full-icu node app.js
В проектах с 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'))
Распространённый подход — выделение отдельного файла инициализации.
intl-setup.jsimport '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/ru'
import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/ru'
index.jsimport './intl-setup'
import './app'
Преимущества:
При SSR важно, чтобы:
использовали одинаковые правила форматирования.
Иначе возможны ошибки:
Text content did not match
Например:
На сервере:
1,000.50
В браузере:
1 000,50
Это приводит к несовпадению HTML.
Необходимо:
Наличие полифиллов зависит от списка поддерживаемых браузеров.
Пример:
> 0.5%
last 2 versions
not dead
Если поддерживаются только современные браузеры, часть полифиллов может не понадобиться.
Поддержка API:
Intl.RelativeTimeFormatIntl.DisplayNamesIntl.ListFormatсущественно различается между браузерами.
Практика крупных проектов:
Полифиллы увеличивают:
Особенно тяжёлыми бывают locale-data.
Плохой вариант:
import '@formatjs/intl-relativetimeformat/locale-data/*'
Хороший вариант:
import '@formatjs/intl-relativetimeformat/locale-data/ru'
import '@formatjs/intl-relativetimeformat/locale-data/en'
При переключении языка locale-data можно загружать динамически.
Пример:
async function loadLocale(locale) {
await import(
`@formatjs/intl-relativetimeformat/locale-data/${locale}`
)
}
FormatJS предоставляет пакет:
@formatjs/intl
Он помогает централизовать работу с интернационализацией.
Однако в production-проектах чаще используется ручное управление для оптимизации bundle size.
Ошибка:
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
Некоторые браузеры используют встроенный API, а другие — полифилл.
Результат:
В тестовой среде важно эмулировать старые браузеры.
Часто используется:
delete Intl.RelativeTimeFormat
После чего подключается полифилл.
Пример 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'
Пример:
// main.js
import './intl-setup'
Vite корректно работает с dynamic import, поэтому conditional polyfills реализуются особенно удобно.
Иногда требуется ручное разделение chunk.
Пример:
if (!Intl.ListFormat) {
import(
/* webpackChunkName: "intl-listformat" */
'@formatjs/intl-listformat/polyfill'
)
}
Наиболее распространённая схема:
polyfill-force только при необходимости.Такой подход обеспечивает: