Skip to content

国际化开发文档

1. 概述

梵医云前端项目使用 vue-i18n 实现国际化功能,支持多语言切换,目前支持简体中文和英文两种语言。

1.1 国际化特点

  • 基于vue-i18n:使用成熟的vue-i18n库实现
  • 按需加载:语言包按需加载,减少初始包体积
  • 类型安全:完整的TypeScript类型定义
  • 持久化存储:语言设置持久化到本地缓存
  • Element Plus集成:自动同步Element Plus组件的语言

1.2 支持的语言

语言代码语言名称文件名
zh-CN简体中文zh-CN.ts
enEnglishen.ts

2. 项目结构

2.1 国际化文件结构

fanyi-cloud-ui/
├── src/
│   ├── locales/              # 语言包目录
│   │   ├── zh-CN.ts        # 简体中文语言包
│   │   └── en.ts          # 英文语言包
│   ├── plugins/
│   │   └── vueI18n/       # vue-i18n插件
│   │       ├── index.ts    # i18n配置
│   │       └── helper.ts   # 辅助函数
│   ├── store/
│   │   └── modules/
│   │       └── locale.ts   # 语言状态管理
│   ├── hooks/
│   │   └── web/
│   │       ├── useI18n.ts  # i18n Hook
│   │       └── useLocale.ts # 语言切换Hook
│   └── layout/
│       └── components/
│           └── LocaleDropdown/ # 语言切换组件

3. 配置说明

3.1 vue-i18n配置

配置文件:[src/plugins/vueI18n/index.ts](file:///e:/git.fanyicloud.com.cn/fanyi-cloud-ui/src/plugins/vueI18n/index.ts)

typescript
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
import type { I18n, I18nOptions } from 'vue-i18n'
import { setHtmlPageLang } from './helper'

export let i18n: ReturnType<typeof createI18n>

const createI18nOptions = async (): Promise<I18nOptions> => {
  const localeStore = useLocaleStoreWithOut()
  const locale = localeStore.getCurrentLocale
  const localeMap = localeStore.getLocaleMap
  const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
  const message = defaultLocal.default ?? {}

  setHtmlPageLang(locale.lang)

  localeStore.setCurrentLocale({
    lang: locale.lang
  })

  return {
    legacy: false,           // 使用Composition API模式
    locale: locale.lang,     // 当前语言
    fallbackLocale: locale.lang, // 回退语言
    messages: {
      [locale.lang]: message
    },
    availableLocales: localeMap.map((v) => v.lang), // 可用语言列表
    sync: true,             // 同步语言
    silentTranslationWarn: true, // 静默翻译警告
    missingWarn: false,
    silentFallbackWarn: true
  }
}

export const setupI18n = async (app: App<Element>) => {
  const options = await createI18nOptions()
  i18n = createI18n(options) as I18n
  app.use(i18n)
}

3.2 应用初始化

在 [src/main.ts](file:///e:/git.fanyicloud.com.cn/fanyi-cloud-ui/src/main.ts) 中初始化i18n:

typescript
import { setupI18n } from '@/plugins/vueI18n'

const setupAll = async () => {
  const app = createApp(App)

  // 在其他插件之前初始化i18n
  await setupI18n(app)

  // ... 其他初始化代码
}

4. 语言包管理

4.1 语言包结构

语言包文件位于 src/locales/ 目录下,采用模块化结构:

typescript
export default {
  common: {
    // 通用文本
    inputText: '请输入',
    selectText: '请选择',
    query: '查询',
    reset: '重置',
    // ...
  },
  login: {
    // 登录相关
    welcome: '欢迎使用梵医云系统',
    username: '用户名',
    password: '密码',
    // ...
  },
  sys: {
    api: {
      // API相关
      operationFailed: '操作失败',
      // ...
    },
    // ...
  }
  // ...
}

4.2 语言包分类

分类说明示例
common通用文本按钮、提示、操作等
login登录相关登录表单、验证码等
sys系统相关API错误、异常处理等
profile个人中心用户信息、密码修改等
form表单相关表单组件、验证等
table表格相关表格操作、分页等
action操作相关新增、编辑、删除等

4.3 添加新的翻译

在语言包文件中添加新的翻译:

typescript
// src/locales/zh-CN.ts
export default {
  // ... 其他翻译
  custom: {
    newFeature: '新功能',
    welcomeMessage: '欢迎使用新功能'
  }
}

// src/locales/en.ts
export default {
  // ... other translations
  custom: {
    newFeature: 'New Feature',
    welcomeMessage: 'Welcome to new feature'
  }
}

5. 使用方法

5.1 在组件中使用

5.1.1 使用useI18n Hook

vue
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'

const { t } = useI18n()

// 简单翻译
const title = computed(() => t('common.query'))

// 带参数的翻译
const message = computed(() => t('common.welcome', { name: '用户' }))

// 带数组的翻译
const items = computed(() => t('common.items', ['项目1', '项目2']))
</script>

<template>
  <div>
    <h1>{{ t('common.welcome') }}</h1>
    <el-button>{{ t('common.query') }}</el-button>
    <p>{{ t('common.welcome', { name: '张三' }) }}</p>
  </div>
</template>

5.1.2 使用命名空间

vue
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'

// 使用命名空间
const { t } = useI18n('login')

const usernameLabel = computed(() => t('username'))
const passwordLabel = computed(() => t('password'))
</script>

<template>
  <el-form>
    <el-form-item :label="t('username')">
      <el-input />
    </el-form-item>
    <el-form-item :label="t('password')">
      <el-input type="password" />
    </el-form-item>
  </el-form>
</template>

5.1.3 在模板中直接使用

vue
<template>
  <div>
    <!-- 直接使用t函数 -->
    <h1>{{ t('common.welcome') }}</h1>
    
    <!-- 带参数 -->
    <p>{{ t('common.welcome', { name: userName }) }}</p>
    
    <!-- 嵌套key -->
    <span>{{ t('sys.api.operationFailed') }}</span>
  </div>
</template>

5.2 在JS/TS中使用

typescript
import { useI18n } from '@/hooks/web/useI18n'

const { t } = useI18n()

// 在函数中使用
function showMessage() {
  const message = t('common.success')
  ElMessage.success(message)
}

// 在异步函数中使用
async function fetchData() {
  try {
    await api.getData()
    ElMessage.success(t('common.success'))
  } catch (error) {
    ElMessage.error(t('sys.api.operationFailed'))
  }
}

5.3 在路由中使用

typescript
// src/router/modules/system.ts
export default {
  path: '/system',
  name: 'System',
  component: Layout,
  redirect: '/system/user',
  meta: {
    title: 'system', // 使用语言包中的key
    icon: 'ep:setting'
  }
}

5.4 在表单验证中使用

typescript
import { useI18n } from '@/hooks/web/useI18n'

const { t } = useI18n()

const rules = {
  username: [
    { required: true, message: t('common.required'), trigger: 'blur' },
    { min: 3, max: 20, message: t('common.lengthError'), trigger: 'blur' }
  ],
  email: [
    { required: true, message: t('common.required'), trigger: 'blur' },
    { type: 'email', message: t('common.validateEmail'), trigger: ['blur', 'change'] }
  ]
}

6. 语言切换

6.1 LocaleDropdown组件

语言切换组件位于:[src/layout/components/LocaleDropdown/src/LocaleDropdown.vue](file:///e:/git.fanyicloud.com.cn/fanyi-cloud-ui/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue)

vue
<template>
  <ElDropdown trigger="click" @command="setLang">
    <Icon icon="ion:language-sharp" />
    <template #dropdown>
      <ElDropdownMenu>
        <ElDropdownItem 
          v-for="item in langMap" 
          :key="item.lang" 
          :command="item.lang"
        >
          {{ item.name }}
        </ElDropdownItem>
      </ElDropdownMenu>
    </template>
  </ElDropdown>
</template>

<script setup lang="ts">
import { useLocaleStore } from '@/store/modules/locale'
import { useLocale } from '@/hooks/web/useLocale'

const localeStore = useLocaleStore()
const langMap = computed(() => localeStore.getLocaleMap)
const currentLang = computed(() => localeStore.getCurrentLocale)

const setLang = (lang: LocaleType) => {
  if (lang === unref(currentLang).lang) return
  // 重新加载页面让整个语言多初始化
  window.location.reload()
  localeStore.setCurrentLocale({ lang })
  const { changeLocale } = useLocale()
  changeLocale(lang)
}
</script>

6.2 使用useLocale Hook

typescript
import { useLocale } from '@/hooks/web/useLocale'

const { changeLocale } = useLocale()

// 切换语言
const switchLanguage = async (lang: 'zh-CN' | 'en') => {
  await changeLocale(lang)
  // 语言切换后会自动刷新页面
}

6.3 手动切换语言

typescript
import { useLocaleStoreWithOut } from '@/store/modules/locale'
import { useLocale } from '@/hooks/web/useLocale'

const localeStore = useLocaleStoreWithOut()
const { changeLocale } = useLocale()

// 设置语言
const setLanguage = async (lang: 'zh-CN' | 'en') => {
  // 更新store
  localeStore.setCurrentLocale({ lang })
  
  // 切换i18n语言
  await changeLocale(lang)
  
  // 刷新页面
  window.location.reload()
}

7. 状态管理

7.1 Locale Store

Store文件:[src/store/modules/locale.ts](file:///e:/git.fanyicloud.com.cn/fanyi-cloud-ui/src/store/modules/locale.ts)

typescript
import { defineStore } from 'pinia'
import { store } from '../index'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'

const { wsCache } = useCache()

const elLocaleMap = {
  'zh-CN': zhCn,
  en: en
}

interface LocaleState {
  currentLocale: LocaleDropdownType
  localeMap: LocaleDropdownType[]
}

export const useLocaleStore = defineStore('locales', {
  state: (): LocaleState => {
    return {
      currentLocale: {
        lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
        elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
      },
      localeMap: [
        {
          lang: 'zh-CN',
          name: '简体中文'
        },
        {
          lang: 'en',
          name: 'English'
        }
      ]
    }
  },
  getters: {
    getCurrentLocale(): LocaleDropdownType {
      return this.currentLocale
    },
    getLocaleMap(): LocaleDropdownType[] {
      return this.localeMap
    }
  },
  actions: {
    setCurrentLocale(localeMap: LocaleDropdownType) {
      this.currentLocale.lang = localeMap?.lang
      this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
      wsCache.set(CACHE_KEY.LANG, localeMap?.lang)
    }
  }
})

7.2 使用Store

typescript
import { useLocaleStore } from '@/store/modules/locale'

const localeStore = useLocaleStore()

// 获取当前语言
const currentLang = computed(() => localeStore.getCurrentLocale.lang)

// 获取语言列表
const langList = computed(() => localeStore.getLocaleMap)

// 设置语言
localeStore.setCurrentLocale({ lang: 'en' })

8. 添加新语言

8.1 创建语言包文件

src/locales/ 目录下创建新的语言包文件:

typescript
// src/locales/ja.ts
export default {
  common: {
    inputText: '入力してください',
    selectText: '選択してください',
    query: '検索',
    reset: 'リセット',
    // ...
  },
  login: {
    welcome: '梵医云システムへようこそ',
    username: 'ユーザー名',
    password: 'パスワード',
    // ...
  }
  // ...
}

8.2 更新Locale Store

在 [src/store/modules/locale.ts](file:///e:/git.fanyicloud.com.cn/fanyi-cloud-ui/src/store/modules/locale.ts) 中添加新语言:

typescript
import ja from 'element-plus/es/locale/lang/ja'

const elLocaleMap = {
  'zh-CN': zhCn,
  en: en,
  'ja': ja  // 添加日语
}

export const useLocaleStore = defineStore('locales', {
  state: (): LocaleState => {
    return {
      currentLocale: {
        lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
        elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
      },
      localeMap: [
        {
          lang: 'zh-CN',
          name: '简体中文'
        },
        {
          lang: 'en',
          name: 'English'
        },
        {
          lang: 'ja',  // 添加日语
          name: '日本語'
        }
      ]
    }
  }
})

8.3 添加类型定义

在类型定义文件中添加新语言类型:

typescript
// src/types/localeDropdown.d.ts
export type LocaleType = 'zh-CN' | 'en' | 'ja'

9. 最佳实践

9.1 命名规范

  • 使用点号分隔的层级结构
  • 使用小写字母和下划线
  • 保持简洁明了
typescript
// 推荐
{
  common: {
    query: '查询',
    reset: '重置'
  },
  sys: {
    api: {
      operationFailed: '操作失败'
    }
  }
}

// 不推荐
{
  CommonQuery: '查询',
  SYS_API_OperationFailed: '操作失败'
}

9.2 翻译一致性

  • 同一术语使用相同的翻译
  • 保持翻译风格一致
  • 注意专业术语的准确性

9.3 性能优化

  • 使用按需加载减少初始包体积
  • 避免在循环中频繁调用t函数
  • 合理使用computed缓存翻译结果
vue
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'

const { t } = useI18n()

// 推荐:使用computed
const title = computed(() => t('common.welcome'))

// 不推荐:在模板中直接调用
</script>

<template>
  <div>
    <!-- 推荐 -->
    <h1>{{ title }}</h1>
    
    <!-- 不推荐 -->
    <h1>{{ t('common.welcome') }}</h1>
  </div>
</template>

9.4 参数化翻译

对于包含动态内容的文本,使用参数化翻译:

typescript
// 语言包
{
  common: {
    welcome: '欢迎,{name}!',
    itemCount: '共 {count} 个项目'
  }
}

// 使用
t('common.welcome', { name: '张三' })
t('common.itemCount', { count: 10 })

10. 常见问题

10.1 翻译不生效

问题原因

  • 语言包文件未正确导入
  • 翻译key拼写错误
  • 语言未正确切换

解决方案

typescript
// 检查语言包是否正确导入
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()

// 检查翻译key是否正确
console.log(t('common.welcome'))

// 检查当前语言
import { useLocaleStore } from '@/store/modules/locale'
const localeStore = useLocaleStore()
console.log(localeStore.getCurrentLocale.lang)

10.2 Element Plus组件语言未切换

问题原因

  • Element Plus语言包未正确配置
  • 语言切换后未刷新页面

解决方案

typescript
// 确保在store中配置了Element Plus语言包
const elLocaleMap = {
  'zh-CN': zhCn,
  en: en
}

// 切换语言后刷新页面
const setLang = (lang: LocaleType) => {
  localeStore.setCurrentLocale({ lang })
  changeLocale(lang)
  window.location.reload() // 必须刷新页面
}

10.3 翻译key不存在

问题原因

  • 翻译key在语言包中不存在
  • 命名空间使用错误

解决方案

typescript
// 检查翻译key是否存在
const { t } = useI18n()

// 使用命名空间时,不需要重复前缀
const { t } = useI18n('login')
t('username') // 正确,相当于 'login.username'

// 不使用命名空间时,需要完整路径
const { t } = useI18n()
t('login.username') // 正确

10.4 动态内容翻译

问题:需要翻译包含动态变量的文本

解决方案

typescript
// 语言包
{
  user: {
    welcome: '欢迎,{name}!',
    message: '您有 {count} 条未读消息'
  }
}

// 使用
t('user.welcome', { name: userName.value })
t('user.message', { count: unreadCount.value })

11. 完整示例

11.1 用户管理页面

vue
<template>
  <ContentWrap>
    <Search 
      :schema="searchSchema" 
      @search="handleSearch" 
      @reset="handleReset"
    >
      <template #actionMore>
        <el-button type="primary" @click="handleAdd">
          {{ t('action.create') }}
        </el-button>
      </template>
    </Search>
  </ContentWrap>

  <ContentWrap>
    <Table
      :columns="tableColumns"
      :data="tableList"
      :loading="loading"
    >
      <template #action="{ row }">
        <el-button link type="primary" @click="handleEdit(row)">
          {{ t('action.edit') }}
        </el-button>
        <el-button link type="danger" @click="handleDelete(row)">
          {{ t('action.delete') }}
        </el-button>
      </template>
    </Table>
  </ContentWrap>
</template>

<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'

const { t } = useI18n()

const searchSchema = computed(() => [
  {
    field: 'username',
    label: t('profile.user.username'),
    component: 'Input'
  },
  {
    field: 'status',
    label: t('common.status'),
    component: 'Select'
  }
])

const tableColumns = computed(() => [
  {
    field: 'username',
    label: t('profile.user.username')
  },
  {
    field: 'nickname',
    label: t('profile.user.nickname')
  },
  {
    field: 'status',
    label: t('common.status')
  }
])

const handleAdd = () => {
  ElMessage.success(t('common.createSuccess'))
}

const handleEdit = (row: any) => {
  console.log(t('action.edit'), row)
}

const handleDelete = (row: any) => {
  ElMessageBox.confirm(
    t('common.delMessage'),
    t('common.confirmTitle'),
    {
      confirmButtonText: t('common.ok'),
      cancelButtonText: t('common.cancel'),
      type: 'warning'
    }
  ).then(() => {
    ElMessage.success(t('common.delSuccess'))
  })
}
</script>

11.2 表单验证

typescript
import { useI18n } from '@/hooks/web/useI18n'

const { t } = useI18n()

const rules = {
  username: [
    { 
      required: true, 
      message: t('common.required'), 
      trigger: 'blur' 
    },
    { 
      min: 3, 
      max: 20, 
      message: t('common.lengthError', { min: 3, max: 20 }), 
      trigger: 'blur' 
    }
  ],
  email: [
    { 
      required: true, 
      message: t('common.required'), 
      trigger: 'blur' 
    },
    { 
      type: 'email', 
      message: t('common.validateEmail'), 
      trigger: ['blur', 'change'] 
    }
  ],
  password: [
    { 
      required: true, 
      message: t('profile.password.oldPwdMsg'), 
      trigger: 'blur' 
    },
    { 
      min: 6, 
      max: 20, 
      message: t('profile.password.pwdRules'), 
      trigger: 'blur' 
    }
  ]
}

注意:本文档持续更新中,如有问题请及时反馈。