不能直接用navigator.language当文件名,因其返回BCP 47标签如"zh-CN"或小写"zh-cn",而语言包通常为zh.json;须先toLowerCase()、split("-")[0]提取主语言码、排除无效值,再校验是否在支持列表中,否则fallback。

不能直接用 navigator.language 的返回值去请求语言包,必须先归一化、校验、fallback,否则 404 或乱码是常态。
为什么 navigator.language 不能直接当文件名用
浏览器返回的是 BCP 47 标签,比如 "zh-CN"、"en-GB"、"ja",甚至小写 "zh-cn"。而你的资源文件大概率叫 zh.json、en.json、ja.json——地区后缀不匹配就会 404;大小写不一致在某些 CDN 或 case-sensitive 文件系统上也会失败。
常见错误现象:fetch("lang/en-GB.json") 返回 404,页面所有文案变成 key(如 "header.title");或加载了 zh-TW.json 却没内容,回退成空对象导致文案消失。
- 必须统一转小写:
navigator.language.toLowerCase() - 必须提取主语言码:
.split("-")[0],得到"zh"、"en"、"ja" - 必须排除无效值:
if (!lang || lang === "und" || lang.length > 3) return "en"
如何静默加载语言包而不阻塞首屏
“静默”不是不报错,而是不中断渲染、不抛未捕获异常、失败有兜底。关键在三点:不 await、不 throw、不依赖 DOM 就绪。
- 用
fetch(langUrl).then(r => r.json()).catch(() => ({})),失败返回空对象,避免后续t("key")报错 - 加载过程完全异步,不要放在
DOMContentLoaded或React.useEffect(() => {}, [])里等它;改用状态标记(如isI18nReady),等 DOM 渲染完再批量更新文案 - 加缓存策略:
fetch(langUrl, { cache: "force-cache" }),或预埋 Service Worker 缓存规则,避免重复请求 - 首次访问可内联默认语言对象(如
const en = { welcome: "Welcome" }),防止白屏
如何让 document.querySelectorAll("[data-i18n]") 正确解析嵌套 key 和参数
只做 el.textContent = dict[key] 是远远不够的。真实场景中,data-i18n 值常为 "form.login.button",还要支持插值如 { name: "Alice" }。
- 按点号拆解 key:
key.split(".").reduce((o, k) => o?.[k], dict),避免dict["form.login.button"]这种直查失败 - 允许传参:
data-i18n-params='{"name": "Alice"}',JSON.parse 后传给格式化函数(如format(str, params)) - 注意空值安全:如果某层为
undefined,应 fallback 到 key 本身或空字符串,而不是抛Cannot read property 'login' of undefined - 不要在循环中反复调用
querySelectorAll,建议一次取 DOM 节点列表,再统一遍历
最容易被忽略的兼容性与降级链
很多人写了 navigator.language 就以为万事大吉,但 IE 不支持 navigator.languages,Safari 旧版可能返回空,SSR 环境下 navigator 根本不存在——降级链必须显式、可测、可打断。
- 优先级顺序建议:
[url?lang=xx, localStorage.getItem("lang"), navigator.languages?.[0] || navigator.language, "en"] - 每一步都要校验是否在
SUPPORTED_LANGS = ["zh", "en", "ja", "es"]中,不在就跳过 - localStorage 存的值也要归一化(小写 + split),不能原样存
"zh-CN"后又拿它去查zh.json - 服务端渲染时,
navigator未定义,必须靠Accept-Language请求头 + 客户端二次校准,不能省略这步

















