# General Translation Knowledge Base: 语言切换器最佳实践：用户体验、SEO 与无障碍访问
URL: https://generaltranslation.com/zh/kb/language-switcher.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 用每种语言的本语名称来标注它，例如 Deutsch 或 日本語，不要使用国旗，因为国旗代表的是国家。每个选项都应通过可抓取的链接指向该语言版本的同一页面，用 cookie 记住用户的选择，并且切勿对已本地化的 URL 自动重定向。
Category: 国际化
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

最需要语言切换器的，恰恰是看不懂当前落地页面的人。因此，下文的每一项决策都遵循同一条原则：即使用户不懂你的默认语言，也必须能看懂这个控件。

## 怎样才算好的语言切换器？

### 1. 用各语言的本名来标注

日语读者在英文页面上找的是“日本語”，而不是“Japanese”；德语读者找的则是“Deutsch”。W3C 的[语言菜单指南](https://www.w3.org/International/questions/qa-navigation-select)建议写“français”，而不是“French”。这种语言对自身的称呼称为自称名 (endonym) ，JavaScript 的 [`Intl.DisplayNames`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) 可以根据区域设置代码直接生成：

```js
const locales = ['en', 'fr', 'de', 'ja', 'ar', 'pt-BR', 'zh-Hant'];

for (const code of locales) {
  // 用每种语言自己的语言写出其名称（即自称）
  const own = new Intl.DisplayNames([code], { type: 'language' }).of(code);
  // 对应的英文名称，用于对比
  const english = new Intl.DisplayNames(['en'], { type: 'language' }).of(code);
  console.log(`${code}: ${own} (${english})`);
}
```

Node.js 24 (ICU 78.3、CLDR 48.0) 中的输出结果：

```text
en: English (English)
fr: français (French)
de: Deutsch (German)
ja: 日本語 (Japanese)
ar: العربية (Arabic)
pt-BR: português (Brasil) (Brazilian Portuguese)
zh-Hant: 繁體中文 (Traditional Chinese)
```

法语和葡萄牙语在句中不会将语言名称首字母大写，因此 CLDR 返回的是 &quot;français&quot;。而在菜单中，每个名称都单独显示，因此应将首字母大写。只有当你为同一种语言提供两个变体时，才需要标注地区，例如让 &quot;Português (Brasil)&quot; 与 &quot;Português (Portugal)&quot; 并列显示。

### 2. 不要用国旗表示语言

W3C 的[语言声明最佳实践](https://www.w3.org/TR/i18n-html-tech-lang/)明确指出：“不要使用国旗图标来表示语言。”其[理由](https://www.w3.org/International/questions/qa-link-lang)是：“国旗代表的是国家，而不是语言。”一种语言可能通行于多个国家 (例如西班牙语在西班牙、墨西哥和阿根廷都有使用) ，一个国家也可能有多种官方语言 (如瑞士) ，而且国旗可能带有国家色彩，令使用同一语言的其他人群感到不适。国旗适合用于国家或商店选择器，因为这类选择关乎的是地区；同时应搭配列出该地区可用的语言名称。

### 3. 放在用户视线所及之处，并使用易于识别的触发控件

W3C [建议](https://www.w3.org/International/questions/qa-navigation-select)将该控件放在“页面顶部或其附近”，而且由于文本标签是 language-specific 的，最好在旁边配上一个易于识别的图像。目前还没有哪个符号能被普遍识别，不过 W3C 指出，地球图标正日渐流行。在当前语言名称旁边放一个地球图标，任何语言的读者都能看懂。许多网站还会在页脚再放一个语言切换器，作为第二个入口，但不要把页脚作为唯一的入口。

### 4. 切换到同一页面，而不是首页

用户正在阅读英文版定价页面，切换到法语后，想看到的是法语版定价页面。如果直接把他们带到法语首页，他们就会找不到原来的位置，还得用一种可能直到现在才看得懂的语言重新查找。请将每个选项链接到当前 URL 对应的翻译版本；如果某个页面尚未翻译，则链接到距离最近的已翻译上级页面，并向用户说明这一点。

### 5. 使用带有 `hreflang` 和 `lang` 的可抓取链接

一般来说，Google [只能抓取带有 `href` 的 `<a>` 元素形式的链接](https://developers.google.com/search/docs/crawling-indexing/links-crawlable)，并且要求多语言网站[添加指向其他语言版本的超链接](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites)，方便用户点击选择。如果 `<select>` 是通过 change 处理函数设置 `window.location` 来跳转的，那它就不是链接，爬虫也就无从跟踪。

为每个链接添加两个属性。`<a>` 上的 `hreflang` 属性用于[提示所链接页面的语言](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a)；它没有任何内置的浏览器行为，与搜索引擎读取的 `<link rel="alternate" hreflang>` 注释也是两回事 (参见[什么是 hreflang](/kb/what-is-hreflang)) 。`lang` 属性用于标记链接文本的语言，这样屏幕阅读器朗读页面时就能按德语规则读出“Deutsch”。WCAG 的 [Language of Parts](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html) 准则正是以用各自语言命名的语言链接为例。该准则也指出了一个局限：当辅助技术仅凭无障碍名称播报链接时 (例如在页面链接列表中) ，不会切换发音规则。

### 6. 记住用户的明确选择

用户选择语言后，应将该选择存储在 cookie 或用户账户中，并在后续访问时使用：一是当用户访问 `/` 等中性根路径时，据此决定跳转目标；二是在语言切换器中显示当前选择。但不要让 cookie 覆盖用户所打开的本地化 URL。W3C [建议](https://www.w3.org/International/questions/qa-accept-lang-locales)&quot;将结果存储在数据库或 cookie 中，供后续访问使用&quot;，[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) 也指出，服务器&quot;绝不应覆盖用户明确做出的语言选择&quot;。

### 7. 不要在每次访问时自动切换

如果每次请求都根据浏览器语言重定向，旅行者、使用共享电脑的人，以及通过特定语言链接打开页面的用户都会被困在当前语言中。浏览器偏好只应用来显示一条可关闭的提示 (“Diese Seite gibt es auch auf Deutsch”) ，把选择权交给用户。[区域设置路由与 SEO](/kb/locale-routing-seo) 介绍了应以哪种信号为准，以及唯一可以安全重定向的场景。

### 8. 确保支持键盘操作和屏幕阅读器访问

由普通链接组成的列表或原生 `<select>` 开箱即用地支持键盘和辅助技术。如果你要自行构建下拉菜单，请遵循 WAI-ARIA [展开/折叠 (disclosure) 模式](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/)：用一个 `<button>` 控制列表的显示与隐藏，按 Enter 键和空格键可展开或收起列表，并通过 `aria-expanded` 反映其当前状态。为该控件设置一个使用页面语言的无障碍名称 (例如在德语页面上使用 `aria-label="Sprache"`) ，并用 `aria-current="page"` 标记当前语言。

## 如何构建语言切换器？

### 纯 HTML

此菜单落实了全部八项实践：使用语言自称 (endonym) 、用地球图标代替国旗、链接到同一页面、为每个链接设置 `hreflang` 和 `lang`、使用展开/收起按钮，以及用 cookie 保存用户的明确选择。

```html title="On /de/pricing"
<nav aria-label="Sprache">
  <button type="button" aria-expanded="false" aria-controls="language-menu">
    <svg aria-hidden="true" width="16" height="16"><!-- 地球图标 --></svg>
    Deutsch
  </button>
  <ul id="language-menu" hidden>
    <li><a href="/en/pricing" hreflang="en" lang="en">English</a></li>
    <li><a href="/fr/pricing" hreflang="fr" lang="fr">Français</a></li>
    <li><a href="/de/pricing" hreflang="de" lang="de" aria-current="page">Deutsch</a></li>
    <li><a href="/ja/pricing" hreflang="ja" lang="ja">日本語</a></li>
  </ul>
</nav>
```

```js title="switcher.js"
const button = document.querySelector('[aria-controls="language-menu"]');
const menu = document.getElementById('language-menu');

// 打开和关闭菜单（<button> 原生支持用 Enter 和 Space 键激活）
button.addEventListener('click', () => {
  const open = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', String(!open));
  menu.hidden = open;
});

// 按 Escape 键收起菜单，并将焦点移回其按钮。
button.closest('nav').addEventListener('keydown', (event) => {
  if (event.key === 'Escape' && !menu.hidden) {
    menu.hidden = true;
    button.setAttribute('aria-expanded', 'false');
    button.focus();
  }
});

// 记住用户的明确选择，以便下次访问时使用
menu.addEventListener('click', (event) => {
  const link = event.target.closest('a[hreflang]');
  if (link) {
    document.cookie = `locale=${link.hreflang}; path=/; max-age=31536000; samesite=lax`;
  }
});
```

### React

该组件使用普通链接。请将当前路径、查询字符串和片段作为 `currentHref` 传入，并同时传入当前区域设置。此组件的前提是：每个区域设置都有 URL 前缀，每个页面在所列出的所有区域设置中均存在，且各语言的 slug 完全相同。如果 slug 经过翻译或部分页面缺失，请根据路由或 CMS 数据解析每个目标地址，而不是直接替换前缀。

```jsx title="LanguageSwitcher.jsx"
const LOCALES = ['en', 'fr', 'de', 'ja'];

// 切换区域设置时保留路径、尾部斜杠、查询字符串和片段。
function hrefForLocale(currentHref, locale) {
  const url = new URL(currentHref, 'https://example.invalid');
  const trailingSlash = url.pathname.endsWith('/');
  const segments = url.pathname.split('/').filter(Boolean);
  if (LOCALES.includes(segments[0])) segments.shift();
  url.pathname = `/${[locale, ...segments].join('/')}${trailingSlash ? '/' : ''}`;
  return `${url.pathname}${url.search}${url.hash}`;
}

// 该语言的自称（用其自身语言书写的名称），首字母大写以用于菜单
function endonym(locale) {
  const name = new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
  return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
}

function rememberChoice(locale) {
  document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
}

export function LanguageSwitcher({ currentHref, currentLocale, label }) {
  return (
    <nav aria-label={label}>
      <ul>
        {LOCALES.map((locale) => (
          <li key={locale}>
            <a
              href={hrefForLocale(currentHref, locale)}
              hrefLang={locale}
              lang={locale}
              aria-current={locale === currentLocale ? 'page' : undefined}
              onClick={() => rememberChoice(locale)}
            >
              {endonym(locale)}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}
```

当 `currentHref="/fr/pricing/?plan=pro#faq"`、`currentLocale="fr"` 且 `label="Langue"` 时，德语链接为 `/de/pricing/?plan=pro#faq`，法语链接则会标记为 `aria-current="page"`。只有当查询参数和片段在目标页面上含义不变时，才应保留它们。如果锚点 ID 经过翻译，还需解析出对应的片段。

### 使用 General Translation

对于公开的 Next.js 页面，GT 的 [`<Link>`](/docs/react/nextjs/link) 会生成符合区域设置的链接。设置 `prefixDefaultLocale: true` 后，这些链接将指向 `/en/pricing` 和 `/de/pricing`。本示例假设英语和德语的定价页面均已发布。传入当前区域设置和本地化的导航标签：

```tsx
import Link from 'gt-next/link';

export function PricingLanguages({ currentLocale, label }: { currentLocale: string; label: string }) {
  return (
    <nav aria-label={label}>
      <Link href="/pricing" locale="en" hrefLang="en" lang="en" aria-current={currentLocale === 'en' ? 'page' : undefined}>English</Link>
      {' · '}
      <Link href="/pricing" locale="de" hrefLang="de" lang="de" aria-current={currentLocale === 'de' ? 'page' : undefined}>Deutsch</Link>
    </nav>
  );
}
```

将当前页面的导航标签本地化。如果需要紧凑的原生控件 (例如在需要登录的应用内) ，请使用区域设置选择器：

[`<LocaleSelector>`](/docs/react/reference/components/locale-selector) 可在 `gt-react`、`gt-next` 以及实验性的 `gt-tanstack-start` 中使用。它会渲染一个原生 `<select>`，列出你配置的所有区域设置，每个选项都以其自身语言显示名称，并将用户的选择保存到 cookie 中。标准的 `<select>` 属性会透传给该组件，因此你可以传入一个用 [`useGT`](/docs/react/reference/hooks/use-gt) 翻译的 `aria-label`：

```tsx
'use client';
import { LocaleSelector, useGT } from 'gt-next';

export function Header() {
  const gt = useGT();
  return <LocaleSelector aria-label={gt('Language')} />;
}
```

对于公开页面，请使用上方的链接提供可供爬虫抓取的导航，并在页面 head 中声明 hreflang 替代项，具体做法参见[多语言 Next.js SEO](/kb/multilingual-nextjs-seo)。如需自定义设计，可使用 [`useLocaleSelector`](/docs/react/reference/hooks/use-locale-selector)，它会返回当前区域设置、区域设置列表以及 `setLocale`。在 Vue 中，请参照 [Vue 区域设置指南](/docs/vue/guides/managing-locales#switcher)，使用 [`useLocale`](/docs/vue/reference/composables/use-locale) 和 [`useSetLocale`](/docs/vue/reference/composables/use-set-locale) 构建切换器。

## 要点总结

* 使用语言的自称名称 (endonym) 作为选项标签，例如 Français 和 日本語；`Intl.DisplayNames` 可以生成这些名称，其中法语和葡萄牙语名称为小写。
* 使用地球图标，而不是国旗：W3C 明确指出“不要使用国旗图标来表示语言”。
* 使用带有 `hreflang` 和 `lang` 属性的普通 `<a href>` 链接，指向同一页面的各语言版本，这样爬虫可以跟踪这些链接，屏幕阅读器也能正确朗读每个语言名称。
* 保存用户明确做出的选择，切勿根据浏览器语言将用户从已本地化的 URL 重定向到其他页面。
* General Translation 的 [`<LocaleSelector>`](/docs/react/reference/components/locale-selector) 会渲染一个原生 `<select>`，以各语言的本族语名称作为标签，并将用户的选择保存在 cookie 中；此外还需为爬虫添加普通链接。

## Related questions

- [如何检测用户的浏览器语言？](https://generaltranslation.com/zh/kb/detect-user-language.mdx): 读取浏览器提供的有序语言列表（在 JavaScript 中为 navigator.languages，在服务器端为 Accept-Language 标头），按 q 值排序，再与你支持的语言区域进行匹配，并设置后备语言区域。Safari 和 Chrome 无痕模式只会报告一种语言，而 IP 地理定位只能反映用户所在位置，无法反映其使用的语言。
- [语言重定向与 SEO：URL、cookie 还是 Accept-Language？](https://generaltranslation.com/zh/kb/locale-routing-seo.mdx): 凡是希望被搜索引擎收录的页面，都应由 URL 决定其语言，因为 Googlebot 不会发送 Accept-Language 请求头，而且大多从美国 IP 地址抓取。只有在决定 / 这类中性根路径重定向到哪里时，才应优先参考 cookie 中保存的用户选择，其次是 Accept-Language，并使用临时的 302 或 307 重定向。
- [什么是 hreflang？如何设置？](https://generaltranslation.com/zh/kb/what-is-hreflang.mdx): Hreflang 用于告诉 Google 哪些 URL 是同一页面的不同语言或区域版本，以便在搜索结果中展示正确的版本。你可以通过 HTML link 标签、HTTP Link 响应头或站点地图来添加它，确保所有链接相互指向，并使用有效的代码，例如 en-GB，而不是 en-UK。
- [如何让网站支持多语言？](https://generaltranslation.com/zh/kb/make-website-multilingual.mdx): 先确定译文放在哪里：代理、CMS 插件、建站工具，还是你自己的代码；然后按照 Google 的建议，为每种语言分配独立的 URL。完整翻译页面以及标题和元描述，用 hreflang 关联各语言版本，并提供语言切换器，而不是自动重定向访问者。

## Previous and next questions

- Previous question: [应该如何命名 i18n 翻译键？](https://generaltranslation.com/zh/kb/i18n-key-naming.mdx)
- Next question: [语言重定向与 SEO：URL、cookie 还是 Accept-Language？](https://generaltranslation.com/zh/kb/locale-routing-seo.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/zh/kb/sitemap.md) for every article.
