React Router v7で通貨金額をフォーマットする方法

通貨記号と区切り記号を使用して価格を表示する

問題

Webアプリケーションで価格を表示するには、相互に関連する2つのローカライゼーションの課題に対処する必要があります。使用される通貨と、金額を表示するための地域の慣習です。1200.50米ドルの価格は、米国のユーザーには「$1,200.50」と表示されますが、フランスでは「1 200,50 $US」と表示されます。通貨記号の位置、小数点区切り記号、桁区切り、スペースはすべてロケールによって異なります。これらの慣習が尊重されない場合、ユーザーは金額を誤読したり、価格が正しいかどうか疑問に思ったりする可能性があり、アプリケーションへの信頼が損なわれます。

アプリケーションが複数の地域にサービスを提供する場合、またはユーザーが異なる通貨で価格を表示できるようにする場合、課題はさらに複雑になります。フォーマット文字列をハードコーディングしたり、記号を手動で配置したりすると、新しいロケールや通貨が追加されたときに壊れやすいコードが作成されます。体系的なアプローチがなければ、アプリケーション全体で一貫性のある正確な価格表示を維持することはエラーが発生しやすくなります。

解決策

ターゲット通貨コードとユーザーのアクティブなロケールの両方を使用して通貨値をフォーマットします。このアプローチは、通貨表示の複雑なルールをブラウザの国際化APIに委任します。これらのAPIには、数百のロケールと通貨の組み合わせに対するフォーマット規則がすでにエンコードされています。

数値の金額、通貨コード、現在のロケールをフォーマット関数に渡すことで、システムは正しい記号、区切り記号、レイアウトを自動的に適用します。これにより、表示される通貨やユーザーがいる地域に関係なく、価格は常にユーザーにとって馴染みのある形式で表示されます。

手順

1. 再利用可能な通貨フォーマットコンポーネントを作成する

FormattedNumberstyleおよびcurrencyプロパティを使用して、金額をフォーマットするreact-intlのコンポーネントを構築します。

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.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オプションは、必要に応じてデフォルトの小数点動作を上書きします。