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>Language Not Found</h1>
      <p>The requested language is not supported.</p>
      <nav>
        <p>Choose a language:</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. 必要に応じて、not-foundページの代わりにデフォルトロケールにリダイレクトする

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

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パスの残りの部分を保持し、ロケールのみが問題である場合により滑らかなユーザーエクスペリエンスを提供します。