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 秒实验
- 把默认值从 zh 改成 en,重跑,看最后两组变化。
- 加一个
zh-Hant的候选,看前缀匹配会不会把繁体用户送到简体页面。 - 想一想:如果你的产品有简繁两版,前缀匹配该怎么改?
常见误区
- 「Accept-Language 是精确匹配」——是前缀匹配,
zh-CN能命中zh。 - 「没发这个头就用英文」——用什么当默认值是你的选择,没有规定。
- 「按 IP 猜语言更准」——出差、VPN、跨境都会猜错,只该当兜底。
自测三问(点开对答案)
1. 客户端发 `zh-CN`,服务端只有 `zh`,算命中吗? —— 算。前缀匹配,从大类往细化匹配。 2. 前缀匹配最大的问题是什么? —— 分不清简繁和地区差异,`zh-Hant` 和 `zh-Hans` 都会撞到 `zh`。 3. 语言协商失败时该回 406 吗? —— 通常不回。给一个默认语言版本更实用,但要在页面上给切换入口。协商三兄弟讲完了。下一个字段开始涉及身份:Authorization。
- ☐ (本节完,下一节 6.4.5 Authorization)