前端多语言
本页在已启用前端的全栈项目中增加中英文页面,通过服务端明确选择 locale,保证 SSR 与首次 hydration 一致。下面的两种语言是应用示例数据,不涉及站点英文文档的翻译。
目录导航
Locale 来源
后端 config.locale 控制请求侧语言协商并填充 req.locale,frontend.i18n 控制前端消息扫描与加载。启用前端 I18n 后,HTML、页面 envelope 与浏览器导航共用具体的有效语言;"inherit" 不会成为字典键或 html lang。
当前端不需要扫描或打包 src/frontend/locales/** 时,设置 frontend.i18n.enabled=false。一次性的页面文案仍可以通过 res.render() 的 messages 显式传入。
关闭扫描时,除非 route 显式传入 messages,useVextI18n() 会返回空对象。Vext 生成的脚手架首页会使用中性的英文 fallback 保持可用;业务组件也应在访问嵌套文案前提供自己的 fallback:
语言优先级如下:
res.render(..., { locale })的具体语言。defaultLocale: "inherit"时,与前端字典匹配的req.locale。- 按
detect顺序探测;无匹配时回退到具体 defaultLocale、首个可用字典或 en-US。
detect 支持 query(query.locale)、header / x-vext-locale(X-Vext-Locale)、cookie(locale cookie)和 accept-language。Accept-Language 按质量权重协商,支持大小写和语言前缀匹配;未知来源会在配置校验时拒绝。URL 路径前缀、用户偏好与鉴权逻辑由 route 显式选择语言。
要继承请求侧协商,配置 locale.supported: ["en-US", "zh-CN"];请求侧缺少 supported 时使用其默认 en-US,匹配成功的继承语言优先于前端 detect。要独立按前端探测列表选择,可把前端 defaultLocale 设为具体语言。
res.render() 把最终 locale 与显式 messages 写入页面 payload,导航响应也携带它们。启用 htmlLang 时,SSR 与后续导航同步 <html lang>;htmlLang: false 移除 Vext 生成的 lang marker。
inject: "used" 尚未按组件裁剪消息,构建会给出诊断;当前选定语言仍完整加载,clientLoad: "current" 可限制语言数量。clientSwitch: "reload" 也不会自动创建选择器,切换步骤仍由应用实现。
前端 Locale 文件
页面文案放在 src/frontend/locales/**。
默认建议按功能模块组织,可继续细分二级目录;这是建议布局,扫描位置可由 frontend.i18n.source 覆盖:
例如 order/payment/zh-CN.json 导出 { "title": "支付" },组件通过 i18n.order.payment.title 读取。目录会形成对象命名空间,原有单文件模式及嵌套对象访问继续有效。文件支持 .ts/.mts/.cts/.js/.mjs/.cjs/.json;同一语言可来自多个模块,但同一规范化语言与目录不能重复定义来源,最终键也不能重复。
文件内顶层已全限定的点号键和数字业务码保持原键,用 i18n["order.payment.title"] 等方括号访问。不要同时用目录键和全限定键定义同一最终消息。前端保留对象结构;后端校验器的消息表采用扁平键,不能将两者的访问方式混用。
也可继续使用根目录单文件布局。下面采用独立 demo 命名空间,避免覆盖模板已有消息;创建以下两个文件:
同一模块的各 locale 文件应保持相同对象结构。缺 key 应视为内容错误,框架不会因为泛型声明就补齐实际消息。
组件中使用文案
Vext 默认前端 API 是对象访问,而不是 t("a.b.c")。
对应路由明确校验并传入语言:
组件明确知道该语言在服务端与浏览器均已加载时,也可以指定(沿用上面的 Messages 类型):
hook 只读取 context,不下载消息也不切换全局语言;指定语言不在 allMessages 时返回当前 messages。current 模式下不要在另一语言页面强行读取未加载语言:SSR 可见的语言集合与浏览器可能不同,造成 mismatch。手工传 messages 会替换当前消息对象,并非深合并补丁。
SSR 与 Hydration
将下面配置合并到 src/config/default.ts。解析器的 i18n 默认关闭,全栈模板已显式开启;这里同时给出明确 fallback 和默认加载模式:
"current" 表示浏览器 hydration 只加载 SSR 当前 locale,减少首屏 JS。
确实需要在浏览器读取其他已构建语言时,可将上述 clientLoad 改为 "all";下面是对应字段片段,沿用已启用的配置:
切换语言
all 会加载所有扫描到的语言,但不自动创建语言选择器、保存用户偏好、改变 html lang 或触发页面重渲染。应用仍需实现这些动作;最简单的完整切换流程是 reload-based:
- 用户选择语言。
- 写入 cookie、用户偏好 API、URL prefix 或其他服务端可见位置。
- 导航或刷新页面。
- SSR 和 hydration 使用同一个 locale。
避免服务端先渲染一种语言,而浏览器 hydration 立刻渲染另一种语言。
缓存与 Vary
如果 HTML 会因语言不同而变化,缓存 key 必须包含语言。
常见策略:
- 保留
Vary: Accept-Language - CDN key 包含 locale
- 使用
/en、/zh这样的语言路径前缀 - 反向代理 key 包含语言偏好 cookie
本页的 /language/en-US 和 /language/zh-CN 直接以 URL 区分语言。i18n.vary: true 合并实际探测涉及的 Accept-Language、X-Vext-Locale 或 Cookie 与既有 Vary;仅由 URL 选择语言时无需追加语言头。设为 false 时由应用负责外部缓存的变体隔离。
框架 freshness 使用同一有效语言构造 key。Cookie、认证/session、自定义 req.locale 及无法由已声明请求来源或 URL 区分的 render locale 覆盖绕过公共缓存。static/revalidate 路由须把影响语言的请求输入纳入 detect 或 URL;依赖未声明的 header/身份变化时使用 dynamic。显式 render locale 与查找语言不同的结果不写入该 key,防止错误回放。缓存层区别见Render Data 与缓存。
生成的 JS/CSS 与 locale chunk 通常通过内容 hash URL 区分;缓存这些具体文件不等同于缓存按语言变化的 HTML。
验证示例
执行 npm run build,成功后运行 npm start -- --port 3000,分别访问两个语言 URL。原始 HTML、h1、html lang 与 hydration 后文案都应对应语言,Console 无 mismatch;普通链接应完整导航到另一语言。请求 /language/unknown 应返回 404。当前显式路径示例不应因更换 Accept-Language 而覆盖路由已选择的语言。结束后停止服务。