🫨

Next.js × next-intl で「国旗が切り替わらない」現象をデバッグ & 修正した話

に公開

TL;DR

  • next-intl でロケールを切り替えても 国旗 Emoji が日本のまま変わらない
  • 原因は 「現在のロケールを誤検知 & 遅延参照していた」 こと
    • useLocale()context の値なので、クライアントサイド遷移直後は古い値のまま描画される
    • さらに国旗を 翻訳ファイル経由 で取得していたため、ja.json の 🇯🇵 が常に返っていた
  • URL パラメータ(/ja, /en, /zh)を直接読む固定マップで国旗を管理 で解決

事象

<span className="text-lg">🇯🇵</span>

上記のように、どの言語でも常に 🇯🇵 が表示される。
Navigation コンポーネントでは下記のような実装にしていた。

const flagT = useTranslations("flags");
const locale = useLocale();           // <- ここで「現在のロケール」を取得

const getFlag = (code: string) => {
  // 翻訳ファイルから取り出す
  try {
    return flagT(code);               // flags.ja → 🇯🇵 など
  } catch {}
  return "🌐";
};

期待

  • /en に遷移 → 🇺🇸
  • /zh に遷移 → 🇨🇳

現実

  • どこへ行っても 🇯🇵 …

原因を分解してみる

1. useLocale() だけでは最新ロケールを取れない

next-intluseLocale()Context 由来。
クライアントルーター (next/navigation) で言語を切り替えると、

  1. router.push("/en/…")
  2. URL は変わる
  3. しかし 初回描画時点では Context がまだ ja
  4. navigation.tsx は古い ja のままレンダリング → 🇯🇵

その後再レンダリングは走るが、国旗は 翻訳ファイルの値 をキャッシュしており変わらない。

2. 国旗 Emoji を翻訳ファイルに置いたのが落とし穴

flags name-space を ja.json / en.json / zh.json すべてに用意。
ところが flagT(code) を呼び出すロケールは「現在のロケール」

  • まだ ja だと思っている → ja.json から 🇯🇵 を返す
  • 結果、国旗は固定で 🇯🇵 に

解決策

(A) URL からロケールを直接取得

import { useParams } from "next/navigation";

const params = useParams();
const localeParam =
  Array.isArray(params?.locale) ? params.locale[0] : params?.locale;
const locale = localeParam ?? useLocale(); // fallback
  • /en/** なら localeParam === "en" が確実
  • Context が更新される前でも 正しいロケール を先取りできる

(B) 国旗は固定マップで管理

翻訳ファイルを通さず、単純な連想配列に変更。

const getFlag = (code: string) =>
  ({ ja: "🇯🇵", en: "🇺🇸", zh: "🇨🇳" }[code] ?? "🌐");
  • 「国旗そのもの」は翻訳ではない
  • ファイル数が増えても メンテしやすい
  • ガベコピ防止で fallback も一行

実際に入れたパッチ

-import { usePathname, useRouter } from "next/navigation";
+import { usePathname, useRouter, useParams } from "next/navigation";

-const flagT = useTranslations("flags");
-const locale = useLocale();
+const params = useParams();
+const localeParam = Array.isArray(params?.locale) ? params.locale[0] : params?.locale;
+const locale = localeParam || useLocale();

-const getFlag = (code: string): string => {
-  const translated = flagT(code);     // ← 削除
--}
+const getFlag = (code: string): string =>
+  ({ ja: "🇯🇵", en: "🇺🇸", zh: "🇨🇳" }[code] ?? "🌐");

得られた学び

  1. Context の値は「遅延」する 可能性がある
    • ルーティング直後の「一瞬」を見逃すとバグになる
  2. 翻訳ファイルに入れるべきデータか?を再考
    • 国旗やアイコンは「翻訳」ではなく「定数」
  3. バグを直す前に 「どう再現するか」を最小化
    • 今回は console.log(locale) を貼って即発見

まとめ

  • useLocale() 依存のままでは、クライアント遷移直後のロケールを正しく反映できない
  • URL パラメータから直接ロケールを取り、国旗はシンプルな固定マップで管理すれば OK
  • 「翻訳」と「定数」を分ける のが可読性&保守性のポイント

これで /en に行けば 🇺🇸、/zh に行けば 🇨🇳 へ即座に切り替わるようになりました。
同じようにハマった方の参考になれば幸いです 🫡

Discussion