如何在 React Router v7 中跨会话记住语言选择

存储用户明确选择的语言

问题

当用户明确选择了一种语言时,这一选择代表了他们的偏好,应当优先于任何自动检测。如果没有持久化机制,这一选择会在浏览器关闭或会话结束时丢失。下次访问时,应用会重新开始,用户不得不再次选择语言。这种重复操作表明应用没有尊重用户的偏好,增加了使用阻力并降低了信任感。

解决方案

在用户选择语言时,将其偏好存储在持久化位置(如 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 有效期为一年,客户端代码可读取用户的语言偏好。

2. 添加用于存储语言选择的操作

创建一个 action,用于处理语言选择表单的提交,并将选择结果存储到 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. 在根 loader 中检查已存储的偏好设置

在根路由的 loader 中添加逻辑,检查是否有已存储的语言偏好,并据此进行重定向。

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;
}

当用户访问根路径时,此 loader 会检查是否有已存储的语言偏好,如果存在,则将其重定向到所选语言的路由。

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 值被篡改或支持的语言发生变化后,重定向到无效或不受支持的语言路由。