如何在 React Router v7 的导航链接中维护 locale

在内部导航中保持 locale 一致

问题

当 locale 信息被编码在 URL 路径中时,每一个导航链接都必须保留该 locale,以确保用户体验的一致性。如果用户正在浏览你网站的法语版本,并点击 /about,他们期望继续以法语浏览 /fr/about。如果链接不支持 locale 感知,用户会在会话中途被切换回默认语言,导致浏览上下文丢失,并被迫手动再次切换语言。这会增加操作难度,破坏本地化体验。

将 locale 前缀硬编码到每个链接中容易出错,并且会让代码库变得脆弱。随着用户在应用中不断导航,活跃的 locale 可能会变化,手动维护数百个链接变得不可持续。

解决方案

创建一个自定义 Link 组件,自动从 URL 读取当前 locale,并将其添加到所有内部导航路径前。通过封装 React Router 的 Link 组件,可以将 locale 处理集中在一个地方。该封装组件会从当前路由中提取 locale 参数,并确保每个目标路径都包含 locale,这样用户的语言选择在导航时就能自动保留,无需手动干预。

这种方式让整个应用中的链接定义保持简洁且与 locale 无关,同时确保每次点击都能携带 locale 上下文。

步骤

构建一个自定义组件,利用 useParams 从 URL 中提取当前 locale,并封装 React Router 的 Link 组件,将 locale 添加到目标路径前。

import { Link, useParams } from "react-router";
import type { LinkProps } from "react-router";

export function LocaleLink({ to, ...props }: LinkProps) {
  const { locale } = useParams<{ locale: string }>();

  const localizedTo =
    typeof to === "string"
      ? `/${locale}${to.startsWith("/") ? to : `/${to}`}`
      : {
          ...to,
          pathname: `/${locale}${to.pathname?.startsWith("/") ? to.pathname : `/${to.pathname}`}`,
        };

  return <Link to={localizedTo} {...props} />;
}

该组件会从当前路由中读取 locale 参数,并自动将其作为前缀添加到你传递给 to 属性的任意路径,无论是字符串还是对象形式。

需要保留 locale 的导航时,将标准 Link 组件替换为 LocaleLink。

import { LocaleLink } from "./LocaleLink";

export function Navigation() {
  return (
    <nav>
      <LocaleLink to="/">Home</LocaleLink>
      <LocaleLink to="/about">About</LocaleLink>
      <LocaleLink to="/products">Products</LocaleLink>
    </nav>
  );
}

/fr/products 上的用户点击 About 链接时,他们会跳转到 /fr/about。locale 前缀会自动添加,无需在链接定义中手动处理。

3. 处理绝对路径和外部链接的边界情况

扩展封装组件以检测路径是否已包含 locale 或指向外部 URL,避免重复添加前缀或破坏外部导航。

import { Link, useParams } from "react-router";
import type { LinkProps } from "react-router";

export function LocaleLink({ to, ...props }: LinkProps) {
  const { locale } = useParams<{ locale: string }>();

  if (!locale) {
    return <Link to={to} {...props} />;
  }

  const isExternal =
    typeof to === "string" &&
    (to.startsWith("http://") || to.startsWith("https://"));
  const alreadyLocalized =
    typeof to === "string" && to.startsWith(`/${locale}/`);

  if (isExternal || alreadyLocalized) {
    return <Link to={to} {...props} />;
  }

  const localizedTo =
    typeof to === "string"
      ? `/${locale}${to.startsWith("/") ? to : `/${to}`}`
      : {
          ...to,
          pathname: `/${locale}${to.pathname?.startsWith("/") ? to.pathname : `/${to.pathname}`}`,
        };

  return <Link to={localizedTo} {...props} />;
}

这样可以防止路径已以 locale 开头时重复添加前缀,并确保外部 URL 保持不变,从而保证组件在各种导航场景下都能可靠工作。