跳转到内容

国际化

前端文案用 vue-i18n,枚举标签的事实源在后端,时间显示由请求头驱动。三块各有各的规则。

语言包

vue-i18n(legacy: false),中英双包按模块拆文件,分住两处:packages/locales/ 只放 shell 自身的命名空间,业务命名空间在 src/locales/,启动时由 registerLocaleMessages() 合并进同一个 i18n 实例(setupI18n() 之后、app.mount() 之前调用)。

text
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       # 消息模块
└── …

键名约定 模块.实体.字段或动作

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.{页面码中的 . 与 - 换成 _}

页面码后端 I18nKeymenu.ts 里的键
identity.positionmenu.identity_positionidentity_position
log.permission-changemenu.log_permission_changelog_permission_change

缺文案时侧边栏会直接显示键名(如 menu.identity_position)——看到这个就是漏了这一步。

Naive UI 内置文案

日期选择器、分页「X / 页」这类组件内置文案随应用 locale 切换:App.vueuseNaiveLocale() 注入 NConfigProviderlocale / date-locale

加新语言时别忘了这一处,否则业务文案切了、组件文案还是中文。

@ 会白屏

DANGER

语言包文案里出现@(如 联系 @admin)会触发 vue-i18n 的 linked message 语法,抛 Invalid linked format 导致整页白屏

必须转义成 {'@'}

ts
// ❌ 白屏
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,避免重复请求。

两个易错点

  1. business.ts 里的 *_OPTIONS 常量是写死中文的,仅作兜底,不要当成事实源使用。
  2. 选项的 value 要用枚举成员名,不是整数。后端实例数据经 JsonStringEnumConverter 序列化为成员名字符串(如 "Enabled"),只有用成员名才能与表格行数据匹配;整数值放在 valueText 备用。

时区

  • 前端在请求拦截器里发 X-Timezone 头(用户已选时区优先,否则跟随浏览器 Intl)。
  • 后端存储恒 UTC,输出时按该头换算。
  • 用户可在偏好里选时区。

「时间显示差几小时」的排查顺序:请求有没有带这个头 → 用户选的时区对不对 → 后端存的是不是 UTC。

加一门新语言

步骤做什么
1packages/locales/langs/src/locales/langs/ 下各加一套语言目录,按模块补齐文案
2packages/locales/index.tsmessagessrc/locales/index.ts 里登记新语言,并在 LocaleSwitcher.vueLOCALES 加一项
3useNaiveLocale() 里补对应的 Naive UI locale / date-locale 映射
4后端补一份 Enums.{culture}.json 与业务资源文件,否则枚举与后端消息仍是旧语言

第 4 步最容易漏——前端切了语言,接口返回的 message 和枚举标签却没变,就是后端资源没补。

相关页面

Released under The MIT License