Sabigara

Intl.NumberFormatの円記号がSafariだけ半角になる問題

Intl.NumberFormat を使うと、ライブラリを追加することなしに、数値をローカライズされたフォーマットに変換できる。

function formatPrice(price: number): string {
  const intl = new Intl.NumberFormat("ja-JP", {
    style: "currency",
    currency: "JPY",
  });
  return intl.format(price);
}

formatPrice(1000);
// -> ¥1,000

これはECMA Scriptに標準化されており、またすでに各ランタイムで実装されているため、特に環境を気にせず使えるようになっている。

・・・はずだが、この機能を使っているページが、なぜかSafariで閲覧したときだけHydration Errorを吐いてしまう問題にぶつかった。

Text content did not match. Server: "¥4,180" Client: "¥4,180"

最初何が違うのかわからなかったが、よく見ると円記号が全角か半角かの違いがある。Chromeで問題ないということは、SSRしているNode環境とChromeは全角で一致しているということだ。

検証してみると、主要ランタイムではSafariのみ半角を出力することがわかった。

ChromeFirefoxSafariNode
全角全角半角全角

解決方法

半角の円記号を全角に replace すればOK。

function formatPrice(price: number): string {
  const intl = new Intl.NumberFormat("ja-JP", {
    style: "currency",
    currency: "JPY",
  })
-  return intl.format(price)
+  return intl.format(price).replace(/^¥/, "¥");
}

Intl.NumberFormat() に渡すオプションで指定できないか調べてみたが、できなさそうに見えた。

Hydrationのエラーが出なければ見過ごしてしまいそうな差異だが、場合によってはそれほど小さくない問題を引き起こしそうな気もする。なんであれ、この辺の実装は統一してほしい。