全部笔记

浏览器语言协商:让偏好、显式选择与缓存各守边界

为三语言网站建立可预测的首次访问规则,而不是每次强制跳转。

用户打开 /en/,网站因为浏览器偏好中文又把他送回 /zh-cn/。语言协商本来是帮助首次访问,结果覆盖了用户刚刚做出的选择。正确规则应先区分“没有选择”与“已经选择”。

本文假设支持简体中文、繁体中文与英文,内容各有明确语言路径。根路径负责首次协商,显式语言路径直接展示对应页面;不支持的浏览器语言回退英文。

先明确“默认语言”的两种产品解释

浏览器可能报告法语优先、英文其次。产品规则若是“最高优先语言不受支持就英文”,结果是英文;若是“寻找列表中优先级最高的受支持语言”,也可能是英文,但在法语之后是繁体时,两种规则就得到不同结果。必须选择并写明规则,不能让服务端取首项、客户端遍历列表而形成不同体验。技术解析器只能解析偏好,不替产品做这个选择。

对于只支持三种语言的个人网站,可以采用首选语言映射后回退英文的简单规则;也可以按质量值遍历受支持项。质量值为零表示不可接受,不能被当作普通低优先级偏好。通配值、空头和格式错误都要有明确默认,非法输入不应导致服务器异常。使用成熟库时仍需测试产品映射,因为库不知道简体和繁体如何对应项目路径。

中文标签还包含语言、文字体系和地区。优先识别明确的 Hans 或 Hant,再按记录好的地区映射,避免 zh-Hant-CN 被单纯地区判断错误覆盖。地区并不保证文字偏好,保存的显式选择能修正这种推断。映射是实用缺省,而不是对用户身份的判断,因此任何语言页面都应提供切换入口。

明确路径优先于保存的偏好,保存的偏好优先于浏览器协商,最后才使用默认语言。这个顺序让复制链接、搜索结果和用户切换具有稳定语义。不要在每个页面运行自动跳转脚本,否则语言切换器会与脚本相互拉扯。

Accept-Language 带权重,客户端 navigator.languages 则是排序列表。服务端解析应使用成熟解析器或严格实现质量值规则,不要仅取头字符串的前两个字母。

function mapLanguage(tag) {
  const normalized = tag.toLowerCase();
  if (normalized === 'zh-hant' || normalized.startsWith('zh-hant-')) return 'zh-tw';
  if (normalized === 'zh-tw' || normalized === 'zh-hk') return 'zh-tw';
  if (normalized === 'zh' || normalized === 'zh-cn' || normalized === 'zh-sg') return 'zh-cn';
  if (normalized === 'zh-hans' || normalized.startsWith('zh-hans-')) return 'zh-cn';
  if (normalized === 'en' || normalized.startsWith('en-')) return 'en';
  return null;
}

这是项目可采用的一种产品映射,不是所有地区用户语言偏好的客观定论。港澳用户也可能偏好英文或简体,显式切换必须始终可用。对没有地区或文字体系的 zh,团队应选择并记录默认策略。

将协商结果限制在没有明确路径的入口

根路径是未选择状态,带语言前缀的路径是明确选择状态。请求 /en/writing/cache/ 时,即使 Cookie 保存简体也直接返回英文。切换语言可以更新偏好,但不改变已经分享出去的显式链接。这样同一个地址对不同访问者仍然代表相同正文,缓存与搜索索引都更容易维护。

Cookie 偏好要校验为有限枚举,未知值忽略,不参与动态路径拼接。保存期限和清理方式应适合站点,不要求用户登录只为记住语言。若站点不需要跨访问保存,完全不存 Cookie 也是合理选择。用户在设备上换了浏览器语言后是否覆盖旧偏好,需要产品规则,不能让两种来源随机竞争。

静态客户端协商时,默认根页应有真实内容或语言链接,无脚本访问仍可进入。自动跳转前避免先显示大量错误语言正文再闪走,可以保持简洁入口,但不把整站变成只有脚本才能访问的空壳。服务端协商则要选择适合偏好的临时响应,不把某个用户的偏好永久编码成所有人共用的跳转。

浏览器首选繁体,Cookie 保存英文,用户打开简体隐私政策。第一判断是路径已经明确,因此显示简体;不用继续协商,也不因 Cookie 改写地址。用户点击繁体后进入繁体同文页面,可以将偏好更新为繁体。下一次打开根路径时才读取该偏好。这个流程把当前任务与未来缺省分开,不让一次入口判断控制整站。

若 Cookie 值失效,根路径忽略它再查看浏览器偏好;若浏览器首选语言不受支持,按已确认规则进入英文。切换按钮不依赖 Cookie 写入成功,隐私模式下仍能通过明确路径访问。保存失败可以不打断阅读,因为偏好保存是便利功能,显式 URL 才是当前内容的确定身份。

分享链接时也使用当前文章的明确语言路径,不分享一个会因接收者偏好改变目标的根入口。用户想分享中文版论证,应让接收者看到中文版,并自行选择是否切换。根入口适合介绍整个网站,不适合替代某篇文章的永久地址。语言路径稳定还能减少搜索结果与社交卡片之间的内容差异。

日志不需要完整保存用户语言列表。有限协商结果、回退原因和规则版本通常足以排查,不把偏好推导成人口统计画像。语言头可能被自动客户端伪造,也未必代表真实所在地。统计语言选择时要区分显式切换与自动缺省,不能声称某种语言请求量直接等于某地区用户数量。

规则升级应保证旧语言路径继续可达,例如内部标识调整不能直接删除此前分享地址。若简体路径从旧形式改为标准形式,提供明确迁移并更新站内链接。迁移响应不重新依据浏览器语言选择目标,否则旧中文版链接可能在迁移后变成英文入口。路径迁移与偏好协商各自完成自己的任务,才不会形成难以解释的跳转链。

静态站点与边缘协商各有成本

静态根页用客户端脚本判断语言,部署简单,但需要等待 JavaScript,并且无脚本用户应看到默认页面和语言入口。边缘重定向可以在首次响应前选择,但要处理缓存与请求头,不能把某个中文用户的跳转缓存给所有人。

对于固定语言路径,正文无需再按浏览器头改变,缓存键也更稳定。根页重定向可以避免共享缓存,或使用平台明确支持的有限语言变体。不要把整个原始语言头作为缓存键,输入组合会迅速扩大。

原始 Accept-Language 可以包含很长列表和不同权重,直接作为键会形成大量等价变体。先按照产品规则得到 en、zh-cn 或 zh-tw,再使用平台支持的隔离方式,或让协商重定向不参与共享缓存。Vary 头具有协议含义,但具体 CDN 支持需要核对,不能只添加头就假设所有平台按语言隔离。

如果保存偏好 Cookie 参与协商,入口缓存还要考虑它,而不是仅看语言头。对于低成本根路径,绕过公共缓存通常比设计多个复杂维度更容易解释。最终语言路径可以长期按 URL 缓存,因为它们不再依据请求头变化。将协商限定到一条入口,能够把不稳定偏好从主体内容缓存中移除。

测试从英文请求开始再发中文请求,并反向执行,观察入口是否错误复用第一位用户的 Location。每种情况下再访问显式路径,确认无二次强制协商。单独用干净缓存请求每种语言,无法发现跨用户缓存污染。顺序测试与无缓存语义测试要同时存在。

切换语言应尽量保留任务

用户在隐私政策页切换语言,目标应是另一语言的隐私政策,而不是首页。维护一个语言路径映射,比简单替换字符串更安全,因为不同语言 slug 可能不一致。如果目标译文不存在,应显示明确回退,不要制造 404。

页面 lang 属性、hreflang 和 canonical 要匹配实际内容。语言按钮不能只靠文字颜色表达当前状态,应有可访问标识。简、繁、EN 的按钮使用相同高度和对齐规则,避免不同字体基线产生视觉错位。

同一篇文章的中文 slug 与英文 slug 可能不同,直接替换路径前缀会得到不存在地址。内部记录可保存稳定内容标识和语言版本地址,切换器按标识寻找目标。没有翻译时,可以显示不可用状态或转到该语言索引,并告知当前文章没有对应版本;不要生成看起来成功却回到不相关页面的链接。

系统还要区分整个页面语言和引用内容语言。英文文章中引用中文代码注释,不需要把整页 lang 改成中文;必要片段可使用局部语言标记。日期、数字格式与时区也独立配置,英文界面可以显示香港时区。若把语言当作所在地,远程工作者和多语言用户会看到错误时间或货币。

可访问切换器用真实链接保持无脚本可用,标明当前语言和每个目标名称。简、繁、EN 视觉紧凑,但辅助名称可以使用完整语言名。不同字体基线不一致时使用固定行高、统一容器高度和对齐方式,不靠单个标签的负边距掩盖问题。键盘焦点样式与当前语言样式应能区分,颜色不是唯一提示。

把协商决定写成可审查的输入输出记录

同一组输入应产生同一个决定,记录可以包含入口是否明确、有效保存偏好、浏览器候选和最终有限语言标识。优先规则逐层退出:明确路径不继续读取偏好;根路径有有效偏好不重新猜测;没有前两者才协商;协商无法得到结果才使用英文。这个记录适合测试与诊断,不必把完整浏览器语言列表写进长期日志。

质量值解析与产品映射分开。解析器识别标签和权重,产品规则决定是选择最高候选后回退,还是寻找最高受支持候选。相同权重时如何保持顺序也应记录,避免不同实现排序不稳定。非法权重与极长列表的处理需要有资源上限和明确回退,不让一个无关偏好头造成服务端异常。可测试样本使用合成输入,不需要收集真实用户的全部列表。

支持集合更新时,旧保存偏好可能过时。先校验枚举,无法识别的值按缺失处理,并提供手动切换。不要把 Cookie 直接插入 URL,哪怕当初由本站写入,客户端仍可修改它。输入是否来自浏览器、是否曾经由系统生成,都不等于现在可信。最终路径只能来自批准的语言映射表。

同一资源的翻译关系不靠路径猜测

某篇文章英文地址为 /en/writing/cache-key/,简体地址可能是另一稳定 slug,繁体尚未完成。翻译关系应来自内容 ID 到语言地址的映射,切换器先查询对应版本。不存在繁体时,明确显示缺少翻译或给出繁体索引入口,不能生成一个预期不存在的路径再让缺失页猜测语言。映射只包含已公开记录,私人翻译草稿不因源文公开而自动可达。

翻译版本也可能不同步。原文修正了配置错误,译文仍然描述旧行为,需要编辑流程记录对应源版本和待同步状态。语言切换可以保留资源身份,但不能承诺所有版本事实完全一致。公开说明与更新时间应真实,不通过复制日期制造同步假象。技术文章的代码和引用尤其需要复核,文字转换不会自动更新依赖版本。

简体与繁体也不能机械地只替换字形。产品术语、地区习惯与链接目标可能不同,文字转换工具可以辅助,最终仍需编辑判断。页面语言标签描述实际文本,内容来源与地区服务范围另行表达。将语言、地区和时区分别建模,才不会因为繁体界面就自动改变账户币种或发布时间。

根入口如何在爬虫与无脚本环境中工作

客户端协商根页应提供可访问的语言入口,即使脚本失败也能继续阅读。不要通过空白页面加自动跳转,让不执行脚本的工具只能看到无内容。服务端协商则要检查无 Accept-Language 输入的稳定结果,并保证每个语言版本通过普通链接可达。搜索爬虫的请求偏好不同于真实用户,语言协商不是发现全部内容的机制。

根页是否作为独立内容身份,需要与 canonical 和语言替代策略一致。如果它只是选择入口,不把它当成三种语言正文的共同 canonical。每个稳定语言页面声明自己的内容身份,替代链接仅指向真实存在的版本。缺失语言不生成占位标签,避免搜索系统与读者都被引向不存在的资源。

对于静态脚本协商,自动导航应避免增加不必要历史记录,使用户按返回键时再次经过根页又被跳走。具体导航方式依浏览器 API 和产品需求选择,并在返回、刷新与直接访问下验证。偏好协商不应成为用户离开页面的障碍;明确路径仍然优先,所以点击英文链接不会再次被中文脚本夺走。

用顺序实验发现入口缓存污染

测试环境准备两个请求,甲偏好简体,乙偏好英文,先后访问相同根路径。预期 Location 分别正确,再反转顺序验证。如果第二个结果总是跟随第一个,就需要检查共享缓存维度,而不是修改语言映射。再给甲保存繁体 Cookie,保持浏览器简体,确认根入口采用保存偏好而明确简体路径仍显示简体。

这组测试要区分客户端保存的跳转与边缘对象。某些永久重定向可能被浏览器记住,因此偏好协商一般不应随意使用永久状态。使用新的受控客户端与检查首跳响应能够帮助定位。报告实际头与平台缓存配置,单独写一个 Vary 不证明该账户的 CDN 确实按它隔离。

若选择绕过入口共享缓存,验收观察它是否仍被其他层缓存。应用返回 no-store 的意图,需要与平台规则实际行为核对;脚本式入口则可能有固定 HTML,语言决定在客户端发生,其静态页面缓存与动态重定向缓存是不同机制。按实际架构测试,不把两者指标混在一起解释。

偏好保存只影响未来默认任务

用户切到英文并成功导航,即使 Cookie 写入被阻止,当前任务也已经完成。不要因此提示整个语言切换失败,也不要自动跳回原语言。保存功能可以无声保持有限便利,或在设置中表达无法长期记住,具体取决于产品。清理偏好后,下次根入口重新协商,现有明确路径保持不变。

在共享设备上,保存偏好可能影响下一位访问者,但不构成身份识别。避免为了记住语言创建不必要的账户与追踪标识。对于只需一次浏览的站点,不保存偏好也合理。每项状态都有明确用途与生命周期,语言入口才能始终围绕降低选择成本,而不是逐渐承担画像、定位或权限判断。

验收一个有限状态表

测试英文、简体、繁体、不支持语言和空语言头;分别测试首次访问、保存偏好与显式路径。重点断言不发生循环重定向,显式英文路径不会被中文偏好覆盖,根页缺少 JavaScript 时仍能进入内容。

时间安排和内容发布时间也不要与语言混为一谈。用户选择英文不意味着所在时区是 UTC,页面语言只描述文本偏好。让语言协商解决一个清晰问题,才不会把地区、货币、时间与权限误装进同一个判断。

再考虑带扩展的语言标签,例如 en-US-u-hc-h23。英文内容路由仍是有限的 en,小时显示偏好却可能由格式化层另行处理。不能为每个扩展值生成新的语言目录,也不能因为格式化 API 接受这个标签,就推导网站拥有对应译文。语言标签解析、公开语言映射与数字日期格式化是三个不同消费者,应分别拥有允许规则。

准备普通英文标签和带小时扩展的标签,断言它们选择同一英文文章;若产品支持该格式偏好,再单独检查时间呈现。缺少所需格式化能力时,回退规则不应改变正文地址。这个样本能发现“直接拿完整 locale 当路径”的实现错误,也能防止把运行时支持的区域数量误当成网站支持的语言数量。

参考:Accept-Language、国际化语言标签。