国际化 - 项目实战4 -vue3 国际化的高阶玩法

32 阅读3分钟

1 业务结构:

src/lang/
├── index.js                 # 主入口文件(重构)
├── languageMap.js           # 保持不变
├── element-plus.i18n.js     # 保持不变
├── modules/                 # 新增模块目录
│   ├── index.js            # 模块聚合
│   ├── common.js           # 公共词汇
│   ├── auth.js             # 认证相关
│   ├── product.js          # 商品管理
│   ├── order.js            # 订单管理
│   ├── finance.js          # 财务管理
│   ├── customer.js         # 客户管理
│   └── report.js           # 报表统计
└── tenant/                 # 租户自定义(可选)
    ├── tenant-001.js
    └── tenant-002.js

2 创建模块化语言文件

// src/lang/modules/common.js 举例
export default {
  'zh-CN': {
    common: {
      save: '保存',
      cancel: '取消',
      delete: '删除',
      edit: '编辑',
      confirm: '确认',
      loading: '加载中...',
      search: '搜索',
      reset: '重置',
      operation: '操作',
      status: '状态',
      createTime: '创建时间',
      updateTime: '更新时间'
    },
    message: {
      success: '操作成功',
      error: '操作失败',
      confirmDelete: '确认删除吗?',
      noData: '暂无数据'
    }
  },
  'en-US': {
    common: {
      save: 'Save',
      cancel: 'Cancel',
      delete: 'Delete',
      edit: 'Edit',
      confirm: 'Confirm',
      loading: 'Loading...',
      search: 'Search',
      reset: 'Reset',
      operation: 'Operation',
      status: 'Status',
      createTime: 'Create Time',
      updateTime: 'Update Time'
    },
    message: {
      success: 'Operation successful',
      error: 'Operation failed',
      confirmDelete: 'Confirm deletion?',
      noData: 'No data'
    }
  },
  'es-ES': {
    common: {
      save: 'Guardar',
      cancel: 'Cancelar',
      delete: 'Eliminar',
      edit: 'Editar',
      confirm: 'Confirmar',
      loading: 'Cargando...',
      search: 'Buscar',
      reset: 'Restablecer',
      operation: 'Operación',
      status: 'Estado',
      createTime: 'Tiempo de creación',
      updateTime: 'Tiempo de actualización'
    },
    message: {
      success: 'Operación exitosa',
      error: 'Operación fallida',
      confirmDelete: '¿Confirmar eliminación?',
      noData: 'Sin datos'
    }
  }
}

3 模块聚合文件

// src/lang/modules/index.js

import common from './common.js'
import auth from './auth.js'
import product from './product.js'
import order from './order.js'
import finance from './finance.js'
import customer from './customer.js'
import report from './report.js'

// 合并所有模块
function mergeModules(modules) {
  const languages = ['zh-CN', 'en-US', 'es-ES']
  const result = {}
  
  languages.forEach(lang => {
    result[lang] = {}
    modules.forEach(module => {
      if (module[lang]) {
        // 深度合并模块
        Object.keys(module[lang]).forEach(key => {
          result[lang][key] = {
            ...result[lang][key],
            ...module[lang][key]
          }
        })
      }
    })
  })
  
  return result
}

const allModules = [common, auth, product, order, finance, customer, report]
const messages = mergeModules(allModules)

export default messages

4 重构主入口文件

import { createI18n } from "vue-i18n"
import messages from './modules/index.js'

// vue模块 之外的调用方法
import { createPinia } from "pinia";
import useUserStore from "@/store/modules/user";

const pinia = createPinia()
const userStore = useUserStore(pinia)

// 动态加载租户自定义配置
const loadTenantMessages = async (tenantType) => {
  try {
    // 从本地文件加载
    const tenantMsg = await import(`./tenant/tenant-${tenantType}.js`)
    return tenantMsg.default || {}
  } catch (error) {
    console.log(`租户 ${tenantType} 无自定义配置`)
    return {}
  }
}

// 创建 i18n 实例(异步)
export const createI18nInstance = async (locale = 'zh-CN') => {
  const tenantType = userStore.userType || '00'

  // 加载租户自定义配置
  const tenantMessages = await loadTenantMessages(tenantType, locale)

  // 正确的合并:对每种语言分别合并
  const finalMessages = {}
  const languages = ['zh-CN', 'en-US', 'es-ES']

  languages.forEach(lang => {
    finalMessages[lang] = {
      ...messages[lang],              // 基础配置
      ...(tenantMessages[lang] || {}) // 租户配置(覆盖基础)
    }
  })

  const i18n = createI18n({
    legacy: false,
    locale: locale,
    fallbackLocale: 'zh-CN',
    messages: finalMessages,
    silentTranslationWarn: true,
    missingWarn: false
  })

  return i18n
}

// 为了保持现有代码兼容性,也提供同步版本
export const i18n = createI18n({
  legacy: false,
  locale: 'zh-CN',
  fallbackLocale: 'zh-CN',
  messages: messages,
})

export default i18n

5 更新语言映射配置

// src/lang/languageMap.js 更新
/**
 * 语言环境配置映射
 */
export function getLocaleConfig(locale) {
  const localeMap = {
    'en': 'en-US',
    'es': 'es-ES', 
    'zh': 'zh-CN',
    'en-US': 'en-US',
    'es-ES': 'es-ES',
    'zh-CN': 'zh-CN'
  }
  return localeMap[locale] || 'es-ES'
}

/**
 * 语言环境配置币种映射
 */
export function getCurrencyConfig(locale) {
  const currencyMap = {
    'en': { locale: 'en-US', symbol: '$', currency: 'USD' },
    'es': { locale: 'es-ES', symbol: '€', currency: 'EUR' },
    'zh': { locale: 'zh-CN', symbol: '¥', currency: 'CNY' },
    'en-US': { locale: 'en-US', symbol: '$', currency: 'USD' },
    'es-ES': { locale: 'es-ES', symbol: '€', currency: 'EUR' },
    'zh-CN': { locale: 'zh-CN', symbol: '¥', currency: 'CNY' }
  }
  return currencyMap[locale] || { locale: 'es-ES', symbol: '€', currency: 'EUR' }
}

/**
 * 获取支持的语言列表
 */
export const SUPPORTED_LANGUAGES = [
  { value: 'zh-CN', label: '中文' },
  { value: 'en-US', label: 'English' },
  { value: 'es-ES', label: 'Español' }
]

6 更新主组件

// APP.vue
<template>
  <el-config-provider :locale="elementLocales[effectiveLang]">
    <router-view></router-view>
  </el-config-provider>
</template>

<script setup>
import { ref, watch, onMounted } from 'vue'
import { useLanguageStore } from '@/store/modules/language'
import { elementLocales } from '@/lang/element-plus.i18n'
import { useI18n } from 'vue-i18n'
import { getLocaleConfig } from '@/lang/languageMap'

const languageStore = useLanguageStore()
const { locale } = useI18n()

// 计算有效的语言代码
const effectiveLang = computed(() => {
  const storeLang = languageStore.language
  return getLocaleConfig(storeLang)
})

// 监听语言变化
watch(effectiveLang, (newLang) => {
  locale.value = newLang
  // 可以在这里添加语言切换的其它逻辑,如更新页面标题等
})

// 初始化语言
onMounted(() => {
  locale.value = effectiveLang.value
})
</script>

7 封装工具函数 - 按需加载

// src/utils/i18n-utils.js
import { i18n } from '@/lang'

/**
 * 动态加载模块(按需加载)
 * 只有拆分模块,只加载个别模块语言的时候使用
 * 当前项目不需要 - 后期备用!!
 */
export const loadLanguageModule = async (moduleName) => {
  try {
    const module = await import(`@/lang/modules/${moduleName}.js`)
    // 合并到现有消息中
    const { locale } = i18n.global
    const currentMessages = i18n.global.getLocaleMessage(locale)
    
    i18n.global.mergeLocaleMessage(locale, {
      ...currentMessages,
      ...module.default[locale]
    })
  } catch (error) {
    console.warn(`Failed to load module: ${moduleName}`, error)
  }
}

/**
 * 获取翻译(带默认值)
 */
export const t = (key, defaultValue = '') => {
  const value = i18n.global.t(key)
  return value === key ? defaultValue : value
}

/**
 * 批量翻译
 */
export const translateBatch = (keys, namespace = '') => {
  const result = {}
  keys.forEach(key => {
    const fullKey = namespace ? `${namespace}.${key}` : key
    result[key] = t(fullKey, key)
  })
  return result
}

8 使用示例

<template>
  <div>
    <h1>{{ $t('order.title') }}</h1>
    <el-button type="primary">
      {{ $t('order.create') }}
    </el-button>
    
    <span>{{ $t('order.status.pending') }}</span>
    <span>{{ $t('common.message.success') }}</span>
  </div>
</template>

<script setup>
import { useI18n } from 'vue-i18n'
import { loadLanguageModule } from '@/utils/i18n-utils'

const { t } = useI18n()

// 如果需要动态加载模块
onMounted(async () => {
  await loadLanguageModule('report') // 按需加载报表模块
})
</script>