📘 第六章 HTTP首部
📄 一句话概括

Accept-Language 按 q 值排出语言偏好。 匹配方式是前缀匹配:zh-CN 能匹配到 zh,简单有效,但分不清简繁。

📝 本节和前面几节的关系

协商三兄弟的最后一个。前两个管格式和编码,这一个管语言。

一、语言标签的层级

语言标签是分层的:zh 是中文,zh-CN 是中国大陆的中文,zh-Hant 是繁体,zh-Hans 是简体。 各段用连字符分隔,从大到小排列——大类在前,细化在后。 所以「前缀匹配」的意思是:客户端要 zh-CN,服务端只有 zh,也算命中;反过来不成立。

二、前缀匹配的两个坑

坑一:分不清简繁。 zh-Hant(繁体)和 zh-Hans(简体)前缀都是 zh,如果服务端只有简体, 繁体用户会被匹配到简体页面。技术上「能用」,体验上不礼貌。 坑二:分不清地区。pt-BR(巴西葡萄牙语)和 pt-PT(葡萄牙)差异比很多人想的大; en-GB 和 en-US 在日期格式、拼写上也不一样。 要精细处理,得用 RFC 4647 的另一种方式(lookup 之外的 filtering),按语言子标签逐级比对, 而不是简单砍掉后半段。但大多数网站的做法就是砍后缀,因为够用。

三、别用 IP 猜语言

常见的「聪明」做法是按 IP 归属地猜语言。 这在国际化产品里经常出错:一个在日本出差的中国人,会被强行切到日语页面。 正确顺序是:先看 Accept-Language,没有才退到 IP 或默认值。 而且切换之后要允许用户手动改,并且记住这个选择。 Demo 里「只要法语」的请求,服务端没有法语,退到了默认中文——这在用户体验上比回 406 好, 但如果用户明明设了法语,那就该在页面上给一个明显的语言切换入口。

四、动手 Demo:五组偏好,看服务端挑哪个语言版本

服务端有三个语言版本(zh、en、ja)。 把 Accept-Language 按 q 排序,逐个做前缀匹配,第一个命中的就是答案,都没命中就用默认的 zh。

tags = []
for part in al.split(u','):
    if u';' in part:
        tag, _, rest = part.partition(u';')
        q = float(rest.split(u'q=')[1])
    else:
        tag, q = part, 1.0
    tags.append((tag.strip().lower(), q))
tags.sort(key=lambda x: -x[1])
pick = None
for tag, q in tags:
    base = tag.split(u'-')[0]        # 砍掉地区部分,做前缀匹配
    if base in SITE:
        pick = base
        break
if pick is None:
    pick = u'zh'                     # 都没命中就用默认

实跑输出:

--------------------------------------------------------------------
Accept-Language:按 q 值挑一个语言版本
--------------------------------------------------------------------
  【中文优先】
    Accept-Language          zh-CN,zh;q=0.9,en;q=0.8
    按 q 排完                zh-cn(1) | zh(0.9) | en(0.8)
    选了                     zh
    正文                     简体中文页面

  【英文优先】
    Accept-Language          en-US,en;q=0.9,zh;q=0.5
    按 q 排完                en-us(1) | en(0.9) | zh(0.5)
    选了                     en
    正文                     English page

  【只要日语】
    Accept-Language          ja
    按 q 排完                ja(1)
    选了                     ja
    正文                     日本語のページ

  【点名只要法语】
    Accept-Language          fr-FR,fr;q=0.9
    按 q 排完                fr-fr(1) | fr(0.9)
    选了                     zh
    正文                     简体中文页面

  【什么都不发】
    Accept-Language          (没发)
    按 q 排完                (空)
    选了                     zh
    正文                     简体中文页面

  匹配规则是「前缀匹配」:客户端说 `zh-CN`,服务端有 `zh` 就算命中。
  这是 RFC 4647 的 lookup 方式,简单但会出错——
  `zh-Hant`(繁体)和 `zh-Hans`(简体)都前缀匹配到 `zh`,
  如果服务端只有简体,繁体用户就会拿到简体页面。
  最后那个「只要法语」的请求,服务端没有法语,退到了默认的中文——
  严格说该回 406,但和 Accept 一样,现实里都选择给个能看的。

zh-CN,zh;q=0.9,en;q=0.8 排出三段,第一段 zh-cn 砍成 zh 命中,选中文。 en-US,en;q=0.9,zh;q=0.5 反过来,选英文。同样一句话的两个语言版本。 只要 ja 就选日语,正文是「日本語のページ」。 只要 fr-FR,fr;q=0.9——法语不在三个版本里,退到默认中文。 注意它没有因为 q=0.9 就退而求其次选 en,而是直接用了默认值。 什么都不发,同样是默认中文。所以「没发」和「发了但没有」结果一样,这就要求默认值选得合理。

30 秒实验

  1. 把默认值从 zh 改成 en,重跑,看最后两组变化。
  2. 加一个 zh-Hant 的候选,看前缀匹配会不会把繁体用户送到简体页面。
  3. 想一想:如果你的产品有简繁两版,前缀匹配该怎么改?

常见误区

  1. 「Accept-Language 是精确匹配」——是前缀匹配,zh-CN 能命中 zh。
  2. 「没发这个头就用英文」——用什么当默认值是你的选择,没有规定。
  3. 「按 IP 猜语言更准」——出差、VPN、跨境都会猜错,只该当兜底。
自测三问(点开对答案) 1. 客户端发 `zh-CN`,服务端只有 `zh`,算命中吗? —— 算。前缀匹配,从大类往细化匹配。 2. 前缀匹配最大的问题是什么? —— 分不清简繁和地区差异,`zh-Hant` 和 `zh-Hans` 都会撞到 `zh`。 3. 语言协商失败时该回 406 吗? —— 通常不回。给一个默认语言版本更实用,但要在页面上给切换入口。

协商三兄弟讲完了。下一个字段开始涉及身份:Authorization。

相关笔记