国际化路由 · 同一个页面,哪个语言版本
多语言站点的路由要回答一个新问题:同一个 /about,该给英文、中文还是日文版?这有两件事要分清。其一,locale 写在哪——大多写进 URL 让它可分享、对 SEO 友好:路径前缀(/en/about)、或域名(en.site.com /
site.fr)。其二,当 URL 没带 locale 时(用户直接访问 /about),靠什么猜一个——这就要读浏览器的 Accept-Language 头去协商。
第一个 lab 看策略怎么决定 locale 从哪来、URL 不带时怎么 302 跳到带前缀的规范地址;第二个 lab 拆开 Accept-Language 的协商算法——它不是一个值而是一串带权重(q)的偏好,还要走 fallback 链(要
zh-TW 没有就退 zh,再退默认)。本站支持 en、zh、zh-TW、fr、ja,默认 en。
1 · 策略:locale 写在 URL 的哪里,不带时怎么办
URL 自带的 locale 优先,且不应再被「猜」覆盖。 如果用户访问 /zh/about,就直接返回中文——哪怕他的 Accept-Language 是英文。Accept-Language 只在 URL 没有携带 locale 时(访问裸 /about)才参与,且结果通常是一次 302
跳到带前缀的规范地址(/about → /zh/about),让此后每个语言版本都有自己唯一、可分享、可被搜索引擎收录的 URL。这也呼应 URL 规范化:一个内容一个规范地址。
2 · Accept-Language 协商:一串带权重的偏好 + fallback 链
Accept-Language 不是单个值,而是浏览器按用户语言设置排好的偏好列表,每项带一个 q 权重(0–1,默认 1)。协商 = 把它按 q 降序排好,逐个去和「本站支持的 locale」匹配:先求精确命中(zh-TW ==
zh-TW),不中就剥掉地区退一步(zh-TW → zh)再试——这就是 fallback 链。第一个匹配上的就是结果;全都不中,落到默认 en。
协商算法本身只有十来行:解析 q、降序、精确命中或剥地区 fallback、全不中回默认。
const SUPPORTED = ['en', 'zh', 'zh-TW', 'fr', 'ja'];
const DEFAULT = 'en';
// "zh-TW,zh;q=0.9,en;q=0.5" → 按 q 降序的偏好列表
function parse(header) {
return header.split(',').map(part => {
const [tag, q] = part.split(';q=');
return { tag: tag.trim(), q: q ? +q : 1 }; // q 缺省为 1
}).sort((a, b) => b.q - a.q); // 权重高的优先
}
function negotiate(header) {
for (const { tag } of parse(header)) {
if (SUPPORTED.includes(tag)) return tag; // 精确命中
const base = tag.split('-')[0]; // zh-TW → zh
if (base !== tag && SUPPORTED.includes(base)) return base; // 剥地区 fallback
}
return DEFAULT; // 全不中 → 默认
}
剥地区 fallback 是分寸活。 zh-TW 退到 zh 多半没问题(都是中文),但 zh 默认指简体,给繁体用户可能并不理想——所以认真做的站点会同时支持 zh 与 zh-TW 并精确区分。更别把不同书写系统混为一谈(简繁、pt-BR
与 pt-PT)。协商只是挑默认:务必再给用户一个显式语言切换器,并把选择记下来(cookie / 账户设置),别让人每次都和 Accept-Language 较劲。
3 · 相关页
- 本系列 · HTTP 重定向——裸
/about→/zh/about用的就是 302;为什么是 302 而非 301(locale 可能随用户而变,不宜永久缓存),看那页的语义表。 - 本系列 · URL 规范化——「一个内容一个规范地址」在多语言下变成「每个语言版本一个规范地址」,并用
hreflang互相声明——同一套规范化思路。 - Intl · 浏览器内建的本地化——路由挑出 locale 之后,日期 / 数字 / 排序怎么按该 locale 格式化,是
Intl那个系列的事。