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

在同一页面上切换语言

问题

用户期望语言切换器能够保留他们当前的位置。当用户在查看英文的产品页面时切换到西班牙语,他们希望看到的是同一个产品页面的西班牙语版本,而不是被重定向到主页。打破这种期望会增加使用的阻力,迫使用户重新导航回原来的位置,从而降低体验,甚至可能导致用户完全放弃任务。

许多语言切换器的实现失败是因为它们将语言选择视为简单的导航,而不是当前视图的转换。如果无法访问当前页面的路由信息,切换器只能链接到固定的目标,从而丢失用户的上下文。

解决方案

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

步骤

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

路由器对象提供了 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 可以保留所有路由信息,包括动态路由查询值。

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 钩子提供了用于翻译 UI 标签的格式化功能。aria-labelaria-current 属性提高了屏幕阅读器用户的可访问性。