不能直接用 navigator.language 原始值加载语言包,因其返回 bcp 47 标签(如“zh-cn”),而资源文件通常仅以主语言码命名(如“zh.json”);需先标准化提取主语言码(split("-")[0].tolowercase()),再校验是否在支持列表中,失败则降级至 localstorage 或默认语言,否则必触发 404 或显示键名。

不能直接用 navigator.language 的原始值去加载语言包,否则大概率 404 或显示 key 名(如 "welcome")——必须先做标准化提取 + 降级校验,再发起资源请求。
为什么 navigator.language 不能直接当文件名用
浏览器返回的值是完整 BCP 47 标签,比如 "zh-CN"、"en-GB"、"pt-BR",而你实际部署的语言资源包通常只叫 zh.json、en.json、pt.json。直接拼接 fetch(`/i18n/${navigator.language}.json`),90% 情况下会触发 404。
更隐蔽的问题是:用户可能支持 "fr-FR" 和 "fr-CA",但你只提供了 fr.json;或者用户系统设为 "ja"(无地区),而你资源命名是 ja-JP.json —— 颗粒度错位,必然加载失败。
- 常见错误现象:
Failed to load resource: the server responded with a status of 404 (),页面文本全为未翻译的键名 - 真实影响:首屏渲染后语言缺失,用户看到的是
"login_button"而非按钮文字,且无 fallback 提示 - 根本原因:把“用户偏好语言标签”和“资源文件标识”当成同一概念,跳过了标准化环节
navigator.languages[0] 比 navigator.language 更可靠
navigator.languages 返回的是用户在浏览器设置中配置的**完整偏好列表**(按优先级排序),例如 ["zh-CN", "zh", "en-US", "en"];而 navigator.language 只取第一个,且在旧版 IE 中不可靠(需回退到 navigator.userLanguage)。
优先用 navigator.languages[0] 是更严谨的做法,但要注意兼容性:
- 现代浏览器(Chrome/Firefox/Safari/Edge ≥ 2020)均支持
navigator.languages - IE 完全不支持,必须降级:
const lang = navigator.languages?.[0] || navigator.language || navigator.userLanguage - 即使拿到
navigator.languages[0],也不能直接用——它仍是"zh-CN"这类带地区码的字符串,仍需.split("-")[0]提取主语言码
如何安全提取主语言码并匹配支持列表
提取主语言码只是第一步,关键是要在校验环节堵住所有缺口。不能只查 "zh" 在不在 SUPPORTED_LANGS 里,还要考虑大小写、格式差异和兜底路径。
- 提取方式统一用
lang.split("-")[0].toLowerCase(),避免"ZH"或"Zh"导致匹配失败 -
SUPPORTED_LANGS必须是小写数组,例如["zh", "en", "ja", "es", "pt"],不包含地区后缀 - 匹配失败时,按顺序 fallback:
localStorage.getItem("preferredLang")→"en"(默认) - 不建议用
Intl.DateTimeFormat().resolvedOptions().locale替代,它依赖操作系统区域设置,和浏览器语言偏好不一致(比如 macOS 用户设了英文系统但浏览器语言是日语)
简短示例:
const getLangCode = () => {
const lang = navigator.languages?.[0] || navigator.language || navigator.userLanguage;
const base = lang?.split("-")[0].toLowerCase();
const SUPPORTED_LANGS = ["zh", "en", "ja", "es"];
return SUPPORTED_LANGS.includes(base)
? base
: localStorage.getItem("preferredLang") || "en";
};
静默加载资源包时最容易被忽略的细节
所谓“静默”,是指不阻塞首屏、不弹提示、不闪动 UI —— 但这需要控制加载时机和错误处理边界。很多人只写了 fetch,却没处理网络失败或 JSON 解析异常,结果资源加载卡住,后续 t("xxx") 全部返回 undefined。
- 加载必须在
DOMContentLoaded后立即触发,但不要放在document.write或同步脚本中 - fetch 失败时,必须有降级逻辑:用默认语言对象兜底,而不是让整个 i18n 系统瘫痪
- 如果采用预加载策略(如把所有语言包内联进 HTML),就不存在网络请求问题,但包体积增大;若走动态 import,则需确保模块路径与提取出的
langCode严格对应(如import(`./locales/${langCode}.js`)) - 最常被跳过的一步:加载成功后,要把当前语言写入
localStorage,否则用户刷新页面又得重新探测——这不算“静默”,而是重复劳动










