Работа с числами в интернационализированных приложениях в JavaScript почти всегда опирается на возможности стандарта ECMAScript Internationalization API (Intl). В экосистеме FormatJS эти механизмы дополняются ICU-совместимым синтаксисом сообщений, удобными обёртками и полифилами, которые позволяют контролировать точность отображения чисел, валют, процентов и временных интервалов.
Ключевая проблема форматирования заключается в том, что внутреннее представление чисел в JavaScript использует IEEE-754 floating point. Это приводит к накоплению погрешностей:
0.1 + 0.2 === 0.3 // false
Форматирование не должно отображать эти артефакты пользователю, поэтому используется округление и контроль точности на уровне форматтеров.
Основной инструмент управления округлением —
Intl.NumberFormat. В FormatJS он используется напрямую или
через ICU-синтаксис сообщений.
const nf = new Intl.NumberFormat("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
nf.format(1.2); // "1.20"
nf.format(1.236); // "1.24"
Смысл параметров:
minimumFractionDigits — гарантирует минимальное число
знаков после запятойmaximumFractionDigits — ограничивает максимальную
точность и инициирует округлениеПо умолчанию используется режим halfExpand (округление
половин вверх). Однако современные реализации Intl позволяют управлять
режимом явно:
const nf = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
roundingMode: "floor"
});
nf.format(1.9); // "1"
ceil — всегда вверхfloor — всегда внизexpand — от нуляtrunc — усечениеhalfCeil, halfFloor,
halfExpand, halfTrunc,
halfEvenОсобое значение имеет halfEven (банковское округление),
минимизирующее систематическую ошибку при больших выборках данных.
Помимо дробной части, форматирование может контролироваться через значащие цифры:
const nf = new Intl.NumberFormat("en-US", {
minimumSignificantDigits: 3,
maximumSignificantDigits: 5
});
nf.format(1234.56); // "1,234.6"
В FormatJS используется ICU MessageFormat, который позволяет задавать числовые форматы прямо в строках сообщений:
const message = "Баланс: {value, number, ::currency/USD}";
или более явно:
const message = "{value, number, .##}";
ICU поддерживает расширенные модификаторы:
::currency/USD — валютное форматирование::percent — проценты::compact-short — компактное представлениеПримеры:
"Доход: {income, number, ::currency/EUR}"
"Рост: {growth, number, ::percent}"
"Просмотры: {views, number, ::compact-short}"
Валютные значения требуют строгого контроля дробной части, который задаётся спецификацией валюты:
const nf = new Intl.NumberFormat("ja-JP", {
style: "currency",
currency: "JPY"
});
nf.format(1234.56); // "¥1,235"
Особенности:
minor unit (например, JPY = 0
знаков)Компактный формат меняет представление числа, сохраняя точность округления:
const nf = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1
});
nf.format(1200); // "1.2K"
nf.format(2500000); // "2.5M"
Здесь округление выполняется до уровня отображаемой значимости, а не исходного значения.
JavaScript не гарантирует точное десятичное представление:
0.3 / 0.1 // 2.9999999999999996
Форматирование решает проблему только на уровне отображения, но не вычисления.
Типичная практика:
const value = Math.round((a + b) * 100) / 100;
Однако предпочтительнее полагаться на Intl:
new Intl.NumberFormat("en-US", {
maximumFractionDigits: 2
}).format(a + b);
В React-ориентированном использовании FormatJS форматирование часто происходит через компоненты:
import { FormattedNumber } from "react-intl";
<FormattedNumber
value={1234.567}
minimumFractionDigits={2}
maximumFractionDigits={2}
/>
Или через ICU:
<FormattedMessage
defaultMessage="Сумма: {amount, number, ::currency/USD}"
values={{ amount: 1234.56 }}
/>
Здесь вся логика округления делегируется Intl, а не бизнес-коду.
В экосистеме FormatJS часто используются полифилы:
@formatjs/intl-numberformat@formatjs/intl-datetimeformat@formatjs/intl-pluralrulesОни важны для:
Разные реализации могут по-разному трактовать edge cases округления, поэтому полифилы фиксируют поведение.
Разные локали влияют не только на разделители, но и на правила представления чисел:
Пример:
new Intl.NumberFormat("de-DE").format(1234.56); // "1.234,56"
new Intl.NumberFormat("en-US").format(1234.56); // "1,234.56"
Округление остаётся одинаковым, но визуальное представление меняется.
Форматирование не заменяет арифметическую точность. В системах с деньгами или измерениями применяются дополнительные подходы:
FormatJS в этом контексте отвечает только за последний этап — представление.
ICU позволяет задавать формат прямо в контексте текста, что устраняет необходимость ручной обработки чисел:
"{count, plural,
one {# элемент}
other {# элементов}
}"
Хотя это не про округление напрямую, числовая точность влияет на выбор формы множественного числа при дробных значениях:
1.0 → one
1.2 → other
Если параметры не заданы:
roundingMode: "halfExpand"new Intl.NumberFormat("en-US").format(1.2); // "1.2"
new Intl.NumberFormat("en-US").format(1.234); // "1.234"
Это поведение важно учитывать при построении UI, где требуется стабильный формат.
При комбинации параметров:
minimumFractionDigitsmaximumFractionDigitsroundingModenotationcompactDisplayформируется итоговая стратегия округления, которая применяется до локализации.
Пример:
new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 2,
roundingMode: "halfEven"
});
Результат зависит от всех параметров одновременно, а не от одного доминирующего.
В крупных приложениях FormatJS используется как слой унификации:
Это позволяет избегать ситуаций, когда одинаковые значения отображаются по-разному в разных компонентах интерфейса из-за локальных настроек форматирования.