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

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

問題

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

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

解決策

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

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

ステップ

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

react-intlのFormattedNumberstylecurrencyプロパティと共に使用して、金額をフォーマットするコンポーネントを構築します。

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}
    />
  );
}

日本円は補助単位を使用しないため、金額は小数点なしで表示されます。minimumFractionDigitsmaximumFractionDigitsオプションは、必要に応じてデフォルトの小数点の動作を上書きします。