必须在SSR或SSG阶段将真实语言代码注入HTML模板的<html>标签,如Next.js用<html lang={locale}>(locale须来自路由或服务端配置),Nuxt用useLocaleHead,静态站硬编码;JS动态设置无效。

模板中怎么动态写入 lang 值才有效
必须在服务端渲染(SSR)或静态生成(SSG)阶段,把真实语言代码直接注入到 HTML 模板的 <html> 标签里,不能留到浏览器里用 JS 补。否则屏幕阅读器、Chrome 翻译按钮、Google SEO 全部读不到。
常见错误是模板里写成 <html lang="{{ locale }}">,但 locale 是空值、硬编码为 zh、或从客户端 localStorage 读取——这些都会导致首屏 HTML 的 lang 值不合法或缺失。
- Next.js:在
app/layout.tsx中用<html lang={locale}>,locale必须来自路由参数(如params.locale)或服务端配置,不能是客户端计算值 - Nuxt:用
useLocaleHead({ lang: 'zh-CN' }),且确保该 hook 在服务端执行并输出到初始 HTML - Hugo/Jekyll:每个语言版本生成独立 HTML 文件,
lang值硬编码进对应语言的模板,比如<html lang="en-US"> - PHP/Java 模板:根据请求头
Accept-Language或用户登录态,在服务端逻辑中拼出正确值,再插入模板
lang 值从哪来?别信客户端传的
浏览器发来的 Accept-Language 头只是线索,不是权威来源;用户登录态里的语言偏好才是真实意图。两者冲突时,以用户显式选择(如语言切换器)为准,且该选择必须同步写入服务端会话或 cookie,供下一次 SSR 读取。
绝对不要做这些事:
立即学习“前端免费学习笔记(深入)”;
- 用
navigator.language或localStorage.getItem('lang')在模板里插值 → 首屏 HTML 还没出来,JS 根本没执行 - 把
lang存在 URL query(如?lang=ja)却不解析它 → 模板收不到值 - 后端拿到
Accept-Language: zh-CN,zh;q=0.9就直接写zh→ 不符合 BCP 47,会被静默忽略
推荐做法:用户切换语言时,先发请求更新服务端 session,再整页刷新(window.location.href = '/ja/'),让新路由触发带正确 lang 的 SSR。
局部多语言内容怎么跟着动
主语言变了,页面里那些显式标注了 lang 的子元素(比如英文术语、日文引文、代码注释)不一定都要改——除非它们的内容也随语言切换而变化。但如果你做了动态翻译,就必须同步更新这些元素的 lang 属性。
- 静态混排(如固定英文 API 名词):
<p lang="en">fetch()</p>可以保持不变 - 动态混排(如翻译后的法语说明块):
<div lang="fr">{{ translatedText }}</div>的lang必须和当前主语言一致,或按实际内容语言设(比如主语言是日语,但这段是中文引文,就仍用lang="zh") - 避免用
<div lang="{{ locale }}">包裹整段内容 → 语义混乱,干扰父级:lang(zh)样式匹配
为什么重写 document.documentElement.outerHTML 是下策
有人试图在 SPA 切换语言时不刷新页面,而是用 JS 重写整个 <html> 标签:
document.documentElement.outerHTML = `<html lang="en-US">${document.documentElement.innerHTML}</html>`
这看似“生效”了,但实际问题一堆:
- ARIA 缓存不会清空,NVDA/VoiceOver 仍沿用旧语音引擎朗读已挂载的节点
- CSS
:lang()伪类不会重新计算,样式可能错乱 - 浏览器翻译按钮不会重新出现,因为它是基于初始 HTML 解析的
- 部分老版本 Safari 会直接崩溃
真正可靠的方案只有两个:服务端吐对值,或者整页刷新。其他都是在绕开浏览器和辅助技术的设计约束,迟早出问题。



















