Как форматировать суммы в валюте в React Router v7

Отображение цен с валютными символами и разделителями

Проблема

При отображении цен в веб-приложениях важно учитывать два связанных аспекта локализации: используемую валюту и региональные правила представления денежных значений. Например, сумма в 1200,50 долларов США для пользователей из США выглядит как "$1,200.50", а во Франции — как "1 200,50 $US". Положение символа валюты, разделитель дробной части, группировка тысяч и пробелы зависят от региона. Если эти правила не соблюдаются, пользователи могут неправильно понять сумму или усомниться в корректности цены, что подрывает доверие к приложению.

Задача усложняется, если приложение работает в нескольких регионах или позволяет пользователям просматривать цены в разных валютах. Жёстко заданные строки формата или ручное размещение символов делают код хрупким — он ломается при добавлении новых локалей или валют. Без системного подхода поддерживать единообразное и корректное отображение цен становится сложно и рискованно.

Решение

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

Передавая числовую сумму, код валюты и текущую локаль в функцию форматирования, система автоматически применяет правильные символы, разделители и порядок элементов. Это гарантирует, что цены всегда будут отображаться в привычном для пользователя формате — независимо от выбранной валюты или региона.

Шаги

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

Создайте компонент, который использует FormattedNumber с пропсами style и currency для форматирования денежных значений.

import { FormattedNumber } from "react-intl";

interface PriceProps {
  amount: number;
  currency: string;
}

export function Price({ amount, currency }: PriceProps) {
  return (
    <FormattedNumber value={amount} style="currency" currency={currency} />
  );
}

Компонент FormattedNumber использует API formatNumber и принимает Intl.NumberFormatOptions. Опция style="currency" указывает форматтеру включать символы валюты, а пропс currency определяет, какую валюту отображать.

2. Используйте компонент Price в роут-компонентах

Отображайте цены в любом компоненте маршрута React Router, передавая числовое значение и код валюты.

import { Price } from "~/components/Price";

export default function ProductPage() {
  const product = {
    name: "Wireless Headphones",
    price: 129.99,
    currency: "USD",
  };

  return (
    <div>
      <h1>{product.name}</h1>
      <p>
        <Price amount={product.price} currency={product.currency} />
      </p>
    </div>
  );
}

Компонент Price автоматически форматирует сумму в соответствии с локалью, настроенной в IntlProvider. Для локали США будет отображаться "$129.99"; для немецкой локали с той же валютой USD — "129,99 $".

3. Императивное форматирование валюты при необходимости

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

import { useIntl } from "react-intl";

export default function CheckoutSummary() {
  const intl = useIntl();
  const subtotal = 89.99;
  const tax = 7.2;
  const total = subtotal + tax;

  const formattedTotal = intl.formatNumber(total, {
    style: "currency",
    currency: "EUR",
  });

  return (
    <div>
      <h2>Order Summary</h2>
      <dl>
        <dt>Subtotal</dt>
        <dd>
          {intl.formatNumber(subtotal, {
            style: "currency",
            currency: "EUR",
          })}
        </dd>
        <dt>Tax</dt>
        <dd>
          {intl.formatNumber(tax, {
            style: "currency",
            currency: "EUR",
          })}
        </dd>
        <dt>Total</dt>
        <dd aria-label={`Total: ${formattedTotal}`}>
          <strong>{formattedTotal}</strong>
        </dd>
      </dl>
    </div>
  );
}

Метод formatNumber принимает значение и объект опций с FormatNumberOptions. Это удобно, когда нужна отформатированная строка для атрибутов, логирования или вне JSX-контекста.

4. Управляйте точностью десятичных для валют без дробных единиц

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

import { FormattedNumber } from "react-intl";

interface PriceProps {
  amount: number;
  currency: string;
}

export function Price({ amount, currency }: PriceProps) {
  const isWholeNumberCurrency = currency === "JPY" || currency === "KRW";

  return (
    <FormattedNumber
      value={amount}
      style="currency"
      currency={currency}
      minimumFractionDigits={isWholeNumberCurrency ? 0 : 2}
      maximumFractionDigits={isWholeNumberCurrency ? 0 : 2}
    />
  );
}

Японская иена не использует минорные единицы, поэтому суммы отображаются без десятичных знаков. Опции minimumFractionDigits и maximumFractionDigits позволяют переопределить стандартное поведение при необходимости.