如何在 Next.js(Pages Router)v16 中构建语言切换组件

在同一页面切换语言

问题

用户期望语言切换器能够保留他们当前所在的位置。当用户在英文的产品页面上切换到西班牙语时,他们希望看到同一个产品页面的西班牙语版本,而不是被重定向到首页。如果打破了这一预期,会增加操作的阻力,迫使用户重新导航回原页面,严重影响体验,甚至可能导致用户放弃当前操作。

许多语言切换器实现失败的原因在于,它们将语言选择仅仅视为一次普通的导航,而不是对当前视图的转换。如果无法获取当前页面的路由信息,切换器只能链接到固定的目标页面,从而丢失用户的上下文。

解决方案

构建一个语言切换器,从路由器中读取当前路由的 pathname 和 query 参数,然后为每个支持的语言生成链接,在切换语言时仅更改 locale,同时保留路由信息。通过将 pathname 和 query 与目标 locale 一起传递给导航 API,切换器可以确保用户在新语言下停留在等效页面。

步骤

1. 创建一个能够读取当前路由的语言切换组件

router 对象提供 pathnameasPathquerylocalelocales 属性,包含构建支持多语言链接所需的全部信息。

import { useRouter } from "next/router";
import Link from "next/link";

export default function LanguageSwitcher() {
  const router = useRouter();
  const { locale, locales, pathname, asPath, query } = router;

  return (
    <nav>
      {locales?.map((loc) => (
        <Link key={loc} href={{ pathname, query }} as={asPath} locale={loc}>
          {loc.toUpperCase()}
        </Link>
      ))}
    </nav>
  );
}

Link 组件接受 locale 属性,用于从当前激活的语言切换到其他语言。将 pathnamequery 作为对象传递给 href,可以保留所有路由信息,包括动态路由的 query 参数值。

2. 为当前语言环境添加样式以提供视觉反馈

高亮显示当前语言,让用户清楚正在查看哪个语言环境。

import { useRouter } from "next/router";
import Link from "next/link";

export default function LanguageSwitcher() {
  const router = useRouter();
  const { locale, locales, pathname, asPath, query } = router;

  return (
    <nav>
      {locales?.map((loc) => {
        const isActive = loc === locale;
        return (
          <Link
            key={loc}
            href={{ pathname, query }}
            as={asPath}
            locale={loc}
            style={{
              fontWeight: isActive ? "bold" : "normal",
              textDecoration: isActive ? "none" : "underline",
              marginRight: "1rem",
            }}
          >
            {loc.toUpperCase()}
          </Link>
        );
      })}
    </nav>
  );
}

将每个语言环境与当前 locale 值进行比较,可以识别当前激活的语言,并应用不同的样式以区分其他可用选项。

3. 使用 react-intl 添加无障碍标签

用可读性更强的语言名称替换语言代码,以提升可用性。

import { useRouter } from "next/router";
import { useIntl } from "react-intl";
import Link from "next/link";

const localeNames: Record<string, string> = {
  en: "English",
  es: "Español",
  fr: "Français",
  de: "Deutsch",
};

export default function LanguageSwitcher() {
  const router = useRouter();
  const intl = useIntl();
  const { locale, locales, pathname, asPath, query } = router;

  return (
    <nav
      aria-label={intl.formatMessage({
        id: "languageSwitcher.label",
        defaultMessage: "Select language",
      })}
    >
      {locales?.map((loc) => {
        const isActive = loc === locale;
        return (
          <Link
            key={loc}
            href={{ pathname, query }}
            as={asPath}
            locale={loc}
            aria-current={isActive ? "true" : undefined}
            style={{
              fontWeight: isActive ? "bold" : "normal",
              textDecoration: isActive ? "none" : "underline",
              marginRight: "1rem",
            }}
          >
            {localeNames[loc] || loc}
          </Link>
        );
      })}
    </nav>
  );
}

useIntl hook 提供了用于翻译 UI 标签的格式化函数。aria-labelaria-current 属性可提升屏幕阅读器用户的无障碍体验。