<p>data-i18n 必须显式添加到每个可翻译文本节点,不可依赖父容器继承;属性翻译需用 data-i18n-* 后缀;语言包须扁平化 JSON 结构、键名严格对齐、并行加载;切换语言不刷新页面,需同步更新 DOM、document.lang 及子节点 lang 属性,并重建 Intl 实例。</p>

data-i18n 标记必须显式加在每个可翻译节点上
不是“加一次父容器就全搞定”,而是每个需要换语言的文本节点都得单独打 data-i18n。比如:<h1 data-i18n="home.title">首页</h1>、<input placeholder="请输入邮箱" data-i18n-placeholder="form.email">、<img alt="logo" data-i18n-alt="common.logo">。
data-i18n 只管 textContent,其他属性得用带后缀的写法: data-i18n-title、data-i18n-placeholder、data-i18n-alt。
别往 <script>、<style> 或 <pre> 里加——它们不渲染为用户可见文本,加了无效。
含 HTML 的文案(如“请阅读服务条款”)要用 innerHTML 替换,但语言包里对应值必须是预审过的纯 HTML 片段,否则有 XSS 风险。
JSON 语言包结构必须扁平且键名严格对齐
每个语言一个文件:locales/zh.json、locales/en.json、locales/ja.json,内容全是顶层键值对,例如:{"home.title": "首页", "form.email": "邮箱地址"}。
禁止嵌套:{"ui": {"home": {"title": "首页"}} 这种结构会让 JS 查找变慢、容易漏键、且难以做 diff 对比。
所有语言文件键名必须完全一致;某语言暂未翻译,也要留空字符串或占位符(如 "form.email": ""),否则 JS langPack[key] 会返回 undefined,导致空白。
建议按功能拆分小文件(common.json、form.json),用 Promise.all 并行加载,避免首屏阻塞。
切换语言时只批量更新 DOM,不 reload 页面
不要用 window.location.reload() 或跳转 URL 实现切换——会丢表单输入、滚动位置和组件状态。
正确做法是:遍历所有带 data-i18n 的元素,查语言包填值;同时同步更新 document.documentElement.lang(注意用 BCP 47 格式,如 'zh-Hans',不是 'zh_CN');再手动更新所有带 lang 属性的子节点(如引用日文的 <p lang="ja"> 不能跟着主语言一起切)。
若页面含时间/数字字段,需重建 Intl.DateTimeFormat 和 Intl.NumberFormat 实例,旧实例不会自动响应语言变更。
表单 value 不参与翻译(用户已输入的内容不能被覆盖),只处理 placeholder、title、alt 等提示类属性。
自动化维护靠 AST 解析,不是正则替换
用 parse5 + estree-walker 或 Cloudflare Workers 的 html-rewriter 做 DOM 树级操作,而不是全局搜 div class="header" 然后 replace——后者极易误伤模态框、评论区等上下文无关的节点。
例如:只把 <div class="header"> 且父节点是 <body>、且无 data-no-rewrite 属性的节点,替换成 <header class="header">,保留原始 class 防样式断裂。
CI 中接入 html-validate,启用 "semantic-elements": "error" 规则,禁止新增 <div class="card-body"> 这类非语义标签;对 legacy class 建白名单,新 class 必须符合 BEM 规范。
真正难的不是改完一次,而是让每次 git commit 都过得了 lint、跑得了 axe 无障碍扫描、截图比对不飘移。



















