EJS通过服务端渲染实现HTML国际化:先由中间件解析Accept-Language或URL参数确定req.locale,再加载对应JSON语言包,模板中调用t()辅助函数安全插值渲染,并同步设置document.documentElement.lang为BCP 47标准值。

怎么用 EJS 实现服务端 HTML 国际化解析
EJS 是轻量、易调试、无需预编译的模板引擎,适合在 Node.js 环境中做 SSR 多语言渲染。它不强制你写新语法, 和 就能直接注入变量或安全 HTML,对已有 HTML 结构侵入极小。
关键不是“怎么写 EJS”,而是“怎么让 EJS 渲染时知道该用哪套语言”。常见错误是把语言判断逻辑塞进模板里(比如 ......),这会导致模板膨胀、复数/格式化无法统一处理、SEO 内容不可控。
- 语言选择必须前置:由 Express 中间件根据
Accept-Language请求头(用accepts库解析)或 URL 参数(如/en/home)确定req.locale,再传给 EJS 渲染上下文 - 语言包按文件加载:每个 locale 对应一个 JSON 文件(
locales/en.json、locales/zh.json),结构扁平,键名一致,缺失字段留空字符串(避免undefined渲染) - 模板里只用
这类调用,翻译逻辑收口到一个t()辅助函数里——它负责查表、支持插值("Hello {name}"+{ name: 'Alice' })、自动 fallback 到默认语言 - 日期/数字/货币必须用
Intl:不能靠字符串拼接,EJS 模板里应调用t.dateTime(date)这类封装好的格式化方法,确保时区、千分位、货币符号都符合当前 locale
为什么 EJS 比 Pug/Handlebars 更适合快速国际化工程化
不是语法优劣问题,是维护成本和协作效率问题。Pug 的缩进敏感、Handlebars 的预编译要求、Nunjucks 的体积,都会拖慢多语言迭代节奏。
真实项目里,设计师改文案、运营补新语种、测试查漏翻,频率远高于功能开发。EJS 允许你直接在 HTML 里加注释说明翻译上下文(比如 <!-- i18n: button label, imperative mood --> ),也支持原生 JS 逻辑嵌入,调试时 console.log(req.locale) 一行就能验证语言是否被正确识别。
立即学习“前端免费学习笔记(深入)”;
- Pug 模板一旦嵌套层级深,
data-i18n属性容易漏写或错位,且编译后报错定位难 - Handlebars 必须提前注册 helper,
t()函数得手动绑定到每个模板作用域,切换语言时还得清缓存 - EJS 的
include可拆分语言包加载逻辑(如),但不强制——你可以全写在一个app.js里初始化,保持简单
怎么同步更新 document.documentElement.lang 和 EJS 渲染结果
服务端用 EJS 渲染出 ,只是静态输出;但用户在页面上点击「切换为中文」时,前端 JS 必须立刻响应,并保证 DOM 状态与语义一致。否则屏幕阅读器读错语言、浏览器翻译按钮失效、CSS ::lang(zh) 伪类不生效。
最容易被忽略的是:**lang 属性不是继承来的,也不是全局开关**。每个带 lang 的元素(比如一段英文引文 <blockquote lang="en"></blockquote>)都得单独保留其语言标识,不能被主语言切换覆盖。
- 服务端渲染时,
根节点的lang必须设为 BCP 47 标准值(如zh-Hans,不是zh_CN或chinese) - 前端语言切换后,JS 不仅要重渲染文本,还要遍历所有含
lang属性的元素,只更新那些没显式声明语言的节点(即lang值等于旧主语言的),跳过lang="ja"、lang="fr"这类局部标记 - 切换后立即触发
document.documentElement.setAttribute('lang', newLang),这是告诉浏览器“当前文档主语言变了”,但绝不递归设置子元素
JSON 语言包结构怎么设计才不容易崩
语言包不是越深越好,也不是越扁越好。崩点往往出现在:键名不一致、嵌套层级错位、复数形式硬编码、HTML 片段未过滤。
例如 en.json 里是 "form.error.required": "This field is required",而 zh.json 写成 "form.required_error": "此字段为必填项" —— 这种键名差异会让 t('form.error.required') 在中文环境下返回 undefined,最终页面显示空字符串或报错。
- 所有语言包必须用同一套键名,建议用小写字母+点号分隔(
header.nav.home),避免空格、大写、特殊字符 - 复数场景必须用库处理(如
i18next的count插值),不要自己写"item(s)"或"{{count}} item{{count > 1 ? 's' : ''}}"—— 阿拉伯语有 6 种复数形式,靠条件判断会漏 - 含 HTML 的翻译项(如
"tos.link": "请阅读<a href="https://www.php.cn/link/07fd2295ead5c4d45892fe3ab22a846a">服务条款</a>")必须走输出,且服务端需校验链接域名白名单,防止 XSS - 键名长度控制在 3 层以内(
common.button.submitOK,pages.dashboard.widgets.stats.loading.text易错且难维护)
真正卡住团队的,从来不是“怎么切语言”,而是“怎么让翻译键不散落、不冲突、不漏翻、不被误删”。EJS + 扁平 JSON + 显式 data-i18n 标记,是目前最可控的组合——它不追求炫技,但每次加新语言,你只需要确认三件事:JSON 键对齐了没、t() 函数 fallback 逻辑稳不稳、lang 属性有没有被局部内容反向污染。



















