跳到主要内容

i18n 多语言接入使用文档

· 阅读需 17 分钟
Quany
软件工程师

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 标签、步骤条标题、卡片/区块标题Pagetitlemeta.title、弹窗 titletabs[].labelsteps[].title
表格列头表格列的 labelcolumns[].label、schema 中列定义 label
表单标签表单项前的文字、详情页字段名schema[].labelformOptionsfield.label、详情 description 的 label
占位符输入框、选择框的 placeholderplaceholdercomponentProps.placeholder
校验提示表单校验失败时的提示、zod 等校验 messagerulesz.string().min(1, $t(...)){ message: $t(...) }
按钮文字主按钮、行内操作、工具栏按钮(新增/导出/导入等)按钮文本、#extra / #actionstoolbarConfig 中按钮文案
操作/链接文案复制、编辑、删除、查看等操作名操作列、下拉菜单项、链接文本
选项标签非字典的固定选项(如业务线、业态等静态选项)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、表格列 helphelpTextcomponentProps.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.editcommon.button.save。defaultValue 在 key 不存在时作为展示文案,存在时用语言包译文。

提取与同步

执行方式:在项目根目录下执行:

cd yunfun-ui-admin-vben
pnpm run extract:i18n

脚本 scripts/extract-i18n-keys.mjs:扫描 apps/web-antd/srcpackages/**/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.developmentVITE_BASE_URLhttp://localhost:48080
API_TOKEN登录态 Token
API_TENANT_ID租户 ID1
AUTO_TRANSLATE是否自动翻译true
WRITE_LOCAL_FILES是否写本地 zh-CN JSONfalse

模块识别(用于上传分组/本地文件名):web.crmLead.*→crm,web.systemUser.*→system 等;key 含点取第一段,否则 common。库内为扁平 key-value,load 接口按 locale 返回。


3. 后端消息翻译

异常由 GlobalExceptionHandler 拦截,用 translationKey = "ERROR_" + ex.getCode()I18nMessageUtil.getMessage,返回译文或原文。前端直接展示接口返回的 msg 即可。

管理系统管理国际化翻译开发配置翻译记录,筛选「后端消息」或前缀 ERROR_,可批量翻译或单条编辑。

错误码与消息翻译规范

  • 翻译键ERROR_ + 错误码数字(如 1003001000ERROR_1003001000),与 GlobalExceptionHandler 约定一致;错误码段遵循 ServiceErrorCodeRange
  • 存储system_i18n_translationscene_type=2。每条错误码至少一条 zh-CN 源语言(source_text/translated_textErrorCode.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_translationscene_type=3

字典自动化流程(推荐):

  • 前置字典管理 中已存在相关字典类型及数据。
  • 脚本自动化:执行 pnpm run sync:i18n-dict,脚本会:1)调用 sync-dict-to-i18nsystem_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 + dataIdsystem_i18n_business_data / system_i18n_translation 批量取译文,仅对存在翻译记录的字段做替换,未配置或无译文的字段保持原值。

前端行为与管理

  • 表单录入组件(I18nInput):在需要录入可多语的业务字段时,使用 @vben/common-ui 提供的 I18nInput(例如在线索池 / 线索个人 / 线索待办等编辑页中),通过 module / domain / 字段名等配置,与后端 @TranslateDomain / @Translate 一一对应,统一录入「原文 + 各目标语言」。
  • 业务接口调用:普通业务页面(如线索列表、来源管理)只需调用原有业务 API,即可拿到已按用户语言翻译后的字段,无需在前端额外 $t() 或手动调用翻译接口。
  • 业务数据翻译管理页:在 系统管理 → 国际化翻译 → 翻译工作台 → 业务数据翻译 中,可按模块/域/字段、fieldKeydataId 查询、编辑和批量维护业务数据翻译。
  • 前端手动调用(可选):如需在自定义页面中手工维护某条业务数据的翻译,可使用 #/api/system/i18n/business-data 中的接口,例如:getBusinessDataList(fieldKey, dataId) 查询现有翻译,saveBusinessDataTranslation({ fieldKey, dataId, sourceText, sourceLang, targetLangTranslations }) 保存修改。

列表页多语搜索(I18nSearchHelper)

列表页按多语字段搜索时,需同时匹配主表字段与 i18n 翻译表(source_texttranslated_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_idapplyI18nLikeCondition / 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"),对应 @TranslateDomainmodule
domain业务域(如 "leadTodo"),对应 @Translatedomain
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(快照表)
生效时机立即生效审批通过后同步生效
适用场景普通业务数据保存/查询审批流程中的暂存数据
同步方式自动保存/自动查询回填手动调用同步接口

后端接入方式

  1. VO/CMD/CO 类上加领域注解:与 @Translate 相同,使用 @TranslateDomain(module = "crm-customer") 声明所属模块。

  2. 字段上加快照注解:在需要做多语的字段上标 @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;
}
  1. 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));
}
}
  1. 同步机制:审批通过后,调用 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 为维度)。
  • 查询时:切面自动从快照表读取当前用户语言翻译并回填字段,同时填充 xxxML Map 供前端编辑回显。
  • 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}.titlemail.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-i18nsync-error-codes-to-i18n 等接口的 permit-all 生效;确保后端已启动且 API_BASE_URL 正确。

附录:脚本说明

Node 脚本

yunfun-ui-admin-vben 目录下执行。

baseURL:默认 http://localhost:48080,可在 .envAPI_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租户 ID1
SCENE_TYPES场景类型,逗号分隔2,3(2=后端消息,3=字典)

多租户时注意脚本内 tenant_id(默认 1),按实际租户修改或保持与登录租户一致。


7. 语言偏好与获取当前语言方式

前端语言标识

  • 所有请求统一通过 Accept-Language 头把当前语言传给后端:在请求拦截器中写入 config.headers['Accept-Language'] = preferences.app.locale,值与前端切换的语言代码一致(如 zh-CNen-US)。
  • 语言切换入口为右上角的 LanguageToggle 组件,使用 @vben/localesloadLocaleMessages 加载语言包,并更新 preferences.app.locale;同时将当前语言写入全局变量 window.__APP_LOCALE__,供 I18nInput 等通用多语组件识别主语言与译文顺序。

用户语言字段(system_users.language)

  • 用户表 system_users 新增 language 字段(如 zh-CNen-USja-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 + 语言取译文。