如何在 React Router v7 中验证 URL 中的语言环境参数

优雅地处理不支持的语言环境代码

问题

当区域标识符成为 URL 结构的一部分时,它们会转变为用户输入,可能包含任意值。用户可能会手动输入 /xx/about/gibberish/contact 或其他任何无效的区域代码到地址栏中。如果没有进行验证,应用程序必须决定如何处理这些无效输入。允许无效的区域代码继续可能会导致翻译缺失、格式破损,或者当 i18n 库尝试加载不存在的区域数据时出现运行时错误。默默地回退到默认区域而不通知用户会让用户对他们正在查看的语言感到困惑。显示错误边界或空白页面会让用户陷入困境,没有明确的前进路径。

这一挑战因区域验证必须在请求生命周期的早期阶段完成而变得更加复杂,即在组件渲染之前和翻译数据加载之前。如果验证发生得太晚,应用程序可能已经尝试获取特定于区域的资源或用无效配置初始化 i18n 提供程序,从而浪费资源并可能导致级联故障。

解决方案

在页面渲染之前,在路由加载器中验证区域参数。React Router 中的加载器在组件挂载之前运行,使其成为检查请求的区域是否存在于应用程序支持的语言列表中的理想位置。如果区域有效,则允许请求正常进行。如果区域无效,则立即将用户重定向到安全的回退路径——可以是带有有效默认区域的相同路径,或者是一个专门的未找到页面来解释问题。

这种方法可以防止无效的区域代码到达您的组件和 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>内容在 {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. 为无效的语言区域创建一个未找到页面

构建一个专门的页面,说明未找到语言区域,并提供导航选项。

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. 可选地重定向到默认语言区域而不是未找到页面

如果您更倾向于静默修正无效的语言区域,而不是显示错误,可以重定向到具有默认语言区域的相同路径。

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 其余路径的同时,仅替换无效的语言区域部分,为用户提供更流畅的体验,当问题仅在于语言区域时。