Цены и скидки

FormatJS предоставляет набор инструментов для форматирования чисел, валют и сообщений, основанных на ICU MessageFormat, что делает работу с ценами и скидками в интерфейсах предсказуемой и локализуемой. В контексте коммерческих интерфейсов ключевую роль играют два направления: корректное отображение валюты и выразительное представление скидок с учётом локали пользователя.

Основой работы с ценами в FormatJS является использование числового форматирования через Intl.NumberFormat, которое оборачивается удобными абстракциями из react-intl.

Форматирование валюты строится на сочетании локали и кода валюты:

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

formatter.format(1999.99);

Результат зависит от локали:

  • 1 999,99 ₽ для ru-RU
  • $1,999.99 для en-US

В экосистеме FormatJS аналогичный результат достигается через FormattedNumber:

import { FormattedNumber } from 'react-intl';

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

Такой подход позволяет декларативно встроить форматирование в UI, сохраняя единый стандарт отображения цен.


Базовая цена и её локализация

В коммерческих интерфейсах цена почти всегда является динамическим значением, передаваемым через сообщения или props. FormatJS поддерживает интерполяцию значений в ICU-сообщениях:

const message = "Цена товара: {price, number, currency}";

Передача значения:

<FormattedMessage
  id="product.price"
  defaultMessage="Цена товара: {price, number, currency}"
  values={{
    price: 1999.99
  }}
/>

Если требуется строгий контроль валюты, используется расширенный формат через formatNumber:

const intl = useIntl();

intl.formatNumber(1999.99, {
  style: 'currency',
  currency: 'RUB'
});

Такой подход позволяет централизовать логику форматирования и избежать дублирования правил в компонентах.


Отображение скидок и вычисление разницы

Скидка в интерфейсе обычно состоит из трёх элементов:

  • исходная цена
  • итоговая цена
  • величина скидки (процент или абсолютное значение)

FormatJS не выполняет бизнес-логику расчётов, но обеспечивает корректное отображение результатов.

Процент скидки

const originalPrice = 2500;
const finalPrice = 2000;

const discount = (1 - finalPrice / originalPrice);

Форматирование процента:

<FormattedNumber
  value={discount}
  style="percent"
/>

Результат:

  • 20% для большинства локалей при значении 0.2

Важно учитывать, что style="percent" ожидает дробное значение, а не уже умноженное на 100 число.


Отображение зачёркнутой цены

В интерфейсах электронной коммерции распространён паттерн отображения старой цены с зачёркиванием. FormatJS отвечает за форматирование, а стилизация выполняется отдельно:

<span style={{ textDecoration: 'line-through' }}>
  <FormattedNumber value={2500} style="currency" currency="RUB" />
</span>

Рядом отображается итоговая цена:

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

Такой подход разделяет визуальную семантику и международное форматирование.


ICU-сообщения для скидочных сценариев

ICU MessageFormat позволяет выразить сложные ценовые конструкции внутри строки:

const message = `
  Вы экономите {amount, number, currency} ({percent, number, percent})
`;

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

<FormattedMessage
  id="discount.info"
  defaultMessage="Вы экономите {amount, number, currency} ({percent, number, percent})"
  values={{
    amount: 500,
    percent: 0.2
  }}
/>

Такой формат удобен для локализации, поскольку переводчики работают со структурой сообщения, а не с кодом.


Округление и точность отображения цен

Цены требуют строгого контроля округления. Intl.NumberFormat внутри FormatJS поддерживает настройки точности:

intl.formatNumber(1999.999, {
  style: 'currency',
  currency: 'RUB',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

Поведение:

  • 1999.999 → 2 000,00 ₽
  • 1999.9 → 1 999,90 ₽

Контроль дробной части критичен при работе с налогами и скидками, где погрешности недопустимы на уровне интерфейса.


Компактное представление цен

Для карточек товаров и списков часто используется компактное представление:

intl.formatNumber(1200000, {
  notation: 'compact',
  compactDisplay: 'short'
});

Результаты:

  • 1,2 млн (ru-RU)
  • 1.2M (en-US)

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


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

В каталогах часто используется диапазон:

  • от минимальной цены
  • до максимальной цены

ICU-сообщения позволяют выразить это явно:

const message = "от {min, number, currency}";

или диапазон:

const message = "{min, number, currency} – {max, number, currency}";

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

<FormattedMessage
  id="price.range"
  defaultMessage="{min, number, currency} – {max, number, currency}"
  values={{
    min: 1000,
    max: 3000
  }}
/>

Скидка в процентах и абсолютная экономия

Сценарий с одновременным отображением двух типов скидки требует комбинированного форматирования:

const original = 4000;
const final = 3000;

const saved = original - final;
const percent = saved / original;

ICU-сообщение:

<FormattedMessage
  id="discount.summary"
  defaultMessage="Скидка {percent, number, percent} — экономия {saved, number, currency}"
  values={{
    percent,
    saved
  }}
/>

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


Локализация валют и кодов ISO

FormatJS опирается на ISO 4217 коды валют:

  • RUB
  • USD
  • EUR
  • KZT
intl.formatNumber(1999, {
  style: 'currency',
  currency: 'KZT'
});

Результат адаптируется под локаль:

  • 1 999 ₸
  • KZT 1,999 (в некоторых англоязычных локалях)

Работа с компонентом FormattedNumber в сложных UI

FormattedNumber поддерживает функциональный рендеринг, что полезно для кастомной стилизации:

<FormattedNumber
  value={1999}
  style="currency"
  currency="RUB"
>
  {(formattedValue) => (
    <span className="price">{formattedValue}</span>
  )}
</FormattedNumber>

Это позволяет отделить форматирование от визуального слоя и внедрять сложные UI-паттерны: бейджи, анимации изменения цены, сравнение версий.


Учет налогов в отображении цены

Хотя FormatJS не занимается расчетами, он часто используется для отображения уже рассчитанных значений:

const priceWithTax = basePrice * 1.2;

Далее форматирование:

<FormattedNumber
  value={priceWithTax}
  style="currency"
  currency="USD"
/>

В интерфейсах с налогами важно не смешивать вычисления и локализацию, чтобы избежать различий между регионами.


Сложные сообщения с несколькими валютными значениями

В некоторых интерфейсах требуется показать цену в разных валютах одновременно:

<FormattedMessage
  id="price.multi"
  defaultMessage="≈ {usd, number, currency} или {eur, number, currency}"
  values={{
    usd: 25,
    eur: 23
  }}
/>

Такой подход применяется для предварительной оценки стоимости в международных сервисах.


Влияние локали на восприятие скидки

Форматирование процентов и валют в FormatJS строго зависит от локали:

  • в одних локалях знак процента ставится без пробела
  • в других добавляется неразрывный пробел
  • разделители дробей и тысяч различаются

Пример:

  • 20%
  • 20 %

Это поведение нельзя фиксировать вручную без потери универсальности интерфейса.


Использование useIntl в ценовой логике интерфейса

Хук useIntl применяется для динамического форматирования в логике компонентов:

const intl = useIntl();

const formattedPrice = intl.formatNumber(price, {
  style: 'currency',
  currency: 'EUR'
});

Далее значение используется в сложных сценариях:

  • сравнение цен
  • условное отображение скидок
  • генерация уведомлений

Динамическое переключение валют

В мультивалютных интерфейсах форматирование зависит от выбранного региона:

intl.formatNumber(price, {
  style: 'currency',
  currency: selectedCurrency
});

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


Сводная модель ценового форматирования

В архитектуре приложений, использующих FormatJS, ценовые данные обычно разделяются на три слоя:

  • данные: исходная цена, скидка, налоги
  • логика: вычисления скидок и итоговой стоимости
  • представление: форматирование через Intl и ICU

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