React Router v7でナビゲーションリンクのロケールを維持する方法

内部ナビゲーション全体でロケールを維持

問題

ロケール情報がURLパスにエンコードされている場合、一貫したユーザー体験を維持するために、すべてのナビゲーションリンクでそのロケールを保持する必要があります。ユーザーがサイトのフランス語版を閲覧していて/aboutへのリンクをクリックした場合、フランス語のまま/fr/aboutに移動することを期待します。ロケール対応リンクがない場合、ユーザーはセッションの途中でデフォルト言語に切り替わってしまい、閲覧コンテキストが壊れ、再度手動で言語を切り替える必要が生じます。これにより摩擦が生じ、ローカライズされた体験が損なわれます。

すべてのリンクにロケールプレフィックスをハードコーディングすることはエラーが発生しやすく、コードベースが脆弱になります。ユーザーがアプリケーション内を移動すると、アクティブなロケールが変更される可能性があり、数百のリンクを手動で更新することは持続不可能です。

解決策

URLから現在のロケールを自動的に読み取り、すべての内部ナビゲーションパスの先頭に追加するカスタムLinkコンポーネントを作成します。React RouterのLinkコンポーネントをラップすることで、ロケール処理を1か所に集中させます。ラッパーは現在のルートからロケールパラメータを抽出し、すべての宛先パスにそれを含めることで、手動介入なしにナビゲーションがユーザーの言語選択を保持することを保証します。

このアプローチにより、アプリケーション全体でリンク定義をクリーンでロケールに依存しない状態に保ちながら、ロケールコンテキストがすべてのクリックとともに移動することを保証します。

手順

1. ロケール対応Linkラッパーコンポーネントを作成

useParamsを使用してURLから現在のロケールを抽出し、React RouterのLinkコンポーネントをラップして宛先パスの先頭にロケールを追加するカスタムコンポーネントを構築します。

import { Link, useParams } from "react-router";
import type { LinkProps } from "react-router";

export function LocaleLink({ to, ...props }: LinkProps) {
  const { locale } = useParams<{ locale: string }>();

  const localizedTo =
    typeof to === "string"
      ? `/${locale}${to.startsWith("/") ? to : `/${to}`}`
      : {
          ...to,
          pathname: `/${locale}${to.pathname?.startsWith("/") ? to.pathname : `/${to.pathname}`}`,
        };

  return <Link to={localizedTo} {...props} />;
}

このコンポーネントは現在のルートからロケールパラメータを読み取り、toプロパティに渡すすべてのパスに自動的にプレフィックスを付け、文字列形式とオブジェクト形式の両方を処理します。

2. アプリケーション全体でLocaleLinkコンポーネントを使用する

ロケールを保持するナビゲーションが必要な場所では、標準のLinkコンポーネントをLocaleLinkに置き換えます。

import { LocaleLink } from "./LocaleLink";

export function Navigation() {
  return (
    <nav>
      <LocaleLink to="/">Home</LocaleLink>
      <LocaleLink to="/about">About</LocaleLink>
      <LocaleLink to="/products">Products</LocaleLink>
    </nav>
  );
}

/fr/productsにいるユーザーがAboutリンクをクリックすると、/fr/aboutに移動します。ロケールプレフィックスは、リンク定義を煩雑にすることなく自動的に追加されます。

3. 絶対パスと外部リンクのエッジケースを処理する

パスにすでにロケールが含まれている場合や外部URLを指している場合を検出するようにラッパーを拡張し、二重プレフィックスや外部ナビゲーションの破損を回避します。

import { Link, useParams } from "react-router";
import type { LinkProps } from "react-router";

export function LocaleLink({ to, ...props }: LinkProps) {
  const { locale } = useParams<{ locale: string }>();

  if (!locale) {
    return <Link to={to} {...props} />;
  }

  const isExternal =
    typeof to === "string" &&
    (to.startsWith("http://") || to.startsWith("https://"));
  const alreadyLocalized =
    typeof to === "string" && to.startsWith(`/${locale}/`);

  if (isExternal || alreadyLocalized) {
    return <Link to={to} {...props} />;
  }

  const localizedTo =
    typeof to === "string"
      ? `/${locale}${to.startsWith("/") ? to : `/${to}`}`
      : {
          ...to,
          pathname: `/${locale}${to.pathname?.startsWith("/") ? to.pathname : `/${to.pathname}`}`,
        };

  return <Link to={localizedTo} {...props} />;
}

これにより、パスがすでにロケールで始まっている場合の二重プレフィックスを防ぎ、外部URLをそのまま通過させることで、すべてのナビゲーションシナリオでコンポーネントが確実に機能します。