Intl.NumberFormat полифилл

Intl.NumberFormat — часть стандарта ECMAScript Internationalization API (Intl), предназначенная для локализованного форматирования чисел. Механизм поддерживает:

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

Поддержка Intl.NumberFormat присутствует в современных браузерах и средах выполнения, однако:

  • старые браузеры реализуют API частично;
  • некоторые окружения не содержат необходимых locale-данных;
  • мобильные WebView часто используют урезанные ICU-таблицы;
  • Node.js может быть собран с ограниченным набором локалей.

Полифилл FormatJS обеспечивает одинаковое поведение API во всех окружениях.


Пакет @formatjs/intl-numberformat

Основной полифилл поставляется в пакете:

npm install @formatjs/intl-numberformat

Дополнительно устанавливаются locale-данные:

npm install @formatjs/intl-numberformat locale-data

Структура импорта:

import '@formatjs/intl-numberformat/polyfill';
import '@formatjs/intl-numberformat/locale-data/ru';

После подключения глобальный объект Intl.NumberFormat получает полную реализацию спецификации.


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

Отсутствие Intl.NumberFormat

Некоторые старые браузеры не содержат Intl вовсе:

if (!Intl || !Intl.NumberFormat) {
  // требуется полифилл
}

Частичная реализация API

Часто API существует, но не поддерживает современные возможности:

new Intl.NumberFormat('ru', {
  notation: 'compact'
});

В старых движках такой код вызывает ошибку или игнорирует настройки.


Недостаток locale-данных

Node.js с minimal ICU:

new Intl.NumberFormat('fr').format(1000);

Результат может оказаться:

1,000

вместо:

1 000

Полифилл загружает необходимые CLDR-данные независимо от ICU.


Базовое подключение

Полный полифилл

import '@formatjs/intl-numberformat/polyfill';
import '@formatjs/intl-numberformat/locale-data/en';
import '@formatjs/intl-numberformat/locale-data/ru';

Использование:

const formatter = new Intl.NumberFormat('ru');

console.log(formatter.format(1234567.89));

Результат:

1 234 567,89

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

FormatJS предоставляет условный импорт.

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

import {shouldPolyfill} from '@formatjs/intl-numberformat/should-polyfill';

async function setup() {
  const unsupportedLocale = shouldPolyfill('ru');

  if (unsupportedLocale) {
    await import('@formatjs/intl-numberformat/polyfill-force');
    await import(`@formatjs/intl-numberformat/locale-data/${unsupportedLocale}`);
  }
}

Разница между polyfill и polyfill-force

polyfill

Подключает реализацию только при необходимости.

import '@formatjs/intl-numberformat/polyfill';

polyfill-force

Всегда заменяет встроенную реализацию.

import '@formatjs/intl-numberformat/polyfill-force';

Полезно:

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

Locale Data

Полифилл не включает все локали автоматически. Locale-данные импортируются отдельно.

Русская локаль

import '@formatjs/intl-numberformat/locale-data/ru';

Несколько локалей

import '@formatjs/intl-numberformat/locale-data/en';
import '@formatjs/intl-numberformat/locale-data/de';
import '@formatjs/intl-numberformat/locale-data/fr';

Динамическая загрузка

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

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

Простое форматирование

const formatter = new Intl.NumberFormat('ru');

formatter.format(1000000);

Результат:

1 000 000

Десятичные дроби

const formatter = new Intl.NumberFormat('ru', {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

formatter.format(12.5);

Результат:

12,50

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

Российский рубль

const formatter = new Intl.NumberFormat('ru', {
  style: 'currency',
  currency: 'RUB'
});

formatter.format(1500);

Результат:

1 500,00 ₽

Доллар США

const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

formatter.format(1500);

Результат:

$1,500.00

Валютные отображения

Символ валюты

currencyDisplay: 'symbol'
$100

Код валюты

currencyDisplay: 'code'
USD 100

Название валюты

currencyDisplay: 'name'
100 US dollars

Проценты

const formatter = new Intl.NumberFormat('ru', {
  style: 'percent'
});

formatter.format(0.25);

Результат:

25 %

Компактная запись чисел

Поддержка compact notation появилась не во всех движках одновременно, поэтому полифилл особенно полезен для этой функции.

Короткая запись

const formatter = new Intl.NumberFormat('ru', {
  notation: 'compact',
  compactDisplay: 'short'
});

formatter.format(1500000);

Результат:

1,5 млн

Длинная запись

const formatter = new Intl.NumberFormat('ru', {
  notation: 'compact',
  compactDisplay: 'long'
});

formatter.format(1500000);

Результат:

1,5 миллиона

Scientific notation

const formatter = new Intl.NumberFormat('en', {
  notation: 'scientific'
});

formatter.format(123456);

Результат:

1.235E5

Engineering notation

const formatter = new Intl.NumberFormat('en', {
  notation: 'engineering'
});

formatter.format(123456);

Результат:

123.456E3

Единицы измерения

Километры

const formatter = new Intl.NumberFormat('ru', {
  style: 'unit',
  unit: 'kilometer'
});

formatter.format(15);

Результат:

15 км

Температура

const formatter = new Intl.NumberFormat('ru', {
  style: 'unit',
  unit: 'celsius'
});

formatter.format(25);

Результат:

25 °C

Настройки отображения единиц

Короткий формат

unitDisplay: 'short'

Узкий формат

unitDisplay: 'narrow'

Полное название

unitDisplay: 'long'

Системы нумерации

Арабские цифры

const formatter = new Intl.NumberFormat('ar', {
  numberingSystem: 'arab'
});

formatter.format(123456);

Управление округлением

Максимум дробных цифр

const formatter = new Intl.NumberFormat('ru', {
  maximumFractionDigits: 1
});

formatter.format(1.27);

Результат:

1,3

Минимум дробных цифр

const formatter = new Intl.NumberFormat('ru', {
  minimumFractionDigits: 3
});

formatter.format(1.2);

Результат:

1,200

Significant digits

const formatter = new Intl.NumberFormat('en', {
  maximumSignificantDigits: 3
});

formatter.format(12345.678);

Результат:

12,300

Форматирование диапазонов

Современная спецификация поддерживает formatRange.

const formatter = new Intl.NumberFormat('ru');

formatter.formatRange(10, 20);

Результат:

10–20

Полифилл обеспечивает поддержку даже в окружениях без реализации метода.


Метод formatToParts

Метод разбивает строку на семантические части.

const formatter = new Intl.NumberFormat('ru', {
  style: 'currency',
  currency: 'RUB'
});

console.log(formatter.formatToParts(1234.5));

Результат:

[
  { type: 'integer', value: '1' },
  { type: 'group', value: ' ' },
  { type: 'integer', value: '234' },
  { type: 'decimal', value: ',' },
  { type: 'fraction', value: '50' },
  { type: 'literal', value: ' ' },
  { type: 'currency', value: '₽' }
]

Использование formatToParts в интерфейсах

Стилизация валюты

const parts = formatter.formatToParts(1234);

const html = parts.map(part => {
  if (part.type === 'currency') {
    return `<strong>${part.value}</strong>`;
  }

  return part.value;
}).join('');

Метод resolvedOptions

Возвращает фактические настройки форматтера.

const formatter = new Intl.NumberFormat('ru', {
  style: 'currency',
  currency: 'RUB'
});

console.log(formatter.resolvedOptions());

Lazy polyfill

Оптимизация загрузки:

async function ensureNumberFormat(locale) {
  const unsupportedLocale = shouldPolyfill(locale);

  if (!unsupportedLocale) {
    return;
  }

  await import('@formatjs/intl-numberformat/polyfill-force');
  await import(
    `@formatjs/intl-numberformat/locale-data/${unsupportedLocale}`
  );
}

Использование с React

Создание форматтера

const formatter = new Intl.NumberFormat('ru', {
  style: 'currency',
  currency: 'RUB'
});

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

function Price({ value }) {
  return (
    <span>
      {formatter.format(value)}
    </span>
  );
}

Использование с react-intl

react-intl автоматически использует Intl.NumberFormat.

<IntlProvider locale="ru">
  <App />
</IntlProvider>

Компонент FormattedNumber

<FormattedNumber
  value={1500}
  style="currency"
  currency="RUB"
/>

Серверный рендеринг

Node.js

import '@formatjs/intl-numberformat/polyfill-force';
import '@formatjs/intl-numberformat/locale-data/ru';

После этого форматирование работает одинаково на сервере и клиенте.


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

Internet Explorer 11

Для IE11 обычно требуется:

npm install @formatjs/intl-numberformat

и дополнительные полифиллы:

npm install core-js

Tree Shaking

FormatJS проектировался с учётом минимизации размера бандла.

Импорт только нужных локалей

import '@formatjs/intl-numberformat/locale-data/ru';

вместо:

import '@formatjs/intl-numberformat/locale-data/*';

Производительность

Создание экземпляра Intl.NumberFormat — относительно дорогая операция.

Нежелательно

items.map(item => (
  new Intl.NumberFormat('ru').format(item.price)
));

Предпочтительно

const formatter = new Intl.NumberFormat('ru');

items.map(item => formatter.format(item.price));

Кэширование форматтеров

const cache = new Map();

function getFormatter(locale, options) {
  const key = JSON.stringify([locale, options]);

  if (!cache.has(key)) {
    cache.set(
      key,
      new Intl.NumberFormat(locale, options)
    );
  }

  return cache.get(key);
}

Поддерживаемые возможности спецификации

Полифилл FormatJS поддерживает:

  • format;
  • formatToParts;
  • formatRange;
  • compact notation;
  • unit formatting;
  • scientific notation;
  • engineering notation;
  • sign display;
  • rounding options;
  • numbering systems;
  • currency formatting;
  • plural-sensitive compact rules.

Ограничения

Размер locale-данных

Подключение большого количества локалей увеличивает bundle size.


Динамический импорт

Некоторые сборщики требуют специальной настройки:

import(
  `@formatjs/intl-numberformat/locale-data/${locale}`
);

Webpack может включить все локали в bundle.


Оптимизация locale-данных

Явное перечисление

const locales = {
  ru: () =>
    import('@formatjs/intl-numberformat/locale-data/ru'),

  en: () =>
    import('@formatjs/intl-numberformat/locale-data/en')
};

Отличия от нативной реализации

FormatJS стремится к полному соответствию спецификации ECMA-402, однако:

  • разные версии ICU могут давать разные результаты;
  • правила CLDR периодически обновляются;
  • браузеры иногда используют устаревшие locale-таблицы.

Полифилл помогает унифицировать поведение между платформами.


Типичная архитектура подключения

Файл инициализации i18n

import '@formatjs/intl-numberformat/polyfill';

import '@formatjs/intl-numberformat/locale-data/en';
import '@formatjs/intl-numberformat/locale-data/ru';

Bootstrap приложения

async function bootstrap() {
  await setupIntl();

  startApplication();
}

Проверка поддержки возможностей

Compact notation

function supportsCompact() {
  try {
    new Intl.NumberFormat('en', {
      notation: 'compact'
    });

    return true;
  } catch {
    return false;
  }
}

Использование с TypeScript

Типы поставляются вместе с пакетом.

const formatter: Intl.NumberFormat =
  new Intl.NumberFormat('ru');

Пример полноценной настройки

import {shouldPolyfill}
  from '@formatjs/intl-numberformat/should-polyfill';

export async function setupNumberFormat(locale) {
  const unsupportedLocale = shouldPolyfill(locale);

  if (!unsupportedLocale) {
    return;
  }

  await import(
    '@formatjs/intl-numberformat/polyfill-force'
  );

  await import(
    `@formatjs/intl-numberformat/locale-data/${unsupportedLocale}`
  );
}

Пример универсального форматирования

const priceFormatter =
  new Intl.NumberFormat('ru', {
    style: 'currency',
    currency: 'RUB'
  });

const percentFormatter =
  new Intl.NumberFormat('ru', {
    style: 'percent'
  });

const compactFormatter =
  new Intl.NumberFormat('ru', {
    notation: 'compact'
  });

console.log(priceFormatter.format(1999));
console.log(percentFormatter.format(0.25));
console.log(compactFormatter.format(1200000));

Результат:

1 999,00 ₽
25 %
1,2 млн