如何在 React Router v7 中构建语言切换组件

在同一页面切换语言

问题

用户期望语言切换器能够保留当前的浏览上下文。当浏览产品页面、帮助文档或账户设置时,从英文切换到西班牙文应显示同一页面的西班牙语版本。然而,许多实现方式将语言选择视为一次导航事件,直接将用户重定向到新语言的首页,用户不得不重新返回原来的页面。这会打断用户的操作流程,尤其是在内容较多、导航层级较深的应用中,用户体验会受到很大影响。

根本原因在于,语言切换器通常使用硬编码的目标 URL,而不是基于当前页面动态生成 URL。如果不读取并转换当前的 URL 结构,切换器就无法在切换语言时保持用户在应用中的位置。

解决方案

构建一个语言切换组件,能够读取当前 URL,提取当前激活的 locale 参数和剩余的路径片段。对于每种支持的语言,仅替换 locale 部分,保留其他路径和查询参数,动态生成新的 URL。将这些 URL 渲染为链接,用户即可在不丢失当前位置的情况下切换语言。

这种方法将 locale 作为 URL 结构中的可替换参数,而不是导航目标,从而确保从 /en/products/shoes 切换到 /es/products/shoes 时,用户的上下文能够被保留。

步骤

1. 创建一个用于构建支持多语言 URL 的辅助函数

定义一个函数,接收当前路径名和目标语言区域,然后通过替换语言区域片段来构建新的路径。

export function buildLocalePath(
  currentPath: string,
  newLocale: string,
): string {
  const segments = currentPath.split("/").filter(Boolean);

  if (segments.length === 0) {
    return `/${newLocale}`;
  }

  segments[0] = newLocale;
  return `/${segments.join("/")}`;
}

该函数会将路径名拆分为多个片段,用目标语言区域替换第一个片段,并重组路径。它能够处理根路径等边界情况,并确保语言区域始终作为第一个片段。

2. 定义支持的语言区域

创建一个配置对象,列出应用支持的所有语言。

export const locales = [
  { code: "en", label: "English" },
  { code: "es", label: "Español" },
  { code: "fr", label: "Français" },
  { code: "de", label: "Deutsch" },
];

该配置作为语言切换器展示语言的唯一数据来源,并为每个语言区域提供用户友好的标签。

3. 构建语言切换组件

创建一个组件,读取当前地址,判断当前激活的语言区域,并为所有其他支持的语言渲染链接。

import { Link, useLocation, useParams } from "react-router";
import { locales, buildLocalePath } from "./i18n-config";

export function LanguageSwitcher() {
  const location = useLocation();
  const params = useParams();
  const currentLocale = params.locale || "en";

  return (
    <nav aria-label="Language switcher">
      <ul>
        {locales.map((locale) => {
          const isActive = locale.code === currentLocale;
          const newPath = buildLocalePath(location.pathname, locale.code);

          return (
            <li key={locale.code}>
              {isActive ? (
                <span aria-current="true">{locale.label}</span>
              ) : (
                <Link to={newPath}>{locale.label}</Link>
              )}
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

该组件使用 useLocation 获取当前路径名,并用 useParams 从 URL 中提取当前语言区域。对于每个支持的语言区域,使用辅助函数生成新路径,并为当前语言渲染非交互元素,其余渲染为链接。

4. 保留查询参数和哈希片段

扩展辅助函数,在切换语言时保留查询字符串和 URL 片段。

export function buildLocalePath(
  currentPath: string,
  search: string,
  hash: string,
  newLocale: string,
): string {
  const segments = currentPath.split("/").filter(Boolean);

  if (segments.length === 0) {
    return `/${newLocale}${search}${hash}`;
  }

  segments[0] = newLocale;
  return `/${segments.join("/")}${search}${hash}`;
}

此更新版本会接收 location 对象中的 search 和 hash 属性,并将其追加到生成的路径,确保过滤、排序参数和锚点在切换语言时得以保留。

5. 更新组件以使用增强后的辅助函数

修改切换器,将完整的位置信息传递给辅助函数。

import { Link, useLocation, useParams } from "react-router";
import { locales, buildLocalePath } from "./i18n-config";

export function LanguageSwitcher() {
  const location = useLocation();
  const params = useParams();
  const currentLocale = params.locale || "en";

  return (
    <nav aria-label="Language switcher">
      <ul>
        {locales.map((locale) => {
          const isActive = locale.code === currentLocale;
          const newPath = buildLocalePath(
            location.pathname,
            location.search,
            location.hash,
            locale.code,
          );

          return (
            <li key={locale.code}>
              {isActive ? (
                <span aria-current="true">{locale.label}</span>
              ) : (
                <Link to={newPath}>{locale.label}</Link>
              )}
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

该组件现在将 location.searchlocation.hash 传递给辅助函数,确保在切换到 Spanish 时,像 /en/products?category=shoes#reviews 这样的 URL 会变为 /es/products?category=shoes#reviews