如何在 React Router v7 中格式化货币金额

使用货币符号和分隔符显示价格

问题

在 Web 应用程序中显示价格需要处理两个相关的本地化问题:所使用的货币和呈现货币值的区域惯例。1200.50 美元的价格在美国用户看来是 "$1,200.50",但在法国则显示为 "1 200,50 $US"。货币符号的位置、小数点分隔符、千位分隔符和间距都因地区而异。如果不遵循这些惯例,用户可能会误读金额或质疑价格的正确性,从而削弱对应用程序的信任。

当一个应用程序服务于多个地区或允许用户以不同货币查看价格时,这一挑战变得更加复杂。硬编码格式字符串或手动放置符号会导致代码脆弱,当添加新地区或货币时容易出错。如果没有系统化的方法,在整个应用程序中保持一致且正确的价格显示将变得容易出错。

解决方案

使用目标货币代码和用户的活动区域来格式化货币值。这种方法将货币呈现的复杂规则委托给浏览器的国际化 API,这些 API 已经编码了数百种地区和货币组合的格式化惯例。

通过将数值金额、货币代码和当前区域传递给格式化函数,系统会自动应用正确的符号、分隔符和布局。这确保了无论显示哪种货币或用户位于哪个地区,价格始终以用户熟悉的格式显示。

步骤

1. 创建一个可复用的货币格式化组件

构建一个使用 react-intl 的 FormattedNumber 组件,并通过 stylecurrency 属性来格式化货币值。

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 组件使用 formatNumber API,并接受 Intl.NumberFormatOptionsstyle="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>订单摘要</h2>
      <dl>
        <dt>小计</dt>
        <dd>
          {intl.formatNumber(subtotal, {
            style: "currency",
            currency: "EUR",
          })}
        </dd>
        <dt>税费</dt>
        <dd>
          {intl.formatNumber(tax, {
            style: "currency",
            currency: "EUR",
          })}
        </dd>
        <dt>总计</dt>
        <dd aria-label={`总计: ${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}
    />
  );
}

例如,日元不使用小数单位,因此金额会以无小数的形式显示。minimumFractionDigitsmaximumFractionDigits 选项在必要时会覆盖默认的小数行为。