国际化
前端文案用 vue-i18n,枚举标签的事实源在后端,时间显示由请求头驱动。三块各有各的规则。
语言包
vue-i18n(legacy: false),中英双包按模块拆文件,分住两处:packages/locales/ 只放 shell 自身的命名空间,业务命名空间在 src/locales/,启动时由 registerLocaleMessages() 合并进同一个 i18n 实例(setupI18n() 之后、app.mount() 之前调用)。
packages/locales/langs/zh-CN/ # shell 命名空间
├── common.ts # 通用(按钮、字段、提示)
├── component.ts # 公共组件文案
├── menu.ts # 菜单标题
├── header.ts # 顶栏
├── page.ts # 页面壳
└── …
src/locales/langs/zh-CN/ # 业务命名空间
├── identity.ts # 身份权限模块
├── log.ts # 日志模块
├── message.ts # 消息模块
└── …键名约定 模块.实体.字段或动作:
// src/locales/langs/zh-CN/identity.ts
export default {
user: {
page_name: '用户管理',
col_status: '状态',
action_reset_password: '重置密码',
},
}
// 使用:t('identity.user.col_status')切换语言:useLocale().setLocale('en-US')。
菜单文案的键怎么来
后端 PageDescriptor.I18nKey 规则是 menu.{页面码中的 . 与 - 换成 _}:
| 页面码 | 后端 I18nKey | menu.ts 里的键 |
|---|---|---|
identity.position | menu.identity_position | identity_position |
log.permission-change | menu.log_permission_change | log_permission_change |
缺文案时侧边栏会直接显示键名(如 menu.identity_position)——看到这个就是漏了这一步。
Naive UI 内置文案
日期选择器、分页「X / 页」这类组件内置文案随应用 locale 切换:App.vue 里 useNaiveLocale() 注入 NConfigProvider 的 locale / date-locale。
加新语言时别忘了这一处,否则业务文案切了、组件文案还是中文。
裸 @ 会白屏
DANGER
语言包文案里出现裸 @(如 联系 @admin)会触发 vue-i18n 的 linked message 语法,抛 Invalid linked format 导致整页白屏。
必须转义成 {'@'}:
// ❌ 白屏
contact: '联系 @admin'
// ✅
contact: "联系 {'@'}admin"邮箱、社交账号、装饰性符号都是高发区。新增文案前扫一遍。
枚举标签:后端单一事实源
枚举标签不在前端语言包里维护——事实源是后端枚举元数据(Enums.{culture}.json 全量),按请求头 X-Language 返回当前语言的标签。
前端有三条取值路径:
| 场景 | 用法 |
|---|---|
| Schema 页字段 | 字段声明 dictionaryCode(枚举名或字典码),useSchemaDictionaries 批量拉取注入 field.options;单元格按值映射 label,搜索区自动渲染下拉 |
| 非 Schema 的下拉/标签 | useEnumOptions(enumName, fallback)(~/hooks),返回随语言/数据响应式更新的 computed |
| 静态兜底 | 元数据为空(未加载/未部署)时回退传入的 fallback 静态选项,保证绝不出现空下拉 |
切语言要响应式
拉取由全局 useEnumService 并发去重,切 locale 时整库重取一次。useSchemaDictionaries / useEnumOptions 只读响应式状态——免刷新即随语言更新,各下拉不各自监听 locale,避免重复请求。
两个易错点
business.ts里的*_OPTIONS常量是写死中文的,仅作兜底,不要当成事实源使用。- 选项的
value要用枚举成员名,不是整数。后端实例数据经JsonStringEnumConverter序列化为成员名字符串(如"Enabled"),只有用成员名才能与表格行数据匹配;整数值放在valueText备用。
时区
- 前端在请求拦截器里发
X-Timezone头(用户已选时区优先,否则跟随浏览器Intl)。 - 后端存储恒 UTC,输出时按该头换算。
- 用户可在偏好里选时区。
「时间显示差几小时」的排查顺序:请求有没有带这个头 → 用户选的时区对不对 → 后端存的是不是 UTC。
加一门新语言
| 步骤 | 做什么 |
|---|---|
| 1 | packages/locales/langs/ 与 src/locales/langs/ 下各加一套语言目录,按模块补齐文案 |
| 2 | 在 packages/locales/index.ts 的 messages 与 src/locales/index.ts 里登记新语言,并在 LocaleSwitcher.vue 的 LOCALES 加一项 |
| 3 | 在 useNaiveLocale() 里补对应的 Naive UI locale / date-locale 映射 |
| 4 | 后端补一份 Enums.{culture}.json 与业务资源文件,否则枚举与后端消息仍是旧语言 |
第 4 步最容易漏——前端切了语言,接口返回的 message 和枚举标签却没变,就是后端资源没补。
相关页面
- 布局与主题:外观定制
- Schema 驱动页面:
dictionaryCode用法 - 接口对接指南:枚举与时间的序列化规则
- 常见问题:白屏排查
