最轻量可控方案是用data-i18n标记元素、JSON语言包及fetch加载;需为所有待翻译节点添加data-i18n属性(如header_title),区分placeholder/value等后缀,统一键名且不可更改;JSON须扁平键值对、符合BCP 47标准(如zh-Hans)、正确MIME类型;切换时同步更新document.documentElement.lang和localStorage,并手动维护显式lang的子元素以保障可访问性。

直接用 data-i18n 标记 + JSON 语言包 + fetch 加载是最轻量、最可控的方案。硬编码到 HTML 或靠 CSS 显隐双语 DOM,后期维护和可访问性都会出问题。
怎么给 HTML 元素打翻译标记
所有要翻译的文本节点(<h1>、<p>、<button> 等)必须加 data-i18n 属性,值为统一键名:
<h1 data-i18n="header_title"></h1> <input type="text" data-i18n-placeholder="search_hint">
注意:data-i18n-placeholder 这类属性不能靠通用逻辑自动识别,得在 JS 中显式处理;不要用 id 或 class 当翻译键——它们语义不明确,且容易和样式/行为逻辑冲突。
- 动态插入的 DOM(比如弹窗、表格行)必须在插入后立即调用翻译函数,否则不会生效
- 带 HTML 的文案(如含
<strong>的说明)要用innerHTML赋值,但需确保内容可信,避免 XSS - 按钮、表单控件等有多个可翻译属性(
value、placeholder、title),得分别对应不同后缀,比如data-i18n-value
JSON 语言包怎么组织才不容易崩
每个语言一个文件,路径统一,例如 locales/en.json、locales/zh-Hans.json,结构必须扁平、纯键值对:
立即学习“前端免费学习笔记(深入)”;
{ "header_title": "Welcome", "search_hint": "Search anything..." }
键名一旦定下就不能改,否则所有页面对应位置会 fallback 到 key 本身(显示 “header_title” 字符串)。常见错误:
- 用
zh_CN或chinese当文件名 —— 必须用 BCP 47 标准码,如zh-Hans、ja、pt-BR - JSON 文件没设正确 MIME 类型(
application/json),导致fetch失败但控制台不报错 - 漏掉 fallback:用户选了
zh-HK,但只有zh-Hans.json,必须兜底加载en.json或主语言包,否则整页文案为空
切换时必须同步改哪些地方
只替换文本是不够的。语言切换本质是两件事:内容更新 + 语义声明更新。缺一不可:
- 调用
document.documentElement.lang = 'zh-Hans'—— 这影响屏幕阅读器、浏览器翻译、字体回退链(比如日语假名不加lang="ja"可能显示为方块) - 手动遍历并更新所有已渲染的
lang属性,尤其是混排内容:<p lang="en">API</p>、<pre lang="bash">curl ...</pre>,这些不会被data-i18n覆盖,但必须保留原lang值 - 把当前语言写进
localStorage.setItem('lang', 'zh-Hans'),并在页面初始化时读取,否则刷新就回退到默认
别试图靠改 document.body.dataset.lang 或加 class 来“模拟”语言切换——lang 是标准属性,只有它被浏览器和辅助技术真正识别。
为什么 document.documentElement.lang 不能只设一次
因为 lang 属性作用域是“该元素及其子节点”,根节点改了,不影响已存在的子元素上显式写的 lang。比如你页面里有一段:<blockquote lang="fr">C’est magnifique</blockquote>,切换成中文后,这段仍得是法语,不能被根 lang 覆盖。而如果某处漏写了 lang,又没 fallback 机制,屏幕阅读器就会按旧语言朗读新文本,造成语义错乱。这是最容易被忽略的可访问性断点。



















