最可控方案是用data-i18n标记+JSON语言包+localStorage记忆,需显式为每个可翻译节点添加data-i18n属性并同步更新document.documentElement.lang及子元素lang属性。

直接用 data-i18n 标记 + 分离的 JSON 语言包 + localStorage 记忆选择,是最可控、不依赖构建工具、且能绕过 SSR/CSR 同构陷阱的方案。硬编码文案、只改 document.documentElement.lang、或靠 CSS [data-lang] 显隐切换,都会在可访问性、SEO、动态内容或 RTL 场景下翻车。
怎么给 HTML 元素打翻译标记才真正生效
所有要换语言的文本节点,必须显式加 data-i18n 属性,值是语言包里的键名(如 "nav.home")。JS 不会猜,也不能靠 class 或 id 推断。
-
data-i18n默认只更新元素的textContent;若要替换placeholder、title、alt,得额外加data-i18n-placeholder、data-i18n-title等带后缀的属性 - 表单控件(
<input>、<select>)、<button>、SVG 内<text>都得单独标,比如<input data-i18n-placeholder="search_hint"> - 别在
<script>、<pre>或注释里加data-i18n——这些节点不渲染为可见文本,加了无意义 - 含 HTML 结构的文案(如「点击服务条款即表示同意」)要用
innerHTML替换,但语言包里对应值必须是可信的纯 HTML 片段,否则有 XSS 风险
为什么必须同步更新 document.documentElement.lang 和子元素 lang
只设根节点 lang,对已渲染的子元素完全无效。浏览器和屏幕阅读器按每个节点自己的 lang 属性决定语音引擎、字体 fallback、标点间距、断行规则——不是继承来的。
- 页面加载时,应根据当前选中的语言码(如
zh-CN)设置document.documentElement.lang = 'zh-CN' - 局部混排内容(如英文术语、日文引用、代码块)必须显式加内联
lang,例如<code lang="en">useState</code>或<blockquote lang="fr">... - 切换语言后,除了更新文案,还得遍历所有带
lang的元素,把非根节点的lang值也同步更新(比如博客里一段lang="ja"的引文,不能跟着主语言切成zh-CN) -
lang值必须符合 BCP 47:用zh-CN,别用zh_CN或chinese;iOS VoiceOver 对ja支持差,得写ja-JP
JSON 语言包怎么组织才不踩坑
结构扁平、键名一致、路径固定,是避免加载失败和 key 错位的核心。别图省事塞进一个大对象里。
立即学习“前端免费学习笔记(深入)”;
- 每个语言一个文件:
locales/zh-CN.json、locales/en-US.json、locales/pt-BR.json,内容全是顶层键值对,不嵌套 - 所有文件键名必须严格一致;哪怕某语言暂时没翻译,也要留空字符串或占位符,否则 JS 查不到 key 就留白
- 加载失败时必须有 fallback:fetch 失败、HTTP 404、或 key 不存在,一律退到默认语言或空字符串,不能让 DOM 文本变 undefined
- 如果用 Web Components 或动态插入内容(如模态框、列表项),新节点插入 DOM 后必须再调一次翻译函数,否则
data-i18n不生效
localStorage 记住语言偏好时容易忽略的边界
存取 localStorage 不是“写了就一定成功”。无痕模式、Strict 浏览器策略、第三方 iframe 下可能静默失败或抛 SecurityError。
- 写入前先做可用性探测:
try { localStorage.setItem('test', 'ok'); localStorage.removeItem('test'); },失败则降级到sessionStorage或 URL 参数(如?lang=ja) - 页面加载时优先读
localStorage.getItem('uiLang'),没值再 fallback 到navigator.language;只存不读,刷新就回退,用户觉得“没记住” - 值建议用标准 BCP 47 标签(
zh-CN、en-GB),别用ch或eng;后端返回的语言码需标准化(转小写+连字符)再赋给lang - 不要在
localStorage存整个语言包 JSON——5MB 上限、同步阻塞、卡主线程;它只适合存语言码
最复杂的点不在翻译本身,而在语言上下文的穿透:从 document.documentElement.lang 到每个 <label lang="zh-Hans">,再到 <input data-i18n-placeholder="email_hint"> 背后的 JSON 键,再到服务端校验返回的错误码映射——漏掉任意一环,多语言就变成“看起来能切,实际处处断裂”的幻觉。



















