Skip to content

国际化(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 解析客户端语言偏好:

java
@Configuration
public class I18nConfiguration {

    @Bean
    public LocaleResolver localeResolver() {
        return new I18nLocaleResolver();
    }
}

语言解析逻辑

I18nLocaleResolver 从 HTTP 请求头 content-language 中解析语言代码:

java
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) {
        // 后端基于请求头,不修改
    }
}

支持的请求头格式:

http
content-language: zh_CN      # 中文简体
content-language: en_US      # 英文
content-language: zh          # 仅语言代码

2. 消息资源配置

application.yml 配置

yaml
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 示例

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 示例

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 expired

3. 使用方式

MessageUtils 工具类

系统提供 MessageUtils 工具类简化国际化消息获取:

java
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()
        );
    }
}

在代码中使用

基础用法:

java
// 简单消息
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 中使用:

java
@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 中使用:

java
@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")
            );
        }

        // 更新逻辑...
    }
}

在异常处理中使用:

java
@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 实现验证消息的多语言。

验证器配置

java
@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;
    }
}

在实体类中使用

java
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;
}

验证失败时返回:

json
// zh_CN
{
  "code": 500,
  "msg": "必填项"
}

// en_US
{
  "code": 500,
  "msg": "Required"
}

自定义验证注解国际化

java
@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 配置

核心配置文件

typescript
// 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. 语言资源文件

中文语言包

typescript
// 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: '关闭所有'
  }
}

英文语言包

typescript
// 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. 在组件中使用

模板中使用

vue
<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 用法

vue
<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. 语言切换

语言切换组件

vue
<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>

应用启动时加载语言

typescript
// 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 配置

核心配置文件

typescript
// 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. 语言资源文件

中文语言包

typescript
// 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: '当前版本'
  }
}

英文语言包

typescript
// 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. 在页面中使用

页面模板使用

vue
<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>

语言切换页面

vue
<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 启动时初始化

typescript
// 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. 消息键命名规范

使用分层命名结构:

properties
# 好的实践 ✅
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. 参数化消息

使用占位符传递动态参数:

java
// 后端
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. 验证消息统一管理

集中定义验证消息:

properties
# messages_zh_CN.properties
validation.required={0}不能为空
validation.length={0}长度必须在{1}到{2}之间
validation.email=邮箱格式不正确
validation.phone=手机号格式不正确
validation.pattern={0}格式不正确

在实体类中引用:

java
public class UserBo {
    @NotBlank(message = "validation.required")
    private String userName;

    @Length(min = 5, max = 20, message = "validation.length")
    private String password;
}

5. 前后端语言同步

保持前后端请求头一致:

typescript
// 前端请求拦截器
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
})

移动端同步语言:

typescript
// 移动端请求拦截器
import { getLanguage } from '@/locales/i18n'

uni.addInterceptor('request', {
  invoke(args) {
    args.header = args.header || {}
    args.header['content-language'] = getLanguage()
  }
})

6. 性能优化

懒加载语言包:

typescript
// 按需加载语言资源
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
}

缓存翻译结果:

typescript
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. 多语言内容管理

数据库表设计:

sql
-- 多语言内容表
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());

动态加载多语言内容:

java
@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 配置错误
  • 消息键不存在

解决方案:

typescript
// 前端请求拦截器添加语言头
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
  • 消息键格式错误

解决方案:

java
// 配置自定义 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 配置

解决方案:

vue
<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 语言未同步更新

解决方案:

typescript
// 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 标签

问题: 需要在翻译文本中包含链接或样式

解决方案:

vue
<!-- 使用 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: '用户协议'
  }
}

总结

国际化是构建全球化应用的基础能力。通过本文档介绍的最佳实践:

  1. 完整方案 - 后端API、前端界面、移动端应用全面国际化
  2. 自动检测 - 智能识别用户语言偏好,提升用户体验
  3. 动态切换 - 支持运行时语言切换,无需重启应用
  4. 统一管理 - 集中管理消息资源,确保翻译一致性
  5. 类型安全 - TypeScript 类型检查避免翻译键错误
  6. 扩展灵活 - 易于添加新语言支持

建议在实际使用中:

  • 建立完整的术语表和翻译规范
  • 定期审查和更新翻译内容
  • 使用专业翻译工具确保质量
  • 收集用户反馈持续优化
  • 考虑使用翻译管理平台(如 Crowdin, Transifex)