国际化必须显式为每个含文本的语义化标签设置lang属性,所有可翻译属性(如placeholder、alt、title、aria-label)均需对应data-i18n扩展属性,多语言code须分层带服务前缀,动态dom需手动触发翻译。

lang 属性必须显式写在每个语义化标签上
只改 根节点,其他地方不加 lang,等于没做国际化。浏览器、屏幕阅读器、字体回退链全按默认语言处理——顿号变英文间距、<pre class="brush:php;toolbar:false;" lang="bash"></pre> 里的代码被中文字体覆盖、<img alt> 还是读英文。
实操建议:
- 所有含文本的语义化标签(
<h1></h1>、<p></p>、<section></section>、<footer></footer>等)都手动加lang属性,值与当前语言包一致,比如lang="zh-Hans" - 已有
lang的特殊元素(如<pre class="brush:php;toolbar:false;" lang="bash"></pre>、<code lang="sql">)保留原值,这是多语言混排的合法场景 -
<script></script>和<style></style>内部不要加lang,它们不参与文本渲染
data-i18n 必须覆盖所有可翻译属性,不只是 textContent
只给 <button>提交</button> 加 data-i18n="btn_submit",但漏掉 placeholder、alt、title 或 aria-label,就会出现:输入框提示仍是英文、图片替代文本没更新、tooltip 不翻、辅助技术播报错位。
实操建议:
- 基础文案用
data-i18n;含placeholder就额外加data-i18n-placeholder;同理data-i18n-alt、data-i18n-title、data-i18n-aria-label -
value属性一般不翻译(属于用户输入数据),但<button></button>和<input type="submit">的显示文案建议统一走textContent更新 - 含 HTML 结构的文案(如“请阅读服务条款”)必须用
innerHTML替换,且语言包里对应值要是可信纯 HTML 片段,不能带用户输入或 JS 执行逻辑
多语言 code 命名需分层且带服务前缀
直接写 intl.get("save") 看似简单,但跨模块时极易冲突——两个团队都定义了 "save",一个指按钮,一个指 API 响应提示,最后谁覆盖谁全看加载顺序。
实操建议:
- code 分两段:
[服务名].[功能名],例如"hpfm.event"(hpfm是服务,event是功能模块) - 第二段再细分:
model.[模型名].[字段名](如model.event.code)或view.[类型].[具体含义](如view.button.save、view.validation.require) - 平台通用文案统一放在
hzero.common下,避免重复定义;跨模块引用时,直接用对方已发布的 code,不另起新键 - 含冒号或花括号的内容必须转义,比如
"测试\:冒号"、"{name} 姓名"中的{要确保语言包里是字符串而非模板语法
动态插入的 DOM 必须手动触发翻译
AJAX 加载的弹窗、分页表格新行、懒加载模块插入后,data-i18n 不会自动生效——它只是个标记,不是监听器。不调用翻译函数,里面的文案永远是原始 key。
实操建议:
- 每次
appendChild、innerHTML = ...或框架render后,立即遍历新节点并执行翻译逻辑,比如translateElement(newNode) - 若用 React/Vue,别依赖全局 i18n 挂载点,应在组件生命周期或
useEffect/mounted中主动触发翻译,尤其注意 Portal 渲染的模态框 - SSR 场景下,服务端渲染时就要完成初始翻译,客户端 hydration 前确保语言包已就位,否则会出现闪屏或文本错乱
lang 漏一个标签、data-i18n-placeholder 少写一次、code 用了无前缀短名、动态节点没触发翻译——任意一项出问题,国际化就断在某个环节,而且很难排查。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











