React Router v7でURLのロケールパラメータを検証する方法

サポートされていないロケールコードを適切に処理する

問題

ロケール識別子がURL構造の一部になると、任意の値を含む可能性のあるユーザー入力に変わります。ユーザーは手動で /xx/about/gibberish/contact、またはその他の無効なロケールコードをアドレスバーに入力する可能性があります。検証がなければ、アプリケーションはこれらの無効な入力をどのように処理するかを決定しなければなりません。無効なロケールを許可すると、翻訳の欠落、フォーマットの破損、またはi18nライブラリが存在しないロケールデータを読み込もうとした際の実行時エラーにつながる可能性があります。ユーザーに通知せずに静かにデフォルトロケールにフォールバックすると、ユーザーがどの言語を閲覧しているのかについて混乱が生じます。エラー境界や空白ページを表示すると、ユーザーは明確な進行経路がなく立ち往生してしまいます。

この課題は、ロケールの検証がコンポーネントのレンダリングや翻訳データの読み込みの前に、リクエストライフサイクルの早い段階で行われなければならないという事実によって複雑化します。検証が遅すぎると、アプリケーションはすでにロケール固有のリソースを取得しようとしたり、無効な設定でi18nプロバイダーを初期化したりして、リソースを無駄にし、潜在的に連鎖的な障害を引き起こす可能性があります。

解決策

ページがレンダリングされる前に、ルートローダーでロケールパラメータを検証します。React Routerのローダーはコンポーネントがマウントされる前に実行されるため、リクエストされたロケールがアプリケーションのサポートされている言語リストに存在するかどうかを確認するのに理想的な場所です。ロケールが有効であれば、リクエストを通常通り進めます。ロケールが無効であれば、ユーザーを安全なフォールバック(有効なデフォルトロケールを持つ同じパス、または問題を説明する専用の404ページ)に即座にリダイレクトします。

このアプローチにより、無効なロケールがコンポーネントやi18nプロバイダーに到達するのを防ぎます。ローダーからリダイレクトレスポンスを返すことで、React Routerの組み込みナビゲーションシステムを活用してエラーを適切に処理します。リダイレクトはSSR中にサーバー上で、またはナビゲーション中にクライアント上で発生し、レンダリング戦略全体で一貫した動作を保証します。ユーザーはURL変更を通じて即座にフィードバックを受け取り、アプリケーションは存在しないロケールのリソースを読み込もうとするのを避けます。

ステップ

1. サポートするロケールを定義する

アプリケーションがサポートする有効なロケールコードのリストを作成します。このリストは検証の信頼できる情報源として機能します。

export const SUPPORTED_LOCALES = ["en", "es", "fr", "de", "ja"] as const;

export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];

export function isValidLocale(locale: string): locale is SupportedLocale {
  return SUPPORTED_LOCALES.includes(locale as SupportedLocale);
}

このヘルパー関数は型安全な検証を提供し、ローダーやアプリケーションの他の部分で再利用できます。

2. ロケールプレフィックス付きルートローダーに検証を追加する

ロケールプレフィックス付きページのルートモジュールで、ロケールパラメータをチェックし、無効な場合はリダイレクトするローダーをエクスポートします。

import type { Route } from "./+types/page";
import { redirect } from "react-router";
import { isValidLocale } from "~/i18n/locales";

export async function loader({ params }: Route.LoaderArgs) {
  const { locale } = params;

  if (!locale || !isValidLocale(locale)) {
    return redirect("/en/not-found");
  }

  return { locale };
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>Content in {loaderData.locale}</h1>
    </div>
  );
}

ローダーはURLからロケールを抽出し、検証し、検証に失敗した場合は安全なフォールバックにリダイレクトします。ロケールが有効な場合、コンポーネントが使用できるデータを返します。

3. ロケールパラメータを含むルートを設定する

routes.tsファイルで、ロケールを動的セグメントとして含むルートを定義します。

import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route(":locale/about", "./routes/about.tsx"),
  route(":locale/contact", "./routes/contact.tsx"),
  route(":locale/not-found", "./routes/not-found.tsx"),
] satisfies RouteConfig;

:localeパラメータを持つ各ルートは、コンポーネントがレンダリングされる前に検証が行われるローダーを呼び出します。

4. 無効なロケールに対する404ページを作成する

ロケールが見つからないことを説明し、ナビゲーションオプションを提供する専用ページを構築します。

import { Link } from "react-router";
import { SUPPORTED_LOCALES } from "~/i18n/locales";

export default function NotFound() {
  return (
    <div>
      <h1>言語が見つかりません</h1>
      <p>リクエストされた言語はサポートされていません。</p>
      <nav>
        <p>言語を選択してください:</p>
        <ul>
          {SUPPORTED_LOCALES.map((locale) => (
            <li key={locale}>
              <Link to={`/${locale}`}>{locale.toUpperCase()}</Link>
            </li>
          ))}
        </ul>
      </nav>
    </div>
  );
}

このページは明確なフィードバックと実行可能な次のステップを提供し、ユーザーがアプリケーションを離れることなくエラーから回復するのを支援します。

5. 完全に無効なパスに対するキャッチオールルートを追加する

定義されたルートパターンに一致しないURLに対して、ルート設定の最後にスプラットルートを追加します。

import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route(":locale/about", "./routes/about.tsx"),
  route(":locale/contact", "./routes/contact.tsx"),
  route(":locale/not-found", "./routes/not-found.tsx"),
  route("*", "./routes/catch-all.tsx"),
] satisfies RouteConfig;

スプラットルートは、前のルートに一致しないパスに一致し、無効なロケールコードとは別に完全に不正な形式のURLを処理できるようにします。

6. オプションとして404ページの代わりにデフォルトロケールにリダイレクトする

エラーを表示するのではなく、無効なロケールを静かに修正することを好む場合は、デフォルトロケールで同じパスにリダイレクトします。

import type { Route } from "./+types/page";
import { redirect } from "react-router";
import { isValidLocale } from "~/i18n/locales";

const DEFAULT_LOCALE = "en";

export async function loader({ params, request }: Route.LoaderArgs) {
  const { locale } = params;

  if (!locale || !isValidLocale(locale)) {
    const url = new URL(request.url);
    const newPath = url.pathname.replace(/^\/[^/]+/, `/${DEFAULT_LOCALE}`);
    return redirect(newPath);
  }

  return { locale };
}

このアプローチは、無効なロケールセグメントのみを置き換えながらURLパスの残りの部分を保持し、ロケールが唯一の問題である場合にスムーズなユーザーエクスペリエンスを提供します。