# General Translation Knowledge Base: 如何检测用户的浏览器语言？
URL: https://generaltranslation.com/zh/kb/detect-user-language.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 读取浏览器提供的有序语言列表（在 JavaScript 中为 navigator.languages，在服务器端为 Accept-Language 标头），按 q 值排序，再与你支持的语言区域进行匹配，并设置后备语言区域。Safari 和 Chrome 无痕模式只会报告一种语言，而 IP 地理定位只能反映用户所在位置，无法反映其使用的语言。
Category: 国际化
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

答案其实就在浏览器里：用户在浏览器或操作系统中设置的一份有序语言列表。你要做的是正确读取这份列表，将其与你已有译文的语言区域进行匹配，并判断何时应让更可靠的信号优先生效。

## 用户的语言偏好从何而来？

有五种信号可以帮你判断应显示哪种语言，但其中只有一部分真正与语言相关。

| 信号                    | 读取位置           | 提供的信息           | 可靠性                | 适用场景                 |
| --------------------- | -------------- | --------------- | ------------------ | -------------------- |
| `navigator.languages` | 浏览器 JavaScript | 按优先级排列的语言偏好     | 较好，但部分浏览器只提供一种语言   | 首次访问时的客户端渲染页面        |
| `Accept-Language` 请求头 | 每个 HTTP 请求     | 同一列表，并以 q 值标注权重 | 较好，但用户在旅行时往往无法自行控制 | 首次访问时的服务器端渲染页面       |
| URL 中的区域设置            | 请求路径，例如 `/fr/` | 访问者请求的语言版本      | 高                  | 公开、可分享的页面            |
| 已保存的偏好                | cookie 或账户设置   | 用户之前做出的选择       | 高                  | 再次访问                 |
| IP 地理定位               | 查找服务           | 大致位置            | 并非语言信号             | 按地区设置默认值 (如货币) ，而非语言 |

### 在浏览器中：`navigator.language` 和 `navigator.languages`

[`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) 以 BCP 47 标签的形式返回用户的首选语言，按偏好从高到低排列；[`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language) 则是该数组的第一个元素：

```js
navigator.language;  // "en-US"
navigator.languages; // ["en-US", "zh-CN", "ja-JP"]
```

当用户更改语言偏好时，浏览器会在 `window` 上触发 `languagechange` 事件。为了防范指纹追踪，Safari 始终只报告一种语言，Chrome 的无痕模式也是如此，因此该列表可能并不包含用户的全部偏好语言，处理时应考虑到这一点。

### 在服务器端：`Accept-Language` 请求头

浏览器会在每个请求中附带同样的语言偏好。[RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language) 将该请求头定义为以逗号分隔的语言范围列表，每个语言范围可附带一个可选的质量值 (`q`) ，取值范围为 0 到 1。省略 `q` 时默认为 1，`q=0` 则表示“不可接受”。以下是该 RFC 自带的示例：

```http
Accept-Language: da, en-gb;q=0.8, en;q=0.7
```

这表示“我首选丹麦语，但也接受英式英语及其他英语变体。”`*` 通配符[表示](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language)任何未列出的语言。浏览器还会添加仅含语言代码的后备项：据 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) 介绍，如果用户的 `navigator.languages` 为 `["en-US", "zh-CN"]`，Chrome 和 Safari 会发送 `en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7`。请按 `q` 值对条目排序，因为 RFC 明确指出，列出的顺序“不可依赖”。

该请求头仅供参考。MDN 指出，服务器“绝不应覆盖用户明确做出的语言选择”，并提到该请求头往往不受用户控制，例如在旅行期间。如果你会缓存根据该请求头选择的页面，请添加 [`Vary: Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary)，以免缓存把上一位访问者的语言版本提供给下一位访问者。

### 为什么 IP 地理定位不是可靠的依据

地理位置并不等于语言。正如 W3C [所指出的](https://www.w3.org/International/questions/qa-link-lang)，“许多国家拥有不止一种官方语言”，而旅行者、外籍人士和 VPN 用户实际阅读的语言，往往也无法通过其 IP 地址推断出来。Google 的态度很明确：“不要根据 IP 分析来调整内容”，部分原因在于“Googlebot 抓取工具通常来自美国”，详见其[多地区网站指南](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites)。按 IP 选择语言的页面只会向抓取工具展示其中一个版本，其余版本则无法被看到。地理位置仍可用来选定合理的默认货币或配送地区，但这一决策应与语言选择分开处理。

## 将用户偏好与你支持的区域设置进行匹配

当用户请求 `pt-PT`，而站点只提供 `pt-BR` 时，就需要一条回退规则。常用的算法有两种，[`Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) 也沿用了相同的名称：

* **Lookup** (查找) 遵循 [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4)：依次尝试每个偏好标签，从末尾逐个去掉子标签 (先 `zh-Hant-CN`，再 `zh-Hant`，然后 `zh`) ，直到找到匹配项；若都不匹配，则返回默认值。
* **Best fit** (最佳匹配) 允许具体实现返回一个“至少与 lookup 结果同样合适、甚至可能更合适”的区域设置，例如同一语言的另一个地区变体。

两者的差异主要体现在地区变体上。使用 [`@formatjs/intl-localematcher`](https://www.npmjs.com/package/@formatjs/intl-localematcher) 0.9.0，针对 `['en', 'fr', 'de', 'pt-BR']` 进行匹配，默认值为 `en`：

| 请求            | Lookup | Best fit |
| ------------- | ------ | -------- |
| `pt-PT`       | `en`   | `pt-BR`  |
| `en-GB`       | `en`   | `en`     |
| `fr-CA`, `de` | `fr`   | `fr`     |
| `zh-TW`       | `en`   | `en`     |

Lookup 的结果可预测，也便于解释。Best fit 能让更多用户看到自己读得懂的语言；至于葡萄牙的葡萄牙语用户是更愿意看巴西葡萄牙语还是英语，这属于产品决策，因此建议结合你的区域设置列表，对两种算法都进行测试。

## 示例：在服务器端协商

这个 Node.js 服务器会依次检查 URL、已保存的 cookie 和请求头。它使用 [`negotiator`](https://www.npmjs.com/package/negotiator) 1.1.0 解析 `Accept-Language`，并按 q 值排序，再通过 `@formatjs/intl-localematcher` 以最佳匹配 (best fit) 算法对结果进行匹配。

```bash
npm install negotiator @formatjs/intl-localematcher
```

```js title="server.mjs"
import { createServer } from 'node:http';
import Negotiator from 'negotiator';
import { match } from '@formatjs/intl-localematcher';

const SUPPORTED = ['en', 'fr', 'de', 'pt-BR'];
const DEFAULT_LOCALE = 'en';

// 过滤掉 "*" 和格式错误的标签；match() 遇到它们会抛出 RangeError。
function isValidTag(tag) {
  try {
    Intl.getCanonicalLocales(tag);
    return true;
  } catch {
    return false;
  }
}

function pickLocale(req) {
  // 1. URL 路径中的区域设置，例如 /fr/pricing
  const { pathname } = new URL(req.url, 'http://localhost');
  const fromPath = pathname.split('/')[1];
  if (SUPPORTED.includes(fromPath)) return { locale: fromPath, source: 'url' };

  // 2. 用户上次访问时保存的选择
  const fromCookie = /(?:^|;\s*)locale=([^;]+)/.exec(req.headers.cookie ?? '')?.[1];
  if (SUPPORTED.includes(fromCookie)) return { locale: fromCookie, source: 'cookie' };

  // 3. Accept-Language：按 q 值排序后，与你支持的区域设置进行匹配
  const requested = new Negotiator(req).languages().filter(isValidTag);
  if (requested.length === 0) return { locale: DEFAULT_LOCALE, source: 'default' };
  return { locale: match(requested, SUPPORTED, DEFAULT_LOCALE), source: 'header' };
}

createServer((req, res) => {
  const { locale, source } = pickLocale(req);
  res.setHeader('Content-Language', locale);
  res.setHeader('Vary', 'Accept-Language, Cookie');
  res.end(`${locale} (from ${source})\n`);
}).listen(3000);
```

使用 `node server.mjs` 启动服务，然后用 `curl -H "Accept-Language: ..." localhost:3000/` 发送请求。在 Node.js 24 上的输出如下：

```text
da, en-gb;q=0.8, en;q=0.7                      -> en (from header)
pt-PT,pt;q=0.9,en;q=0.8                        -> pt-BR (from header)
fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5   -> fr (from header)
ja, de;q=0.5                                   -> de (from header)
en;q=0.5, de                                   -> de (from header)
*                                              -> en (from default)
(no header)                                    -> en (from default)
fr, with cookie locale=de                      -> de (from cookie)
/fr/pricing, with cookie locale=de             -> fr (from url)
```

`isValidTag` 过滤器必不可少：如果没有它，一旦请求头中包含 `*`，`match()` 就会抛出 `RangeError: Incorrect locale information provided`，进而导致请求崩溃。

## 示例：在浏览器中检测

在客户端渲染的页面中，你也可以对 `navigator.languages` 运用同样的思路。下面这个版本实现了一个简单的 RFC 4647 查找算法，不依赖任何第三方库：

```js title="detect-locale.js"
const SUPPORTED = ['en', 'fr', 'de', 'pt-BR'];
const DEFAULT_LOCALE = 'en';

// 查找：依次尝试每个首选标签，并从末尾逐个移除子标签。
function lookup(requested, supported, fallback) {
  const lower = supported.map((tag) => tag.toLowerCase());
  for (const tag of requested) {
    const parts = tag.split('-');
    while (parts.length) {
      const i = lower.indexOf(parts.join('-').toLowerCase());
      if (i !== -1) return supported[i];
      parts.pop();
    }
  }
  return fallback;
}

function preferredLocale(nav) {
  const requested = nav.languages?.length ? nav.languages : [nav.language];
  return lookup(requested, SUPPORTED, DEFAULT_LOCALE);
}

// 在浏览器中：preferredLocale(navigator)
```

以下是几组示例偏好列表的输出结果 (在 Node.js 24 中运行，并使用模拟的 `navigator` 对象) ：

```text
["fr-CA","en-US"] -> fr
["pt-BR","pt"]    -> pt-BR
["pt-PT","en"]    -> en
["ja-JP"]         -> en
["de-AT","de"]    -> de
```

调用 `preferredLocale` 之前，先检查用户是否已保存过选择；并将其返回结果仅作为起点：用该语言渲染页面，同时显示语言切换器。不要依据该结果进行重定向。

## 应以哪个信号为准？

自动检测只是首次访问时的推测。当各信号不一致时，请按以下优先级处理：

1. **URL。** 分享的 `/de/` 链接显示德语；在语言切换器中选择某种语言后，会跳转到该语言对应的 URL。
2. **已保存的选择。** 即 cookie 或账户设置。它决定中性的根路径将用户引导至何处，或应建议哪种语言，但绝不会改写已本地化的 URL。
3. **`Accept-Language` 请求头**，或客户端的 `navigator.languages`。仅在没有已保存的选择时使用，且只用于根路径或作为建议。
4. **默认区域设置。** 没有任何匹配时使用的后备选项。

W3C 建议以该请求头作为[起点](https://www.w3.org/International/questions/qa-accept-lang-locales)，允许用户更改语言，并将结果“存储在数据库或 cookie 中，供后续访问使用”。按此顺序处理时涉及的重定向、缓存和 SEO，请参阅[区域设置路由与 SEO](/kb/locale-routing-seo)；切换器本身的实现，请参阅[构建语言切换器](/kb/language-switcher)。

## gt-next、gt-react 和 gt-node 如何解析区域设置

在 General Translation 的 `gt-next` 中，[`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) 按以下顺序解析每个请求的区域设置：访问者刚刚选择的区域设置 (通过重置 cookie 跟踪) 、URL 中的区域设置 (例如 `/es/about`) 、已保存的区域设置 cookie、记录上一个客户端路由的 referrer 区域设置 cookie、`Accept-Language` 请求头，最后是你的 `defaultLocale`。解析结果默认保存在 `generaltranslation.locale` cookie 中，可通过 [`headersAndCookies`](/docs/react/nextjs/config#headers-and-cookies) 重命名。设置 [`ignoreBrowserLocales`](/docs/react/nextjs/config#ignore-browser-locales) 即可完全忽略该请求头。

如果请求的区域设置不受支持，这些库会先从浏览器偏好中选出最匹配的受支持区域设置，其次选择语言相同的区域设置 (例如为 `en-US` 选择 `en-GB`) ，最后回退到默认区域设置。可使用 [`useLocale`](/docs/react/reference/hooks/use-locale) 读取结果；在异步代码中，则使用 `gt-next/server` 中的 [`getLocale`](/docs/react/nextjs/reference/functions/get-locale)。

在 React 单页应用中，`gt-react` 的 [`initializeGTSPA`](/docs/react/reference/config#initialize-spa) 会在首次渲染前根据 cookie 和浏览器设置解析区域设置。在 Node.js 服务器上，`gt-node` 的 [`getRequestLocale`](/docs/node/reference/functions/get-request-locale) 会将 `Accept-Language` 与传给 [`initializeGT`](/docs/node/reference/functions/initialize-gt) 的区域设置列表进行协商。例如配置了 `en`、`fr`、`de` 和 `pt-BR` 时，它对 `fr-CH, fr;q=0.9, en;q=0.8` 返回 `fr`，对 `pt-PT,pt;q=0.9` 返回 `pt-BR`。请参照 [gt-node 区域设置指南](/docs/node/guides/detecting-locale)，在调用它之前先检查 cookie 或路径。

## 要点总结

* `navigator.languages` 和 `Accept-Language` 包含相同的有序语言偏好；Safari 和 Chrome 无痕模式只会暴露一种语言。
* 匹配之前，先解析 q 值 (取值 0 到 1，默认为 1) 并据此排序，同时剔除 `*` 和格式错误的标签；`@formatjs/intl-localematcher` 遇到 `*` 会抛出异常。
* 如果你只有 `pt-BR`，lookup 会将 `pt-PT` 映射到默认区域设置，而 best fit 会将其映射到 `pt-BR`。请根据需求慎重选择。
* 按以下优先级解析语言：URL、已保存的选择、`Accept-Language`，最后是默认值。IP 地理定位绝不应作为判断语言的依据。
* 除刚选择的区域设置外，`gt-next` 会依次检查 URL、`generaltranslation.locale` cookie、`Accept-Language`，最后是 `defaultLocale`。

## Related questions

- [语言重定向与 SEO：URL、cookie 还是 Accept-Language？](https://generaltranslation.com/zh/kb/locale-routing-seo.mdx): 凡是希望被搜索引擎收录的页面，都应由 URL 决定其语言，因为 Googlebot 不会发送 Accept-Language 请求头，而且大多从美国 IP 地址抓取。只有在决定 / 这类中性根路径重定向到哪里时，才应优先参考 cookie 中保存的用户选择，其次是 Accept-Language，并使用临时的 302 或 307 重定向。
- [语言切换器最佳实践：用户体验、SEO 与无障碍访问](https://generaltranslation.com/zh/kb/language-switcher.mdx): 用每种语言的本语名称来标注它，例如 Deutsch 或 日本語，不要使用国旗，因为国旗代表的是国家。每个选项都应通过可抓取的链接指向该语言版本的同一页面，用 cookie 记住用户的选择，并且切勿对已本地化的 URL 自动重定向。
- [什么是 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: [如何找出未翻译的硬编码字符串？](https://generaltranslation.com/zh/kb/find-hardcoded-strings.mdx)
- Next question: [如何翻译 i18n JSON 文件？](https://generaltranslation.com/zh/kb/translate-json-files.mdx)

## Sitemap

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