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

内部ナビゲーション間でロケールを維持する

問題

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

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

解決策

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

このアプローチにより、アプリケーション全体でリンク定義をクリーンでロケールに依存しないままにしながら、ロケールコンテキストがクリックごとに引き継がれることを保証します。

ステップ

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を変更せずに渡すことで、コンポーネントがすべてのナビゲーションシナリオで確実に機能するようになります。