React Router v7でセッション間の言語選択を記憶する方法

ユーザーが明示的に選択した言語を保存する

問題

ユーザーが明示的に言語を選択した場合、その選択はユーザーの好みを反映しており、自動検出よりも優先されるべきです。永続化がなければ、この選択はブラウザを閉じたりセッションが終了したりすると消えてしまいます。次回訪問時にアプリケーションは初期状態に戻り、ユーザーは再び言語を選択する必要があります。この繰り返しは、アプリケーションがユーザーの設定を尊重していないことを示し、摩擦を生み出し、信頼を損なうことになります。

解決策

ユーザーが言語を選択した際に、その選択をCookieなどの永続的な場所に保存します。その後の訪問時には、ブラウザのヘッダーやその他の検出方法に頼る前に、この保存された設定を確認します。有効な保存言語が見つかった場合、ユーザーを自動的にその言語のルートにリダイレクトします。これにより、ユーザーの明示的な選択が優先され、セッションをまたいで維持されることが保証されます。

手順

1. 言語設定を保存するためのCookieを作成する

長い有効期限を持つ、ユーザーが選択した言語を保持するCookieを定義します。

import { createCookie } from "react-router";

export const languagePreference = createCookie("language-preference", {
  maxAge: 31536000,
  httpOnly: false,
  secure: process.env.NODE_ENV === "production",
  sameSite: "lax",
});

このCookieは1年間持続し、設定を読み取るためにクライアントサイドのコードからアクセス可能です。

2. 言語選択を保存するアクションを追加する

言語選択フォームの送信を処理し、選択をCookieに保存するアクションを作成します。

import { redirect } from "react-router";
import type { Route } from "./+types/root";
import { languagePreference } from "./cookies";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const selectedLanguage = formData.get("language");

  if (typeof selectedLanguage === "string") {
    return redirect(`/${selectedLanguage}`, {
      headers: {
        "Set-Cookie": await languagePreference.serialize(selectedLanguage),
      },
    });
  }

  return redirect("/");
}

ユーザーが言語選択を送信すると、このアクションはそれをCookieに保存し、適切な言語ルートにリダイレクトします。

3. 言語選択コンポーネントを作成する

ユーザーが希望する言語を選択できるフォームコンポーネントを構築します。

import { Form } from "react-router";

export function LanguageSelector({
  currentLanguage,
}: {
  currentLanguage: string;
}) {
  return (
    <Form method="post">
      <select
        name="language"
        defaultValue={currentLanguage}
        onChange={(e) => e.currentTarget.form?.requestSubmit()}
      >
        <option value="en">English</option>
        <option value="es">Español</option>
        <option value="fr">Français</option>
        <option value="de">Deutsch</option>
      </select>
    </Form>
  );
}

このコンポーネントは、ユーザーが選択を変更すると自動的に送信され、設定を保存するアクションがトリガーされます。

4. ルートローダーで保存された設定を確認する

ルートルートローダーに、保存された言語設定を確認し、それに応じてリダイレクトするロジックを追加します。

import { redirect } from "react-router";
import type { Route } from "./+types/root";
import { languagePreference } from "./cookies";

export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const cookieHeader = request.headers.get("Cookie");
  const storedLanguage = await languagePreference.parse(cookieHeader);

  if (url.pathname === "/" && storedLanguage) {
    return redirect(`/${storedLanguage}`);
  }

  return null;
}

ユーザーがルートパスにアクセスすると、このローダーは保存された言語設定を確認し、設定が存在する場合は選択した言語ルートにリダイレクトします。

5. 保存された言語をサポートされているロケールと照合して検証する

リダイレクトに使用する前に、保存された設定が有効であることを確認します。

import { redirect } from "react-router";
import type { Route } from "./+types/root";
import { languagePreference } from "./cookies";

const SUPPORTED_LANGUAGES = ["en", "es", "fr", "de"];

export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const cookieHeader = request.headers.get("Cookie");
  const storedLanguage = await languagePreference.parse(cookieHeader);

  if (
    url.pathname === "/" &&
    storedLanguage &&
    SUPPORTED_LANGUAGES.includes(storedLanguage)
  ) {
    return redirect(`/${storedLanguage}`);
  }

  return null;
}

この検証により、Cookieの値が改ざんされた場合や、設定が保存された後にサポートされている言語が変更された場合に、無効または非サポートの言語ルートへのリダイレクトを防止します。