国际化(i18n)最佳实践
概述
国际化(Internationalization, i18n)是构建面向全球用户应用的关键特性。RuoYi-Plus-UniApp 项目实现了完整的三层国际化解决方案,涵盖后端 Java API、前端 Vue 管理端和 UniApp 移动端,支持中文简体和英文双语切换,并提供灵活的扩展机制支持更多语言。
核心价值:
- 多语言支持 - 覆盖后端API响应、前端界面和移动端应用的完整国际化
- 自动检测 - 后端根据请求头自动识别语言,移动端自动检测系统语言
- 动态切换 - 前端和移动端支持用户实时切换语言,无需刷新页面
- 统一管理 - 集中管理消息资源,确保术语翻译的一致性
- 验证国际化 - 表单验证错误消息支持多语言显示
- 类型安全 - TypeScript 类型检查确保翻译键的正确性
支持语言:
| 语言 | 代码 | 后端 | 前端 | 移动端 |
|---|---|---|---|---|
| 中文简体 | zh_CN | ✅ | ✅ | ✅ |
| 英文 | en_US | ✅ | ✅ | ✅ |
技术架构:
┌─────────────────────────────────────────────────────────────────┐
│ 国际化架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 后端(Spring Boot): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ I18nLocaleResolver │ │
│ │ ↓ 解析 content-language 请求头 │ │
│ │ LocaleContextHolder │ │
│ │ ↓ 存储当前线程 Locale │ │
│ │ MessageSource │ │
│ │ ↓ 从 messages_zh_CN.properties 获取消息 │ │
│ │ MessageUtils.message("user.not.exists") │ │
│ │ → "用户不存在" 或 "User does not exist" │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 前端(Vue 3 + i18n): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ createI18n({ │ │
│ │ locale: 'zh_CN', │ │
│ │ messages: { zh_CN, en_US } │ │
│ │ }) │ │
│ │ ↓ │ │
│ │ $t('login.title') │ │
│ │ → "登录" 或 "Login" │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 移动端(UniApp + i18n): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ languageState │ │
│ │ ↓ 从缓存读取 or 系统语言 │ │
│ │ setLanguage('en_US') │ │
│ │ ↓ 更新 WotUI Locale │ │
│ │ $t('app.title') │ │
│ │ → "若依移动端" 或 "RuoYi Mobile" │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘后端国际化实现
1. 核心配置
后端国际化基于 Spring Boot 的 MessageSource 机制,通过自定义 LocaleResolver 实现语言检测和切换。
LocaleResolver 配置
系统使用自定义的 I18nLocaleResolver 解析客户端语言偏好:
@Configuration
public class I18nConfiguration {
@Bean
public LocaleResolver localeResolver() {
return new I18nLocaleResolver();
}
}语言解析逻辑
I18nLocaleResolver 从 HTTP 请求头 content-language 中解析语言代码:
public class I18nLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getHeader("content-language");
if (StringUtils.hasText(language)) {
// 支持格式: zh_CN, en_US
String[] parts = language.split("_");
if (parts.length == 2) {
return new Locale(parts[0], parts[1]);
} else if (parts.length == 1) {
return new Locale(parts[0]);
}
}
// 返回系统默认语言
return Locale.getDefault();
}
@Override
public void setLocale(HttpServletRequest request,
HttpServletResponse response,
Locale locale) {
// 后端基于请求头,不修改
}
}支持的请求头格式:
content-language: zh_CN # 中文简体
content-language: en_US # 英文
content-language: zh # 仅语言代码2. 消息资源配置
application.yml 配置
spring:
messages:
# 消息资源文件基础名称
basename: i18n/messages
# 编码格式
encoding: UTF-8
# 缓存时间(-1表示永久缓存)
cache-duration: -1资源文件结构
ruoyi-admin/
└── src/main/resources/
└── i18n/
├── messages.properties # 默认/回退语言(通常为英文)
├── messages_zh_CN.properties # 中文简体
└── messages_en_US.properties # 英文messages_zh_CN.properties 示例
# 通用
common.required=必填项
common.id.invalid=ID无效
common.length.invalid=长度不在范围内
common.success=操作成功
common.fail=操作失败
# 操作类型
operation.add.success=新增成功
operation.add.fail=新增失败
operation.update.success=修改成功
operation.update.fail=修改失败
operation.delete.success=删除成功
operation.delete.fail=删除失败
operation.export.success=导出成功
operation.export.fail=导出失败
operation.import.success=导入成功
operation.import.fail=导入失败
# 用户相关
user.not.exists=用户不存在
user.password.not.match=用户名或密码错误
user.blocked=用户已被停用,请联系管理员
user.account.locked=账号已被锁定,请{0}分钟后重试
user.account.not.activated=账号未激活,请先激活账号
user.username.exists=用户名已存在
user.email.exists=邮箱已被使用
user.phone.exists=手机号已被使用
# 登录相关
login.success=登录成功
login.fail=登录失败
login.password.error=密码错误
login.account.not.exists=账号不存在
logout.success=退出成功
register.success=注册成功
register.fail=注册失败
# 验证码
captcha.invalid=验证码错误
captcha.expired=验证码已过期,请刷新后重试
sms.code.invalid=短信验证码错误
sms.code.expired=短信验证码已过期
email.code.invalid=邮箱验证码错误
email.code.expired=邮箱验证码已过期
# 社交登录
social.login.success=第三方登录成功
social.login.fail=第三方登录失败
social.bind.success=绑定成功
social.bind.fail=绑定失败
social.unbind.success=解绑成功
social.already.bound=该账号已被其他用户绑定
# 文件上传
file.upload.success=文件上传成功
file.upload.fail=文件上传失败
file.size.exceed=文件大小超过限制
file.type.not.allowed=不支持的文件类型
# 权限
permission.denied=权限不足
permission.role.denied=角色权限不足
permission.data.denied=数据权限不足
# 限流
rate.limit.exceeded=访问过于频繁,请稍后再试
# 租户
tenant.not.exists=租户不存在
tenant.expired=租户已过期,请联系管理员
tenant.disabled=租户已被停用,请联系管理员
tenant.package.expired=租户套餐已过期messages_en_US.properties 示例
# Common
common.required=Required
common.id.invalid=Invalid ID
common.length.invalid=Length out of range
common.success=Operation successful
common.fail=Operation failed
# Operations
operation.add.success=Added successfully
operation.add.fail=Failed to add
operation.update.success=Updated successfully
operation.update.fail=Failed to update
operation.delete.success=Deleted successfully
operation.delete.fail=Failed to delete
operation.export.success=Exported successfully
operation.export.fail=Failed to export
operation.import.success=Imported successfully
operation.import.fail=Failed to import
# User
user.not.exists=User does not exist
user.password.not.match=Incorrect username or password
user.blocked=User has been disabled, please contact administrator
user.account.locked=Account locked, please try again in {0} minutes
user.account.not.activated=Account not activated
user.username.exists=Username already exists
user.email.exists=Email already in use
user.phone.exists=Phone number already in use
# Login
login.success=Login successful
login.fail=Login failed
login.password.error=Incorrect password
login.account.not.exists=Account does not exist
logout.success=Logout successful
register.success=Registration successful
register.fail=Registration failed
# Captcha
captcha.invalid=Invalid captcha
captcha.expired=Captcha expired, please refresh
sms.code.invalid=Invalid SMS code
sms.code.expired=SMS code expired
email.code.invalid=Invalid email code
email.code.expired=Email code expired
# Social Login
social.login.success=Social login successful
social.login.fail=Social login failed
social.bind.success=Binding successful
social.bind.fail=Binding failed
social.unbind.success=Unbinding successful
social.already.bound=Account already bound to another user
# File Upload
file.upload.success=File uploaded successfully
file.upload.fail=File upload failed
file.size.exceed=File size exceeds limit
file.type.not.allowed=File type not allowed
# Permission
permission.denied=Permission denied
permission.role.denied=Role permission denied
permission.data.denied=Data permission denied
# Rate Limit
rate.limit.exceeded=Too many requests, please try again later
# Tenant
tenant.not.exists=Tenant does not exist
tenant.expired=Tenant expired, please contact administrator
tenant.disabled=Tenant disabled, please contact administrator
tenant.package.expired=Tenant package expired3. 使用方式
MessageUtils 工具类
系统提供 MessageUtils 工具类简化国际化消息获取:
public class MessageUtils {
private static MessageSource messageSource;
/**
* 根据消息键和参数获取消息
*
* @param code 消息键
* @param args 参数
* @return 国际化消息
*/
public static String message(String code, Object... args) {
return messageSource.getMessage(
code,
args,
LocaleContextHolder.getLocale()
);
}
}在代码中使用
基础用法:
// 简单消息
String message = MessageUtils.message("user.not.exists");
// zh_CN: "用户不存在"
// en_US: "User does not exist"
// 带参数的消息
String message = MessageUtils.message("user.account.locked", 5);
// zh_CN: "账号已被锁定,请5分钟后重试"
// en_US: "Account locked, please try again in 5 minutes"在 Controller 中使用:
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping
public R<Void> add(@Validated @RequestBody UserBo bo) {
// 检查用户名是否存在
if (userService.checkUserNameUnique(bo.getUserName())) {
return R.fail(MessageUtils.message("user.username.exists"));
}
boolean success = userService.insertUser(bo);
return success
? R.ok(MessageUtils.message("operation.add.success"))
: R.fail(MessageUtils.message("operation.add.fail"));
}
}在 Service 中使用:
@Service
public class UserServiceImpl implements UserService {
@Override
public void updateUser(UserBo bo) {
User user = userDao.selectById(bo.getUserId());
if (user == null) {
throw new ServiceException(
MessageUtils.message("user.not.exists")
);
}
// 更新逻辑...
}
}在异常处理中使用:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public R<Void> handleServiceException(ServiceException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
log.error("系统异常", e);
return R.fail(MessageUtils.message("common.fail"));
}
}4. 验证消息国际化
系统集成了 Hibernate Validator 国际化支持,通过自定义 I18nMessageInterpolator 实现验证消息的多语言。
验证器配置
@Configuration
public class CoreAutoConfiguration {
@Bean
public Validator validator(MessageSource messageSource) {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setMessageInterpolator(
new I18nMessageInterceptor(messageSource)
);
validator.getValidationPropertyMap().put(
HibernateValidatorConfiguration.FAIL_FAST, "true"
);
return validator;
}
}在实体类中使用
public class UserBo {
@NotBlank(message = "common.required")
private String userName;
@NotBlank(message = "common.required")
@Length(min = 5, max = 20, message = "common.length.invalid")
private String password;
@Email(message = "email.invalid")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "phone.invalid")
private String phone;
}验证失败时返回:
// zh_CN
{
"code": 500,
"msg": "必填项"
}
// en_US
{
"code": 500,
"msg": "Required"
}自定义验证注解国际化
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface ValidUsername {
String message() default "user.username.invalid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 在 messages.properties 中定义消息
// messages_zh_CN.properties
user.username.invalid=用户名格式不正确,只能包含字母、数字和下划线
// messages_en_US.properties
user.username.invalid=Invalid username format, only letters, numbers and underscores allowed前端国际化实现
前端使用 Vue i18n 插件实现完整的国际化支持,涵盖页面文本、Element Plus 组件、提示消息等所有用户界面元素。
1. i18n 配置
核心配置文件
// src/locales/i18n.ts
import { createI18n } from 'vue-i18n'
import zhCN from './zh_CN'
import enUS from './en_US'
import el_zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import el_en from 'element-plus/dist/locale/en.mjs'
export type LanguageType = 'zh_CN' | 'en_US'
const i18n = createI18n({
// 全局注入 $t, $d, $n 等函数
globalInjection: true,
// 允许 Composition API
allowComposition: true,
// 使用 Vue 3 Composition API 模式
legacy: false,
// 默认语言
locale: 'zh_CN',
// 回退语言
fallbackLocale: 'zh_CN',
// 语言消息
messages: {
zh_CN: zhCN,
en_US: enUS
}
})
export default i18n
// Element Plus 语言包
export const elementLocales = {
zh_CN: el_zhCn,
en_US: el_en
}2. 语言资源文件
中文语言包
// src/locales/zh_CN.ts
export default {
// 按钮
button: {
query: '查询',
reset: '重置',
add: '新增',
edit: '修改',
delete: '删除',
export: '导出',
import: '导入',
save: '保存',
cancel: '取消',
confirm: '确定',
submit: '提交',
back: '返回',
refresh: '刷新',
search: '搜索',
upload: '上传',
download: '下载'
},
// 对话框
dialog: {
title: {
add: '添加{0}',
edit: '修改{0}',
detail: '{0}详情',
import: '导入{0}'
},
confirm: '确认操作',
tips: '提示',
deleteConfirm: '是否确认删除{0}?',
clearConfirm: '是否确认清空所有数据?'
},
// 消息提示
message: {
success: '操作成功',
error: '操作失败',
saveSuccess: '保存成功',
saveError: '保存失败',
updateSuccess: '更新成功',
updateError: '更新失败',
deleteSuccess: '删除成功',
deleteError: '删除失败',
importSuccess: '导入成功',
importError: '导入失败',
exportSuccess: '导出成功',
exportError: '导出失败',
selectOne: '请至少选择一条数据',
selectOneOnly: '只能选择一条数据'
},
// 占位符
placeholder: {
input: '请输入{0}',
select: '请选择{0}',
dateRange: '选择日期范围',
search: '请输入搜索内容'
},
// 提示文本
tooltip: {
edit: '编辑',
delete: '删除',
detail: '详情',
refresh: '刷新',
export: '导出',
import: '导入'
},
// 路由名称
route: {
home: '首页',
system: '系统管理',
user: '用户管理',
role: '角色管理',
menu: '菜单管理',
dept: '部门管理',
post: '岗位管理',
dict: '字典管理',
config: '参数设置',
notice: '通知公告',
log: '日志管理',
loginLog: '登录日志',
operateLog: '操作日志',
monitor: '系统监控',
online: '在线用户',
job: '定时任务',
druid: '数据监控',
server: '服务监控',
cache: '缓存监控'
},
// 登录页面
login: {
title: '若依后台管理系统',
username: '用户名',
password: '密码',
captcha: '验证码',
rememberMe: '记住密码',
login: '登录',
loginSuccess: '登录成功',
usernameRequired: '请输入用户名',
passwordRequired: '请输入密码',
captchaRequired: '请输入验证码'
},
// 导航栏
navbar: {
profile: '个人中心',
logout: '退出登录',
language: '语言',
chinese: '中文',
english: 'English'
},
// 标签页
tagsView: {
refresh: '刷新页面',
close: '关闭当前',
closeOthers: '关闭其他',
closeAll: '关闭所有'
}
}英文语言包
// src/locales/en_US.ts
export default {
// Buttons
button: {
query: 'Query',
reset: 'Reset',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
export: 'Export',
import: 'Import',
save: 'Save',
cancel: 'Cancel',
confirm: 'Confirm',
submit: 'Submit',
back: 'Back',
refresh: 'Refresh',
search: 'Search',
upload: 'Upload',
download: 'Download'
},
// Dialogs
dialog: {
title: {
add: 'Add {0}',
edit: 'Edit {0}',
detail: '{0} Detail',
import: 'Import {0}'
},
confirm: 'Confirm Operation',
tips: 'Tips',
deleteConfirm: 'Confirm to delete {0}?',
clearConfirm: 'Confirm to clear all data?'
},
// Messages
message: {
success: 'Operation successful',
error: 'Operation failed',
saveSuccess: 'Save successful',
saveError: 'Save failed',
updateSuccess: 'Update successful',
updateError: 'Update failed',
deleteSuccess: 'Delete successful',
deleteError: 'Delete failed',
importSuccess: 'Import successful',
importError: 'Import failed',
exportSuccess: 'Export successful',
exportError: 'Export failed',
selectOne: 'Please select at least one item',
selectOneOnly: 'Can only select one item'
},
// Placeholders
placeholder: {
input: 'Please enter {0}',
select: 'Please select {0}',
dateRange: 'Select date range',
search: 'Please enter search content'
},
// Tooltips
tooltip: {
edit: 'Edit',
delete: 'Delete',
detail: 'Detail',
refresh: 'Refresh',
export: 'Export',
import: 'Import'
},
// Routes
route: {
home: 'Home',
system: 'System',
user: 'User',
role: 'Role',
menu: 'Menu',
dept: 'Department',
post: 'Post',
dict: 'Dictionary',
config: 'Config',
notice: 'Notice',
log: 'Log',
loginLog: 'Login Log',
operateLog: 'Operate Log',
monitor: 'Monitor',
online: 'Online Users',
job: 'Scheduled Jobs',
druid: 'Druid Monitor',
server: 'Server Monitor',
cache: 'Cache Monitor'
},
// Login Page
login: {
title: 'RuoYi Management System',
username: 'Username',
password: 'Password',
captcha: 'Captcha',
rememberMe: 'Remember Me',
login: 'Login',
loginSuccess: 'Login successful',
usernameRequired: 'Please enter username',
passwordRequired: 'Please enter password',
captchaRequired: 'Please enter captcha'
},
// Navbar
navbar: {
profile: 'Profile',
logout: 'Logout',
language: 'Language',
chinese: '中文',
english: 'English'
},
// Tags View
tagsView: {
refresh: 'Refresh',
close: 'Close Current',
closeOthers: 'Close Others',
closeAll: 'Close All'
}
}3. 在组件中使用
模板中使用
<template>
<div class="user-page">
<!-- 基础翻译 -->
<h1>{{ $t('route.user') }}</h1>
<!-- 带参数的翻译 -->
<el-dialog :title="$t('dialog.title.add', ['用户'])">
<el-form>
<!-- 占位符翻译 -->
<el-input
v-model="form.username"
:placeholder="$t('placeholder.input', ['用户名'])"
/>
<!-- 按钮文本 -->
<el-button type="primary">{{ $t('button.save') }}</el-button>
<el-button>{{ $t('button.cancel') }}</el-button>
</el-form>
</el-dialog>
<!-- 消息提示 -->
<el-button @click="handleSave">{{ $t('button.save') }}</el-button>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const handleSave = () => {
// 在 JS 中使用 t 函数
ElMessage.success(t('message.saveSuccess'))
}
</script>Composition API 用法
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
const { t, locale } = useI18n()
// 计算属性
const title = computed(() => t('route.user'))
// 动态切换语言
const changeLanguage = (lang: string) => {
locale.value = lang
}
// 在函数中使用
const showMessage = () => {
const message = t('message.success')
console.log(message)
}
</script>4. 语言切换
语言切换组件
<template>
<el-dropdown @command="handleCommand">
<span class="language-switch">
<i class="el-icon-s-flag"></i>
{{ currentLanguageName }}
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh_CN">中文</el-dropdown-item>
<el-dropdown-item command="en_US">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLayoutStore } from '@/stores/layout'
import { elementLocales } from '@/locales/i18n'
const { locale } = useI18n()
const layoutStore = useLayoutStore()
// 当前语言名称
const currentLanguageName = computed(() => {
return locale.value === 'zh_CN' ? '中文' : 'English'
})
// 切换语言
const handleCommand = (lang: string) => {
// 更新 i18n 语言
locale.value = lang
// 更新 Element Plus 语言
layoutStore.setElementLocale(elementLocales[lang])
// 保存到本地存储
localStorage.setItem('language', lang)
}
</script>应用启动时加载语言
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './locales/i18n'
const app = createApp(App)
// 从本地存储读取语言设置
const savedLanguage = localStorage.getItem('language')
if (savedLanguage) {
i18n.global.locale.value = savedLanguage
}
app.use(i18n)
app.mount('#app')移动端国际化实现
移动端基于 UniApp 平台实现国际化,支持自动检测系统语言和手动切换,并与 WotUI 组件库深度集成。
1. i18n 配置
核心配置文件
// locales/i18n.ts
import { reactive, computed } from 'vue'
import { cache } from '@/utils/cache'
import { Locale as WotLocale } from 'wot-design-uni'
import zhCN from './zh-CN'
import enUS from './en-US'
// 语言类型
export type LanguageType = 'zh_CN' | 'en_US'
// 语言缓存键
const LANGUAGE_CACHE_KEY = 'app_language'
// 语言配置
const messages = {
zh_CN: zhCN,
en_US: enUS
}
// 语言状态
export const languageState = reactive({
// 当前语言
current: getLanguageFromCache() || getSystemLanguage(),
// 当前语言名称
get currentName() {
return this.current === 'zh_CN' ? '中文' : 'English'
},
// 是否中文
get isChinese() {
return this.current === 'zh_CN'
},
// 是否英文
get isEnglish() {
return this.current === 'en_US'
},
// 语言选项
get options() {
return [
{ label: '中文', value: 'zh_CN' },
{ label: 'English', value: 'en_US' }
]
}
})
/**
* 从缓存读取语言
*/
function getLanguageFromCache(): LanguageType | null {
return cache.get(LANGUAGE_CACHE_KEY)
}
/**
* 保存语言到缓存
*/
function setLanguageToCache(language: LanguageType) {
cache.set(LANGUAGE_CACHE_KEY, language)
}
/**
* 获取系统语言
*/
function getSystemLanguage(): LanguageType {
const systemInfo = uni.getSystemInfoSync()
const language = systemInfo.language
// 中文环境返回 zh_CN
if (language.startsWith('zh')) {
return 'zh_CN'
}
// 默认返回英文
return 'en_US'
}
/**
* 获取当前语言
*/
export function getLanguage(): LanguageType {
return languageState.current
}
/**
* 获取当前语言的消息
*/
export function getCurrentMessages() {
return messages[languageState.current]
}
/**
* 翻译函数
*/
export function t(key: string, params?: Record<string, any>): string {
const keys = key.split('.')
let value: any = getCurrentMessages()
// 按路径查找
for (const k of keys) {
value = value?.[k]
if (value === undefined) {
return key
}
}
// 参数替换
if (params && typeof value === 'string') {
Object.keys(params).forEach(key => {
value = value.replace(new RegExp(`\\{${key}\\}`, 'g'), params[key])
})
}
return value
}
/**
* 设置语言
*/
export function setLanguage(language: LanguageType) {
languageState.current = language
setLanguageToCache(language)
// 更新 WotUI 语言
updateWotUILocale(language)
}
/**
* 更新 WotUI 组件库语言
*/
function updateWotUILocale(language: LanguageType) {
const wotLocale = language === 'zh_CN' ? 'zh-CN' : 'en-US'
WotLocale.use(wotLocale, messages[language])
}
/**
* 初始化语言
*/
export function initLanguage() {
const language = getLanguageFromCache() || getSystemLanguage()
setLanguage(language)
}2. 语言资源文件
中文语言包
// locales/zh-CN.ts
export default {
// 应用通用
app: {
title: '若依移动端',
loading: '加载中...',
noMore: '没有更多了',
loadFailed: '加载失败',
retry: '重试'
},
// 按钮
button: {
confirm: '确定',
cancel: '取消',
save: '保存',
submit: '提交',
reset: '重置',
search: '搜索',
add: '新增',
edit: '编辑',
delete: '删除',
refresh: '刷新',
back: '返回'
},
// 消息
message: {
success: '操作成功',
fail: '操作失败',
saveSuccess: '保存成功',
deleteSuccess: '删除成功',
selectOne: '请至少选择一项'
},
// 占位符
placeholder: {
input: '请输入',
select: '请选择',
search: '请输入搜索关键字'
},
// 登录页
login: {
title: '若依移动端',
username: '用户名',
password: '密码',
login: '登录',
register: '注册',
forgotPassword: '忘记密码',
usernameRequired: '请输入用户名',
passwordRequired: '请输入密码'
},
// 我的页面
my: {
title: '我的',
profile: '个人信息',
settings: '设置',
language: '语言',
theme: '主题',
about: '关于',
logout: '退出登录',
logoutConfirm: '确认退出登录?'
},
// 设置页面
settings: {
title: '设置',
language: '语言选择',
theme: '主题模式',
clearCache: '清除缓存',
clearCacheConfirm: '确认清除所有缓存?',
clearCacheSuccess: '缓存清除成功',
version: '当前版本'
}
}英文语言包
// locales/en-US.ts
export default {
// App Common
app: {
title: 'RuoYi Mobile',
loading: 'Loading...',
noMore: 'No more data',
loadFailed: 'Load failed',
retry: 'Retry'
},
// Buttons
button: {
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
submit: 'Submit',
reset: 'Reset',
search: 'Search',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
refresh: 'Refresh',
back: 'Back'
},
// Messages
message: {
success: 'Operation successful',
fail: 'Operation failed',
saveSuccess: 'Save successful',
deleteSuccess: 'Delete successful',
selectOne: 'Please select at least one item'
},
// Placeholders
placeholder: {
input: 'Please enter',
select: 'Please select',
search: 'Please enter search keyword'
},
// Login Page
login: {
title: 'RuoYi Mobile',
username: 'Username',
password: 'Password',
login: 'Login',
register: 'Register',
forgotPassword: 'Forgot Password',
usernameRequired: 'Please enter username',
passwordRequired: 'Please enter password'
},
// My Page
my: {
title: 'My',
profile: 'Profile',
settings: 'Settings',
language: 'Language',
theme: 'Theme',
about: 'About',
logout: 'Logout',
logoutConfirm: 'Confirm to logout?'
},
// Settings Page
settings: {
title: 'Settings',
language: 'Language Selection',
theme: 'Theme Mode',
clearCache: 'Clear Cache',
clearCacheConfirm: 'Confirm to clear all cache?',
clearCacheSuccess: 'Cache cleared successfully',
version: 'Version'
}
}3. 在页面中使用
页面模板使用
<template>
<view class="container">
<!-- 基础翻译 -->
<view class="title">{{ t('app.title') }}</view>
<!-- 带参数的翻译 -->
<wd-button @click="handleSave">
{{ t('button.save') }}
</wd-button>
<!-- 在属性中使用 -->
<wd-input
v-model="username"
:placeholder="t('placeholder.input')"
/>
<!-- 列表渲染 -->
<view v-for="item in languageState.options" :key="item.value">
{{ item.label }}
</view>
</view>
</template>
<script lang="ts" setup>
import { t, languageState } from '@/locales/i18n'
import { ref } from 'vue'
const username = ref('')
const handleSave = () => {
uni.showToast({
title: t('message.saveSuccess'),
icon: 'success'
})
}
</script>语言切换页面
<template>
<view class="settings-page">
<wd-cell-group>
<wd-cell
:title="t('settings.language')"
:value="languageState.currentName"
is-link
@click="showLanguagePicker = true"
/>
</wd-cell-group>
<wd-action-sheet
v-model="showLanguagePicker"
:actions="languageActions"
@select="handleLanguageSelect"
/>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { t, setLanguage, languageState, type LanguageType } from '@/locales/i18n'
const showLanguagePicker = ref(false)
const languageActions = computed(() => [
{ name: '中文', value: 'zh_CN' },
{ name: 'English', value: 'en_US' }
])
const handleLanguageSelect = (action: any) => {
setLanguage(action.value as LanguageType)
showLanguagePicker.value = false
uni.showToast({
title: t('message.success'),
icon: 'success'
})
}
</script>App 启动时初始化
// main.ts
import { createSSRApp } from 'vue'
import { initLanguage } from './locales/i18n'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
// 初始化语言
initLanguage()
return {
app
}
}最佳实践
1. 消息键命名规范
使用分层命名结构:
# 好的实践 ✅
user.not.exists=用户不存在
user.password.not.match=用户名或密码错误
operation.add.success=新增成功
operation.delete.confirm=确认删除?
# 不好的实践 ❌
userNotExists=用户不存在
error_01=用户名或密码错误
add_ok=新增成功
del_confirm=确认删除?命名层级建议:
模块.功能.状态
例如:
- user.login.success (用户.登录.成功)
- order.payment.fail (订单.支付.失败)
- file.upload.size.exceed (文件.上传.大小.超出)2. 参数化消息
使用占位符传递动态参数:
// 后端
MessageUtils.message("user.account.locked", 5)
// "账号已被锁定,请5分钟后重试"
// "Account locked, please try again in 5 minutes"
// 前端
t('dialog.title.add', ['用户'])
// "添加用户"
// "Add User"3. 保持翻译一致性
建立术语表:
| 中文 | 英文 | 使用场景 |
|---|---|---|
| 新增 | Add | 按钮、操作 |
| 修改 | Edit | 按钮、操作 |
| 删除 | Delete | 按钮、操作 |
| 查询 | Query | 按钮 |
| 搜索 | Search | 输入框 |
| 保存 | Save | 表单提交 |
| 提交 | Submit | 表单提交 |
| 取消 | Cancel | 对话框 |
| 确定 | Confirm | 对话框 |
4. 验证消息统一管理
集中定义验证消息:
# messages_zh_CN.properties
validation.required={0}不能为空
validation.length={0}长度必须在{1}到{2}之间
validation.email=邮箱格式不正确
validation.phone=手机号格式不正确
validation.pattern={0}格式不正确在实体类中引用:
public class UserBo {
@NotBlank(message = "validation.required")
private String userName;
@Length(min = 5, max = 20, message = "validation.length")
private String password;
}5. 前后端语言同步
保持前后端请求头一致:
// 前端请求拦截器
import axios from 'axios'
import { useI18n } from 'vue-i18n'
axios.interceptors.request.use(config => {
const { locale } = useI18n()
config.headers['content-language'] = locale.value
return config
})移动端同步语言:
// 移动端请求拦截器
import { getLanguage } from '@/locales/i18n'
uni.addInterceptor('request', {
invoke(args) {
args.header = args.header || {}
args.header['content-language'] = getLanguage()
}
})6. 性能优化
懒加载语言包:
// 按需加载语言资源
const loadLanguageAsync = (lang: string) => {
return import(`./locales/${lang}.ts`).then(module => {
i18n.global.setLocaleMessage(lang, module.default)
return module.default
})
}
// 切换语言时加载
const setLanguage = async (lang: string) => {
if (!i18n.global.availableLocales.includes(lang)) {
await loadLanguageAsync(lang)
}
i18n.global.locale.value = lang
}缓存翻译结果:
const translationCache = new Map<string, string>()
const t = (key: string): string => {
const cacheKey = `${locale.value}:${key}`
if (translationCache.has(cacheKey)) {
return translationCache.get(cacheKey)!
}
const value = i18n.global.t(key)
translationCache.set(cacheKey, value)
return value
}7. 多语言内容管理
数据库表设计:
-- 多语言内容表
CREATE TABLE sys_i18n_content (
id BIGINT PRIMARY KEY,
module VARCHAR(50), -- 模块(user/order/product)
content_key VARCHAR(100), -- 内容键(name/description)
language VARCHAR(10), -- 语言(zh_CN/en_US)
content TEXT, -- 内容
create_time DATETIME
);
-- 示例数据
INSERT INTO sys_i18n_content VALUES
(1, 'product', 'name', 'zh_CN', '苹果手机', NOW()),
(2, 'product', 'name', 'en_US', 'Apple Phone', NOW()),
(3, 'product', 'description', 'zh_CN', '高性能智能手机', NOW()),
(4, 'product', 'description', 'en_US', 'High-performance smartphone', NOW());动态加载多语言内容:
@Service
public class I18nContentService {
@Autowired
private I18nContentDao contentDao;
public String getContent(String module, String key) {
Locale locale = LocaleContextHolder.getLocale();
String language = locale.toString();
return contentDao.selectContent(module, key, language);
}
}常见问题
1. 后端消息不生效
问题: 后端返回的消息始终是英文
原因:
- 客户端未发送
content-language请求头 - LocaleResolver 配置错误
- 消息键不存在
解决方案:
// 前端请求拦截器添加语言头
axios.interceptors.request.use(config => {
config.headers['content-language'] = 'zh_CN'
return config
})
// 检查后端配置
@Bean
public LocaleResolver localeResolver() {
return new I18nLocaleResolver()
}
// 检查消息文件
# messages_zh_CN.properties
user.not.exists=用户不存在2. 验证消息未国际化
问题: 验证失败返回的是消息键而不是翻译文本
原因:
- 未配置
I18nMessageInterceptor - 消息键格式错误
解决方案:
// 配置自定义 MessageInterpolator
@Bean
public Validator validator(MessageSource messageSource) {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setMessageInterpolator(
new I18nMessageInterceptor(messageSource)
);
return validator;
}
// 实体类中使用正确的消息键
@NotBlank(message = "common.required") // ✅ 正确
@NotBlank(message = "{common.required}") // ❌ 错误(不要加大括号)
private String userName;3. 前端切换语言后部分组件未更新
问题: Element Plus 组件(如日期选择器)语言未切换
原因: Element Plus 有独立的 locale 配置
解决方案:
<template>
<el-config-provider :locale="currentLocale">
<router-view />
</el-config-provider>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { elementLocales } from '@/locales/i18n'
const { locale } = useI18n()
const currentLocale = computed(() => {
return elementLocales[locale.value]
})
</script>4. 移动端系统语言检测不准确
问题: 移动端始终显示英文
原因:
- 未正确初始化语言
- WotUI 语言未同步更新
解决方案:
// App.vue onLaunch 生命周期中初始化
import { initLanguage } from '@/locales/i18n'
onLaunch(() => {
initLanguage()
})
// 确保 WotUI 语言同步
import { Locale as WotLocale } from 'wot-design-uni'
const setLanguage = (lang: LanguageType) => {
languageState.current = lang
// 更新 WotUI
const wotLocale = lang === 'zh_CN' ? 'zh-CN' : 'en-US'
WotLocale.use(wotLocale, messages[lang])
}5. 翻译文本中包含 HTML 标签
问题: 需要在翻译文本中包含链接或样式
解决方案:
<!-- 使用 v-html (注意XSS风险) -->
<div v-html="$t('agreement.text')"></div>
<!-- 或使用插槽 -->
<i18n-t keypath="agreement.text" tag="p">
<template #link>
<a href="/terms">{{ $t('agreement.terms') }}</a>
</template>
</i18n-t>
<!-- 语言文件 -->
export default {
agreement: {
text: '我已阅读并同意 {link}',
terms: '用户协议'
}
}总结
国际化是构建全球化应用的基础能力。通过本文档介绍的最佳实践:
- 完整方案 - 后端API、前端界面、移动端应用全面国际化
- 自动检测 - 智能识别用户语言偏好,提升用户体验
- 动态切换 - 支持运行时语言切换,无需重启应用
- 统一管理 - 集中管理消息资源,确保翻译一致性
- 类型安全 - TypeScript 类型检查避免翻译键错误
- 扩展灵活 - 易于添加新语言支持
建议在实际使用中:
- 建立完整的术语表和翻译规范
- 定期审查和更新翻译内容
- 使用专业翻译工具确保质量
- 收集用户反馈持续优化
- 考虑使用翻译管理平台(如 Crowdin, Transifex)
