国际化 i18n 在 Vue3 项目完整实践

作者:忆笙智云官方 | 发布时间:2026-06-12 11:00 | 更新时间:2026-07-12 11:00

国际化i18n在Vue3项目完整实践

引言

随着企业级应用的全球化需求增长,国际化(Internationalization,简称 i18n)已成为标配功能。一个支持多语言的系统不仅能服务更多用户群体,还能提升产品的专业度和竞争力。然而,国际化不仅仅是文本翻译,还涉及日期格式、数字格式、货币符号、布局方向等多维度的本地化处理。

本文分享在 Vue 3 + vue-i18n 项目中实现国际化的完整实践,涵盖中文/英文/繁体中文三语言支持、语言切换即时生效、翻译文件组织、以及新增语言包的扩展方案。

核心内容

一、国际化整体架构

flowchart TB
    A[国际化架构] --> B[翻译资源层]
    A --> C[状态管理层]
    A --> D[视图渲染层]
    A --> E[工具函数层]

    B --> B1[语言包JSON文件]
    B --> B2[按模块拆分]
    B --> B3[懒加载支持]

    C --> C1[Pinia 语言状态]
    C --> C2[localStorage 持久化]
    C --> C3[服务端同步]

    D --> D1[$t 函数]
    D --> D2[v-t 指令]
    D --> D3[组件内翻译]

    E --> E1[日期格式化]
    E --> E2[数字格式化]
    E --> E3[复数处理]

二、vue-i18n 基础配置

2.1 安装依赖

npm install vue-i18n@9

2.2 创建 i18n 实例

/**
 * i18n 国际化配置
 * 支持中文简体、英文、繁体中文
 */
import { createI18n } from 'vue-i18n'

// 支持的语言列表
export const SUPPORT_LOCALES = [
  { code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
  { code: 'en', name: 'English', flag: '🇺🇸' },
  { code: 'zh-TW', name: '繁體中文', flag: '🇹🇼' },
]

// 默认语言
const DEFAULT_LOCALE = 'zh-CN'

// 创建 i18n 实例
const i18n = createI18n({
  legacy: false,           // 使用 Composition API 模式
  locale: DEFAULT_LOCALE,  // 默认语言
  fallbackLocale: 'zh-CN', // 回退语言
  globalInjection: true,   // 全局注入 $t 函数
  messages: {},            // 初始为空,按需加载
})

export default i18n

三、翻译文件组织

3.1 目录结构

src/locales/
├── index.js              # i18n 入口配置
├── zh-CN/                # 简体中文
│   ├── common.json       # 公共翻译
│   ├── menu.json         # 菜单翻译
│   ├── validation.json   # 表单验证
│   ├── table.json        # 表格相关
│   ├── button.json       # 按钮文本
│   └── modules/          # 业务模块
│       ├── user.json
│       ├── role.json
│       ├── dept.json
│       └── dict.json
├── en/                   # 英文
│   ├── common.json
│   ├── menu.json
│   ├── validation.json
│   ├── table.json
│   ├── button.json
│   └── modules/
│       ├── user.json
│       ├── role.json
│       ├── dept.json
│       └── dict.json
└── zh-TW/                # 繁体中文
    ├── common.json
    ├── menu.json
    ├── validation.json
    ├── table.json
    ├── button.json
    └── modules/
        ├── user.json
        ├── role.json
        ├── dept.json
        └── dict.json

3.2 翻译文件示例

zh-CN/common.json:

{
  "app": {
    "title": "管理系统",
    "loading": "加载中...",
    "noData": "暂无数据",
    "success": "操作成功",
    "fail": "操作失败",
    "confirm": "确认",
    "cancel": "取消",
    "close": "关闭",
    "save": "保存",
    "delete": "删除",
    "edit": "编辑",
    "add": "新增",
    "search": "搜索",
    "reset": "重置",
    "export": "导出",
    "import": "导入",
    "refresh": "刷新",
    "back": "返回",
    "more": "更多",
    "detail": "详情",
    "status": "状态",
    "createTime": "创建时间",
    "updateTime": "更新时间",
    "remark": "备注",
    "operation": "操作"
  }
}

en/common.json:

{
  "app": {
    "title": "Management System",
    "loading": "Loading...",
    "noData": "No Data",
    "success": "Operation Successful",
    "fail": "Operation Failed",
    "confirm": "Confirm",
    "cancel": "Cancel",
    "close": "Close",
    "save": "Save",
    "delete": "Delete",
    "edit": "Edit",
    "add": "Add",
    "search": "Search",
    "reset": "Reset",
    "export": "Export",
    "import": "Import",
    "refresh": "Refresh",
    "back": "Back",
    "more": "More",
    "detail": "Detail",
    "status": "Status",
    "createTime": "Create Time",
    "updateTime": "Update Time",
    "remark": "Remark",
    "operation": "Operation"
  }
}

zh-CN/validation.json:

{
  "required": "{field}不能为空",
  "minLength": "{field}长度不能少于{min}个字符",
  "maxLength": "{field}长度不能超过{max}个字符",
  "email": "请输入有效的邮箱地址",
  "phone": "请输入有效的手机号码",
  "idCard": "请输入有效的身份证号",
  "number": "{field}必须为数字",
  "integer": "{field}必须为整数",
  "url": "请输入有效的URL地址",
  "range": "{field}必须在{min}到{max}之间"
}

en/validation.json:

{
  "required": "{field} is required",
  "minLength": "{field} must be at least {min} characters",
  "maxLength": "{field} must be at most {max} characters",
  "email": "Please enter a valid email address",
  "phone": "Please enter a valid phone number",
  "idCard": "Please enter a valid ID card number",
  "number": "{field} must be a number",
  "integer": "{field} must be an integer",
  "url": "Please enter a valid URL",
  "range": "{field} must be between {min} and {max}"
}

四、语言包按需加载

大型项目中翻译文件可能很大,按需加载可以优化首屏性能:

/**
 * 语言包加载器
 * 支持按需加载和全量加载两种模式
 */
import i18n from '@/locales'

/** 语言包模块映射 */
const messageModules = import.meta.glob('../locales/*/modules/*.json')

/**
 * 加载指定语言的公共翻译
 * @param {string} locale 语言代码
 */
async function loadCommonMessages(locale) {
  const modules = [
    `../locales/${locale}/common.json`,
    `../locales/${locale}/menu.json`,
    `../locales/${locale}/validation.json`,
    `../locales/${locale}/table.json`,
    `../locales/${locale}/button.json`,
  ]

  const messages = {}
  const results = await Promise.all(
    modules.map(path => import(path /* @vite-ignore */))
  )

  results.forEach((mod, index) => {
    const key = modules[index].split('/').pop().replace('.json', '')
    messages[key] = mod.default
  })

  return messages
}

/**
 * 加载指定语言和模块的翻译
 * @param {string} locale  语言代码
 * @param {string} module  模块名称
 */
async function loadModuleMessages(locale, module) {
  const path = `../locales/${locale}/modules/${module}.json`
  try {
    const mod = await import(path /* @vite-ignore */)
    return { [module]: mod.default }
  } catch (e) {
    console.warn(`加载翻译模块失败: ${locale}/modules/${module}`)
    return {}
  }
}

/**
 * 设置当前语言并加载翻译
 * @param {string} locale 语言代码
 */
export async function setI18nLanguage(locale) {
  // 加载公共翻译
  const messages = await loadCommonMessages(locale)

  // 合并到 i18n
  i18n.global.setLocaleMessage(locale, messages)
  i18n.global.locale.value = locale

  // 设置 HTML lang 属性
  document.documentElement.setAttribute('lang', locale)

  // 持久化
  localStorage.setItem('locale', locale)
}

/**
 * 按需加载业务模块翻译
 * @param {string} module 模块名称
 */
export async function loadModule(module) {
  const locale = i18n.global.locale.value
  const messages = await loadModuleMessages(locale, module)

  // 合并到现有翻译中
  const existing = i18n.global.getLocaleMessage(locale)
  i18n.global.setLocaleMessage(locale, { ...existing, ...messages })
}

五、语言切换即时生效

5.1 语言状态管理

/**
 * 国际化状态管理
 */
import { defineStore } from 'pinia'
import { setI18nLanguage, SUPPORT_LOCALES } from '@/locales'

export const useLocaleStore = defineStore('locale', {
  state: () => ({
    /** 当前语言 */
    currentLocale: localStorage.getItem('locale') || 'zh-CN',

    /** 支持的语言列表 */
    supportedLocales: SUPPORT_LOCALES,
  }),

  getters: {
    /** 当前语言信息 */
    currentLocaleInfo: (state) => {
      return SUPPORT_LOCALES.find(l => l.code === state.currentLocale)
    },
  },

  actions: {
    /** 切换语言 */
    async switchLocale(locale) {
      if (this.currentLocale === locale) return

      await setI18nLanguage(locale)
      this.currentLocale = locale

      // 同步到服务端(用户偏好)
      try {
        await updateUserLocale({ locale })
      } catch (e) {
        // 服务端同步失败不影响本地使用
        console.warn('语言偏好同步服务端失败', e)
      }
    },
  },
})

5.2 语言切换组件

<template>
  <el-dropdown trigger="click" @command="onLocaleChange">
    <span class="locale-switcher">
      <el-icon><Globe /></el-icon>
      {{ currentLocaleInfo?.name }}
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item
          v-for="locale in localeStore.supportedLocales"
          :key="locale.code"
          :command="locale.code"
          :class="{ active: localeStore.currentLocale === locale.code }"
        >
          {{ locale.flag }} {{ locale.name }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup>
import { computed } from 'vue'
import { Globe } from '@element-plus/icons-vue'
import { useLocaleStore } from '@/store/modules/locale'

const localeStore = useLocaleStore()

const currentLocaleInfo = computed(() => localeStore.currentLocaleInfo)

const onLocaleChange = (locale) => {
  localeStore.switchLocale(locale)
}
</script>

六、组件中使用翻译

6.1 模板中使用

<template>
  <div class="user-management">
    <!-- 使用 $t 函数 -->
    <h2>{{ $t('user.title') }}</h2>

    <!-- 带参数的翻译 -->
    <p>{{ $t('user.welcome', { name: userName }) }}</p>

    <!-- 在属性中使用 -->
    <el-input :placeholder="$t('user.searchPlaceholder')" />

    <!-- 在表单验证中使用 -->
    <el-form :rules="formRules" :model="formData">
      <el-form-item :label="$t('user.username')" prop="username">
        <el-input v-model="formData.username" />
      </el-form-item>
    </el-form>
  </div>
</template>

6.2 Composition API 中使用

<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

const { t, locale } = useI18n()

// 在 JS 逻辑中使用翻译
const formRules = computed(() => ({
  username: [
    { required: true, message: t('validation.required', { field: t('user.username') }), trigger: 'blur' },
    { min: 2, max: 20, message: t('validation.range', { field: t('user.username'), min: 2, max: 20 }), trigger: 'blur' },
  ],
  email: [
    { required: true, message: t('validation.required', { field: t('user.email') }), trigger: 'blur' },
    { type: 'email', message: t('validation.email'), trigger: 'blur' },
  ],
}))

// 动态翻译表格列
const tableColumns = computed(() => [
  { prop: 'username', label: t('user.username') },
  { prop: 'email', label: t('user.email') },
  { prop: 'phone', label: t('user.phone') },
  { prop: 'status', label: t('common.app.status') },
  { prop: 'createTime', label: t('common.app.createTime') },
])
</script>

七、日期与数字格式化

7.1 日期格式化

/**
 * 日期格式化工具
 * 根据当前语言自动调整日期格式
 */
import { useI18n } from 'vue-i18n'

/** 各语言的日期格式偏好 */
const DATE_FORMATS = {
  'zh-CN': {
    date: 'YYYY年MM月DD日',
    datetime: 'YYYY年MM月DD日 HH:mm:ss',
    short: 'MM-DD',
    time: 'HH:mm:ss',
  },
  'en': {
    date: 'MM/DD/YYYY',
    datetime: 'MM/DD/YYYY HH:mm:ss',
    short: 'MM/DD',
    time: 'HH:mm:ss',
  },
  'zh-TW': {
    date: 'YYYY年MM月DD日',
    datetime: 'YYYY年MM月DD日 HH:mm:ss',
    short: 'MM-DD',
    time: 'HH:mm:ss',
  },
}

/**
 * 格式化日期
 * @param {Date|string} date   日期
 * @param {string}      format 格式类型:date/datetime/short/time
 * @param {string}      locale 语言代码
 */
export function formatDate(date, format = 'datetime', locale = 'zh-CN') {
  const dayjs = // 使用 dayjs 或其他日期库
  const formatStr = DATE_FORMATS[locale]?.[format] || DATE_FORMATS['zh-CN'][format]
  return dayjs(date).format(formatStr)
}

7.2 数字格式化

/**
 * 数字格式化工具
 * 根据当前语言自动调整数字格式(千分位、小数点等)
 */

/**
 * 格式化数字
 * @param {number} number  数字
 * @param {string} locale  语言代码
 * @param {object} options Intl.NumberFormat 选项
 */
export function formatNumber(number, locale = 'zh-CN', options = {}) {
  return new Intl.NumberFormat(locale, options).format(number)
}

// 使用示例:
// formatNumber(1234567.89, 'zh-CN')     → "1,234,567.89"
// formatNumber(1234567.89, 'en')         → "1,234,567.89"

/**
 * 格式化货币
 */
export function formatCurrency(number, locale = 'zh-CN', currency = 'CNY') {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).format(number)
}

// 使用示例:
// formatCurrency(1234.5, 'zh-CN', 'CNY')  → "¥1,234.50"
// formatCurrency(1234.5, 'en', 'USD')      → "$1,234.50"

八、新增语言包扩展方案

当需要支持新语言时,遵循以下流程:

flowchart TB
    A[新增语言需求] --> B[创建语言目录
如: src/locales/ja/]
    B --> C[复制现有语言包
作为翻译模板]
    C --> D[逐文件翻译
替换所有文本值]
    D --> E[在 SUPPORT_LOCALES
中注册新语言]
    E --> F[测试验证
所有页面翻译正确]
    F --> G[发布上线]

    style A fill:#FF9800,color:#fff
    style G fill:#4CAF50,color:#fff

8.1 扩展步骤

// 步骤1:在 SUPPORT_LOCALES 中添加新语言
export const SUPPORT_LOCALES = [
  { code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
  { code: 'en', name: 'English', flag: '🇺🇸' },
  { code: 'zh-TW', name: '繁體中文', flag: '🇹🇼' },
  // 新增日语支持
  { code: 'ja', name: '日本語', flag: '🇯🇵' },
]
# 步骤2:创建语言包目录和文件
mkdir -p src/locales/ja/modules
# 复制 zh-CN 下的所有 JSON 文件到 ja/ 目录
# 然后逐个翻译 JSON 中的值(注意只翻译值,不要翻译键名)
// src/locales/ja/common.json
{
  "app": {
    "title": "管理システム",
    "loading": "読み込み中...",
    "noData": "データなし",
    "success": "操作成功",
    "fail": "操作失敗",
    "confirm": "確認",
    "cancel": "キャンセル",
    "save": "保存",
    "delete": "削除",
    "edit": "編集",
    "add": "追加",
    "search": "検索",
    "reset": "リセット"
  }
}

8.2 翻译完整性检查

/**
 * 翻译完整性检查工具
 * 对比不同语言包的 key 是否一致,发现遗漏的翻译
 */
export function checkTranslationIntegrity(baseLocale = 'zh-CN') {
  const baseMessages = i18n.global.getLocaleMessage(baseLocale)

  const issues = []

  SUPPORT_LOCALES.forEach(({ code }) => {
    if (code === baseLocale) return

    const targetMessages = i18n.global.getLocaleMessage(code)
    const missingKeys = findMissingKeys(baseMessages, targetMessages, code)

    if (missingKeys.length > 0) {
      issues.push({
        locale: code,
        missingKeys,
        count: missingKeys.length,
      })
    }
  })

  return issues
}

/**
 * 递归查找缺失的翻译 key
 */
function findMissingKeys(source, target, prefix = '') {
  const missing = []

  for (const key in source) {
    const fullKey = prefix ? `${prefix}.${key}` : key

    if (!(key in target)) {
      missing.push(fullKey)
    } else if (typeof source[key] === 'object' && source[key] !== null) {
      missing.push(...findMissingKeys(source[key], target[key] || {}, fullKey))
    }
  }

  return missing
}

九、服务端翻译同步

sequenceDiagram
    participant C as 客户端
    participant S as 服务端
    participant DB as 数据库

    C->>S: 登录时获取用户语言偏好
    S->>DB: 查询 user_config
    DB-->>S: locale = 'en'
    S-->>C: 返回语言偏好

    C->>C: 设置语言为 en
    C->>C: 加载英文翻译包

    Note over C: 用户切换语言

    C->>S: PUT /api/user/locale {locale: 'zh-CN'}
    S->>DB: 更新用户语言偏好
    S-->>C: 更新成功
    C->>C: 切换为中文
    C->>C: 加载中文翻译包

结论与建议

核心要点

  1. 翻译文件按模块拆分:避免单个文件过大,支持按需加载
  2. Composition API 模式:使用 useI18n() 而非 this.$t,更灵活
  3. 语言切换即时生效:无需刷新页面,切换后所有文本自动更新
  4. 参数化翻译:使用 {field} 占位符,避免翻译拼接
  5. 完整性检查:开发阶段检查翻译遗漏,确保每种语言的 key 一致

最佳实践建议

建议 说明
键名使用点分路径 user.management.titleuserManagementTitle 更清晰
翻译值不要拼接 用参数化代替字符串拼接,避免语序问题
日期数字本地化 使用 Intl API 处理日期和数字格式
懒加载翻译包 大型项目按模块懒加载,优化首屏性能
翻译复用 公共文本(按钮、状态等)放在 common.json,避免重复
自动化检查 CI 流程中加入翻译完整性检查
上下文标注 为翻译人员提供上下文注释,避免歧义

相关资源