i18n 多语言接入使用文档
1. 概述
涵盖场景:前端固定标签、后端消息、字典数据、业务数据、邮件模板。统一存库(system_i18n_translation),前端通过接口按需加载;业务 key 以数据库为准,common/ui 等由框架或本地 base 提供。
自动化脚本:前端固定标签(extract:i18n)、后端消息(sync:i18n-message)、字典数据(sync:i18n-dict)均支持脚本自动化,免 token、免人工。
2. 前端固定标签翻译
代码中的固定文案(按钮、表单标签、表格列头、提示等)通过提取脚本写入数据库(scene_type=1),前端经 GET /system/i18n-pack/load?locale=xx 加载并与本地 base 合并。
需要对哪些使用 $t()
凡是在界面上直接展示给用户的中文(或需多语的)固定文案,都应使用 $t('key', { defaultValue: '...' }) 包裹,以便被提取并参与多语。具体包括:
| 场景 | 说明 | 示例位置 / 写法 |
|---|---|---|
| 页面/弹窗/区块标题 | 列表页标题、详情页标题、抽屉/弹窗标题、路由 meta.title、Tab 标签、步骤条标题、卡片/区块标题 | Page 的 title、meta.title、弹窗 title、tabs[].label、steps[].title |
| 表格列头 | 表格列的 label | columns[].label、schema 中列定义 label |
| 表单标签 | 表单项前的文字、详情页字段名 | schema[].label、formOptions 中 field.label、详情 description 的 label |
| 占位符 | 输入框、选择框的 placeholder | placeholder、componentProps.placeholder |
| 校验提示 | 表单校验失败时的提示、zod 等校验 message | rules、z.string().min(1, $t(...))、{ message: $t(...) } |
| 按钮文字 | 主按钮、行内操作、工具栏按钮(新增/导出/导入等) | 按钮文本、#extra / #actions、toolbarConfig 中按钮文案 |
| 操作/链接文案 | 复制、编辑、删除、查看等操作名 | 操作列、下拉菜单项、链接文本 |
| 选项标签 | 非字典的固定选项(如业务线、业态等静态选项) | options[].label、{ value, label: $t(...) } |
| 确认对话框 | 确定/取消按钮、对话框标题与正文(如「确定删除选中的 x 条吗?」) | confirm($t(...))、Modal.confirm 的 title/content/okText/cancelText |
| 提示/反馈 | 成功、失败等 toast/message 文案 | message.success($t(...))、message.error($t(...)) |
| 空状态文案 | 表格/列表无数据、下拉无选项时的提示 | emptyText、空状态组件文案、「暂无数据」等 |
| 帮助与说明 | 表单项下方的 helpText、图片等组件的 mask/tooltip、表格列 help | helpText、componentProps.mask、列定义 help 等 |
不必用 $t():字典项用 useDictOptions/dict-tag(走 dict_{dictType}_{value});后端返回的 msg 由后端翻译;纯数字/ID/占位符变量不参与多语。
格式:提取脚本只识别 $t('key', { defaultValue: '...' })(key 与 defaultValue 必填)。带插值用命名参数,如 $t('key', { defaultValue: '共 {count} 条', count: 变量 }),占位符名与参数键一致。
使用方式
- 需被提取:
$t('key', { defaultValue: '...' })(提取脚本只识别该形式)。 - key 已存在(如
ui.actionTitle.edit、路由 meta.title):可写$t(key),不传第二参数。 - 带替换:命名参数
$t(key, { defaultValue: '…', count: a })可提取;数组$t(key, [a,b])对应{0}、{1},key 须已在语言包。模板字符串`共 ${total} 条`提取后变为{0},调用时传0: total。
<h1>{{ $t('web.crmLead.assignRuleForm.title.edit', { defaultValue: '编辑自动分配规则' }) }}</h1>
$t('ui.actionTitle.edit', ['员工'])
$t('web.crmLead.poolImport.message.importSuccess', {
defaultValue: '导入成功:共处理 {count} 个文件,导入 {success} 条线索',
count: files.length,
success: result.successCount || 0,
})
翻译键命名
建议:模块.功能.组件.属性,如 web.crmLead.assignRuleForm.title.edit、common.button.save。defaultValue 在 key 不存在时作为展示文案,存在时用语言包译文。
提取与同步
执行方式:在项目根目录下执行:
cd yunfun-ui-admin-vben
pnpm run extract:i18n
脚本 scripts/extract-i18n-keys.mjs:扫描 apps/web-antd/src、packages/**/src 下 .vue/.tsx/.jsx/.ts/.js,识别 $t('key', { defaultValue: '...' })(支持单/双引号、模板字符串;模板中 ${expr} → {0}、{1});将 zh-CN 上传至 {API_BASE_URL}/admin-api/system/i18n-pack/upload,写入 scene_type=1,并调自动翻译为 i18n.languages 中其他语言。默认不写本地文件(WRITE_LOCAL_FILES=true 时写 zh-CN 的 JSON)。
覆盖策略:脚本采用合并模式(overwrite=false):数据库中已存在的 key 会被跳过、不覆盖;只有本次扫描到且库中不存在的 key 才会新增入库并触发翻译。适合多人协同开发,可放心多次执行,不会破坏已有译文。
新增键流程:代码中写 $t('key', { defaultValue: '...' }) → 执行 pnpm run extract:i18n → 新键入库,前端 load 时生效;可在 系统管理 → 国际化翻译 → 开发配置 → 语言包管理 查看。
环境变量(脚本请求带 admin-api 前缀):
| 变量名 | 说明 | 默认值 |
|---|---|---|
API_BASE_URL | 后端根地址 | .env.development 的 VITE_BASE_URL 或 http://localhost:48080 |
API_TOKEN | 登录态 Token | 无 |
API_TENANT_ID | 租户 ID | 1 |
AUTO_TRANSLATE | 是否自动翻译 | true |
WRITE_LOCAL_FILES | 是否写本地 zh-CN JSON | false |
模块识别(用于上传分组/本地文件名):web.crmLead.*→crm,web.systemUser.*→system 等;key 含点取第一段,否则 common。库内为扁平 key-value,load 接口按 locale 返回。
3. 后端消息翻译
异常由 GlobalExceptionHandler 拦截,用 translationKey = "ERROR_" + ex.getCode() 查 I18nMessageUtil.getMessage,返回译文或原文。前端直接展示接口返回的 msg 即可。
管理:系统管理 → 国际化翻译 → 开发配置 → 翻译记录,筛选「后端消息」或前缀 ERROR_,可批量翻译或单条编辑。
错误码与消息翻译规范
- 翻译键:
ERROR_+ 错误码数字(如1003001000→ERROR_1003001000),与GlobalExceptionHandler约定一致;错误码段遵循ServiceErrorCodeRange。 - 存储:
system_i18n_translation,scene_type=2。每条错误码至少一条 zh-CN 源语言(source_text/translated_text与ErrorCode.getMsg()一致),唯一约束(translation_key, target_lang, tenant_id, deleted)。 - category:1 异常 / 2 验证 / 3 业务 / 4 系统;业务异常建议 1。
- 接入:业务中
throw exception(ErrorCodeConstants.XXX);脚本自动提取并同步,或在翻译记录页手动新增/修复翻译。
后端消息自动化流程(推荐):
- 脚本自动化:执行
pnpm run sync:i18n-message,脚本会:1)从 Java 源码提取错误码;2)调用sync-error-codes-to-i18n写入翻译表;3)调用auto-translate自动翻译。免 token、免人工。 - 页面手动修复翻译:系统管理 → 国际化翻译 → 开发配置 → 翻译记录,筛选「后端消息」或前缀
ERROR_,勾选后 批量翻译 或单条编辑。
4. 字典数据翻译
翻译键:dict_{dictType}_{value}。接口:GET /system/i18n-translation/get-text?translationKey=...&targetLang=...,返回译文字符串。
使用:useDictOptions(dictType, valueType) 取选项列表(自动翻译+缓存);dict-tag 展示标签。手动调用:getTranslationText(translationKey, currentLang),返回字符串,失败用 originalLabel 兜底。
存储:system_i18n_translation,scene_type=3。
字典自动化流程(推荐):
- 前置:字典管理 中已存在相关字典类型及数据。
- 脚本自动化:执行
pnpm run sync:i18n-dict,脚本会:1)调用sync-dict-to-i18n将system_dict_data同步到翻译表;2)调用auto-translate自动翻译。免 token、免人工。 - 页面手动修复翻译:系统管理 → 国际化翻译 → 开发配置 → 翻译记录,筛选「字典数据」,勾选后 批量翻译 或单条编辑。
零散补充:字典管理 中进入某条字典编辑,表单底部点击 翻译,可写入 zh-CN 并触发翻译。
5. 业务数据翻译
后端 TranslationAspect + TranslationService.handleQuery 按当前用户语言将业务字段翻译后返回,前端正常调业务 API 即可。翻译键:{client}.{module}.{domain}.{name}(如 web.crm-lead.leadTodo.todoContent)。
后端接入方式(按当前实现):
- 对象上加领域注解:在返回给前端的 VO / CMD / CO 类上标
@TranslateDomain(module = "crm-lead")、@TranslateDomain(module = "system")等,用于声明所属模块。 - 字段上加业务数据注解:在需要做多语的字段上标
@Translate(domain = "leadTodo", name = "todoContent", id = "id")这类注解,domain对应业务域,name为字段名,id为主键字段名(默认id)。当前项目只使用字段级@Translate,方法级@Translate作为开关能力暂未使用。 - 业务数据多语:仅依赖 VO 上的
@TranslateDomain+@Translate注解,无需额外配置。TranslationAspect在 Controller 方法返回后扫描注解并回填翻译。 - 查询时自动替换:用户语言为非 zh-CN 时,切面(
TranslationAspect)按fieldKey + dataId从system_i18n_business_data/system_i18n_translation批量取译文,仅对存在翻译记录的字段做替换,未配置或无译文的字段保持原值。
前端行为与管理:
- 表单录入组件(I18nInput):在需要录入可多语的业务字段时,使用
@vben/common-ui提供的I18nInput(例如在线索池 / 线索个人 / 线索待办等编辑页中),通过module/domain/ 字段名等配置,与后端@TranslateDomain/@Translate一一对应,统一录入「原文 + 各目标语言」。 - 业务接口调用:普通业务页面(如线索列表、来源管理)只需调用原有业务 API,即可拿到已按用户语言翻译后的字段,无需在前端额外
$t()或手动调用翻译接口。 - 业务数据翻译管理页:在 系统管理 → 国际化翻译 → 翻译工作台 → 业务数据翻译 中,可按模块/域/字段、
fieldKey与dataId查询、编辑和批量维护业务数据翻译。 - 前端手动调用(可选):如需在自定义页面中手工维护某条业务数据的翻译,可使用
#/api/system/i18n/business-data中的接口,例如:getBusinessDataList(fieldKey, dataId)查询现有翻译,saveBusinessDataTranslation({ fieldKey, dataId, sourceText, sourceLang, targetLangTranslations })保存修改。
列表页多语搜索(I18nSearchHelper)
列表页按多语字段搜索时,需同时匹配主表字段与 i18n 翻译表(source_text、translated_text)。I18nSearchHelper 提供统一的多语搜索能力。
接入方式:
- PageReqVO 实现接口:分页请求 VO 实现
I18nSearchable,Service 注入I18nBusinessDataService。 - Service 层:调用
enrichWithFields(全字段模糊)或enrich(支持 per-field fuzzy),从 i18n 表反查data_id填充到 VO:
// 全字段模糊匹配(从 voClass 解析 domainPrefix)
I18nSearchHelper.enrichWithFields(pageReqVO, LeadPoolRespVO.class,
List.of("brandName", "contactName", "mobile"),
fieldName -> switch (fieldName) {
case "brandName" -> pageReqVO.getBrandName();
case "contactName" -> pageReqVO.getContactName();
case "mobile" -> pageReqVO.getMobile();
default -> null;
},
i18nBusinessDataService);
// 部分字段精确匹配(从 voClass 解析 fieldKey)
I18nSearchHelper.enrich(pageReqVO, LeadTodoRespVO.class,
List.of(
I18nSearchFieldConfig.Spec.field("todoContent"), // 模糊
I18nSearchFieldConfig.Spec.field("brandName", false) // 精确
),
fieldName -> switch (fieldName) {
case "todoContent" -> pageReqVO.getTodoContent();
case "brandName" -> pageReqVO.getBrandName();
default -> null;
},
i18nBusinessDataService);
- Mapper 层:使用
getI18nIds获取data_id,applyI18nLikeCondition/applyI18nEqCondition构建条件(主表字段 LIKE/EQ 搜索词) OR (id IN i18nIds):
I18nSearchHelper.applyI18nLikeCondition(wrapper, reqVO.getBrandName(),
I18nSearchHelper.getI18nIds(reqVO, "brandName"),
LeadMainDO::getBrandName, LeadMainDO::getId);
field_key 约定:{client}.{module}.{domain}.{name}(如 web.crm-lead.leadPool.brandName)。可使用 TranslationAnnotationUtils.getFieldKeyFromAnnotation(voClass, fieldName, client) 从 @Translate 注解解析。
业务数据删除时自动清理多语
在 Service 层的删除方法上添加 @CleanupI18nOnDelete 注解,业务数据删除成功后会自动清理对应的多语数据。
注解参数:
| 参数 | 必填 | 说明 |
|---|---|---|
module | 是 | 模块编码(如 "crm-lead"),对应 @TranslateDomain 的 module |
domain | 是 | 业务域(如 "leadTodo"),对应 @Translate 的 domain |
doClass | 否 | 业务 DO 类,用于获取表名,清理时按 business_table_name 过滤,兼容历史数据 |
dataIdParam | 否 | 数据ID参数名,默认 "id",支持 SpEL(如 "#reqVO.id") |
batch | 否 | 是否批量删除,默认 false |
示例:
// 单个删除(指定 doClass 便于按表名过滤)
@Override
@CleanupI18nOnDelete(module = "crm-lead", domain = "leadTodo", doClass = LeadTodoDO.class)
@Transactional(rollbackFor = Exception.class)
public void deleteLeadTodo(Long id) {
leadTodoMapper.deleteById(id);
}
// 批量删除
@Override
@CleanupI18nOnDelete(module = "crm-lead", domain = "leadTodo", batch = true, dataIdParam = "ids")
@Transactional(rollbackFor = Exception.class)
public void deleteLeadTodos(List<Long> ids) {
leadTodoMapper.deleteBatchIds(ids);
}
业务数据多语快照(审批场景)
适用场景:审批流程中需要暂存变更的多语数据,待审批通过后再正式生效。例如客户变更审批、品牌变更审批等场景。
与 @Translate 的区别:
| 特性 | @Translate | @TranslateSnapshot |
|---|---|---|
| 存储位置 | system_i18n_business_data(业务多语表) | system_i18n_business_data_snapshot(快照表) |
| 生效时机 | 立即生效 | 审批通过后同步生效 |
| 适用场景 | 普通业务数据保存/查询 | 审批流程中的暂存数据 |
| 同步方式 | 自动保存/自动查询回填 | 手动调用同步接口 |
后端接入方式:
-
VO/CMD/CO 类上加领域注解:与
@Translate相同,使用@TranslateDomain(module = "crm-customer")声明所属模块。 -
字段上加快照注解:在需要做多语的字段上标
@TranslateSnapshot,配置与@Translate基本一致,额外支持doClass指定 DO 类用于自动获取表名:
@TranslateDomain(module = "crm-customer")
public class CustomerSaveReqVO {
@TranslateSnapshot(domain = "customer", name = "customerName", id = "id", doClass = CustomerDO.class)
private String customerName;
@TranslateSnapshot(domain = "customer", name = "customerDesc", id = "id", doClass = CustomerDO.class)
private String customerDesc;
// 多语 Map 字段,后缀默认 ML
private Map<String, String> customerNameML;
private Map<String, String> customerDescML;
}
- Controller 方法上加注解:在需要启用快照处理的 Controller 方法上加
@TranslateSnapshot作为开关(方法级注解本身不需要配置 domain/name,只作为开关使用):
@RestController
@RequestMapping("/crm/customer")
public class CustomerController {
@PostMapping("/save")
@TranslateSnapshot // 启用快照处理
public Result<Long> saveCustomer(@RequestBody @Valid CustomerSaveReqVO reqVO) {
return success(customerService.saveCustomer(reqVO));
}
}
- 同步机制:审批通过后,调用
I18nBusinessDataSnapshotService.syncSnapshotToBusinessData将快照数据同步到业务多语表:
@Service
@RequiredArgsConstructor
public class CustomerApprovalService {
private final I18nBusinessDataSnapshotService snapshotService;
/**
* 审批通过回调
*/
public void onApprovalPassed(Long customerId) {
// 将快照表数据同步到业务多语表,正式生效
snapshotService.syncSnapshotToBusinessData("crm_customer", customerId.toString());
}
}
关键说明:
- 保存时:切面自动识别
@TranslateSnapshot标记的字段,将多语数据写入快照表(以business_table_name + data_id为维度)。 - 查询时:切面自动从快照表读取当前用户语言翻译并回填字段,同时填充
xxxMLMap 供前端编辑回显。 - dataId 获取:优先从
idMethod指定的方法获取,其次从id指定的字段获取,新增数据时若 ID 为空则跳过保存。 - 表名获取:优先从
doClass的@TableName注解读取,其次尝试从 VO 类名推断对应 DO 类。
📝 示例
表单/按钮/表格列:$t('web.crmLead.xxx.field.name', { defaultValue: '规则名称' })、$t('common.button.save', { defaultValue: '保存' })、列 label: $t('web.crmLead.table.column.name', { defaultValue: '名称' })。
业务数据多语表单(I18nInput + 注解对应关系,示例摘自线索公海):
<I18nInput
:model-value="field.value"
@update:modelValue="(val: string) => field.onChange?.(val)"
v-model:modelValueMl="companyNameML"
module-code="crm-lead"
domain="leadPool"
name="companyName"
:data-id="formData?.id ? Number(formData.id) : 0"
@translate="(p) => handleI18nTranslate('companyName', p)"
:placeholder="$t('web.crmLead.poolForm.placeholder.companyName', {
defaultValue: '请输入公司名称',
})"
/>
对应后端 VO 上的配置大致为:
@TranslateDomain(module = "crm-lead")
public class LeadPoolRespVO {
@Translate(domain = "leadPool", name = "companyName", id = "id")
private String companyName;
}
6. 邮件模板翻译
发送时按收件用户语言(用户表 locale → 请求上下文 → 默认 zh-CN)取译文,键:mail.template.{模板code}.title、mail.template.{模板code}.content。存 system_i18n_translation,scene_type=5,category=4。
自动流程:保存/更新邮件模板时自动写 zh-CN 源语言、各目标语言占位,并触发按 mail.template.{code}. 前缀的批量翻译。业务无需单独调翻译接口。
管理:系统管理 → 国际化翻译 → 开发配置 → 翻译记录,筛选「邮件模板」或前缀 mail.template.,可批量翻译或单条编辑。占位符(如 {name})翻译时保留。
❓ 常见问题
| 问题 | 答案 |
|---|---|
| 如何查看语言包? | 系统管理 → 国际化翻译 → 开发配置 → 语言包管理 / 翻译记录(按 scene_type 筛选);前端切换语言时自动 load。 |
| extract:i18n 报错? | 脚本依赖后端,需启动服务、API_BASE_URL 正确,需登录时设 API_TOKEN;仅上传不翻译可设 AUTO_TRANSLATE=false。 |
| 只翻译部分语言? | 平台配置 i18n.languages 只保留需要的语言即可。 |
| 是否保留 locales/langs? | 先加载本地 base + 框架 common/ui,再合并 load 接口数据;业务 key 不要求维护本地 JSON,可选 WRITE_LOCAL_FILES=true 写 zh-CN 快照。 |
| 支持语言在哪配置? | 平台配置 i18n.languages(JSON 数组)。 |
| web.crmLead.* 对应模块? | 脚本识别为 crm 模块(上传分组/本地 crm.json);库内为扁平 key。 |
| load 返回结构? | 扁平 key-value,前端会转嵌套供 vue-i18n 使用。 |
| 固定标签存库吗? | 是。同表 system_i18n_translation,scene_type:1 前端UI / 2 后端消息 / 3 字典 / 4 业务数据 / 5 邮件模板。 |
| sync:i18n-message / sync:i18n-dict 报错? | 脚本免 token、免 JWT,依赖 permit-all 配置;需重启后端使 sync-dict-to-i18n、sync-error-codes-to-i18n 等接口的 permit-all 生效;确保后端已启动且 API_BASE_URL 正确。 |
附录:脚本说明
Node 脚本
在 yunfun-ui-admin-vben 目录下执行。
baseURL:默认 http://localhost:48080,可在 .env 或 API_BASE_URL 环境变量中配置;接口路径固定为 {baseURL}/admin-api/system/i18n-pack/...。
| 命令 | 用途 |
|---|---|
pnpm run extract:i18n | 扫描前端 $t(),上传并自动翻译(scene_type=1) |
pnpm run sync:i18n-message | 后端消息:从 Java 源码自动提取 ErrorCode 并同步到翻译表,再自动翻译(scene_type=2) |
pnpm run sync:i18n-dict | 字典数据:先同步 system_dict_data→翻译表,再自动翻译(scene_type=3) |
pnpm run sync:i18n-message-dict | 后端消息 + 字典一并执行 |
环境变量(免 token,依赖 permit-all 配置):
| 变量 | 说明 | 默认值 |
|---|---|---|
API_BASE_URL | 后端根地址 | 默认 localhost:48080,可配 .env 或 env |
API_TENANT_ID | 租户 ID | 1 |
SCENE_TYPES | 场景类型,逗号分隔 | 2,3(2=后端消息,3=字典) |
多租户时注意脚本内 tenant_id(默认 1),按实际租户修改或保持与登录租户一致。
7. 语言偏好与获取当前语言方式
前端语言标识:
- 所有请求统一通过
Accept-Language头把当前语言传给后端:在请求拦截器中写入config.headers['Accept-Language'] = preferences.app.locale,值与前端切换的语言代码一致(如zh-CN、en-US)。 - 语言切换入口为右上角的 LanguageToggle 组件,使用
@vben/locales的loadLocaleMessages加载语言包,并更新preferences.app.locale;同时将当前语言写入全局变量window.__APP_LOCALE__,供I18nInput等通用多语组件识别主语言与译文顺序。
用户语言字段(system_users.language):
- 用户表
system_users新增language字段(如zh-CN、en-US、ja-JP),作为用户语言偏好存储。 - 语言切换时,前端通过
updateUserProfile({ language: targetLocale })异步更新该字段;登录后会把language写入登录上下文,用于邮件模板等场景按用户偏好发送对应语言的内容。
后端获取当前语言的方式:
- 通用消息 / 错误提示:通过
I18nMessageUtil.getCurrentLanguage()从当前请求的Accept-Language头解析出语言代码(优先级:Accept-Language> 默认zh-CN),并在GlobalExceptionHandler中按此语言翻译错误码消息。 - 字典多语:
DictDataController的 simple-list 接口同样依赖Accept-Language,对非zh-CN请求自动返回对应语言的字典标签,前端切换语言后通过刷新字典缓存重新拉取。 - 业务数据多语:
TranslationAspect内部按当前用户语言(优先:登录上下文中的locale/language,否则回退到中文)决定是否替换业务字段的返回值;一般情况下,业务代码无需显式获取语言,只要正常走统一的切面与翻译服务即可。 - 手动获取翻译文本:在需要后端手工调用翻译服务的场景,可使用
I18nMessageUtil.getMessage(key)(自动按当前请求语言返回译文)或getMessage(key, lang)(显式指定语言),也可以调用translationService.getTranslationText(translationKey, targetLang)直接按 key + 语言取译文。
