如何在 React Router v7 中验证 URL 中的 locale 参数
优雅地处理不受支持的语言代码
问题
当 locale 标识符成为 URL 结构的一部分时,它就变成了用户输入,可能包含任意值。用户可能会手动在地址栏输入 /xx/about、/gibberish/contact 或其他无效的 locale 代码。如果没有进行验证,应用程序必须决定如何处理这些无效输入。允许无效的 locale 继续处理,可能导致缺失翻译、格式错误,或者在 i18n 库尝试加载不存在的 locale 数据时出现运行时错误。如果静默回退到默认 locale 而不告知用户,会让用户对当前所用语言产生困惑。显示错误边界或空白页则会让用户陷入困境,无法继续操作。
挑战在于,locale 验证必须在请求生命周期的早期进行,也就是在组件渲染和加载翻译数据之前。如果验证发生得太晚,应用可能已经尝试获取特定 locale 的资源,或用无效配置初始化了 i18n provider,导致资源浪费,甚至引发级联故障。
解决方案
在页面渲染前,在路由 loader 中验证 locale 参数。React Router 的 loader 会在组件挂载前运行,非常适合用来检查请求的 locale 是否在应用支持的语言列表中。如果 locale 有效,则正常处理请求;如果无效,则立即将用户重定向到安全的回退路径——可以是带有有效默认 locale 的同一路径,或是专门说明问题的 not-found 页面。
这种方法可以防止无效的 locale 传递到你的组件和 i18n provider。通过在 loader 中返回重定向响应,你可以利用 React Router 内置的导航系统优雅地处理错误。重定向会在 SSR(服务器端渲染)期间或客户端导航时发生,从而确保不同渲染策略下行为一致。用户会通过 URL 变化获得即时反馈,应用也能避免尝试加载不存在的 locale 资源。
步骤
1. 定义支持的 locale
创建一个包含应用支持的有效 locale 代码的列表。该列表作为校验的唯一数据源。
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);
}
此辅助函数提供类型安全的校验,并可在 loader 及应用其他部分复用。
2. 在带有 locale 前缀的路由 loader 中添加校验
在带有 locale 前缀页面的路由模块中,导出一个 loader,用于检查 locale 参数并在无效时进行重定向。
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>
);
}
loader 会从 URL 中提取 locale,进行校验,如果校验失败则重定向到安全的 fallback。如果 locale 有效,则返回组件可用的数据。
3. 配置带有 locale 参数的路由
在 routes.ts 文件中,定义包含 locale 动态片段的路由。
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 参数的路由都会调用其 loader,校验会在组件渲染前进行。
4. 为无效 locale 创建 not-found 页面
构建一个专门页面,说明未找到该 locale,并提供导航选项。
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,请在路由配置末尾添加一个通配(splat)路由。
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 路径的其他部分,仅替换无效的语言区域段,从而在仅语言区域有误时为用户提供更流畅的体验。