Как форматировать суммы в валюте в 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 позволяют переопределить стандартное поведение при необходимости.