Vue 国际化(i18n):vue-i18n 多语言配置深度指南
【摘要】 一、引言1.1 国际化的重要性在全球化时代,多语言支持已成为现代Web应用的基本要求。Vue生态中,vue-i18n作为官方推荐的国际化解决方案,提供了完整的多语言管理能力。1.2 技术价值与市场分析class VueI18nAnalysis { /** 国际化市场分析 */ static getMarketAnalysis() { return { ...
一、引言
1.1 国际化的重要性
1.2 技术价值与市场分析
class VueI18nAnalysis {
/** 国际化市场分析 */
static getMarketAnalysis() {
return {
'全球覆盖需求': '75%的企业需要多语言支持',
'用户增长影响': '多语言应用用户增长提升40%',
'收入提升': '本地化应用收入平均增长25%',
'技术成熟度': 'vue-i18n在Vue生态中占比85%'
};
}
/** 技术选型对比 */
static getTechnologyComparison() {
return {
'vue-i18n': {
'Vue集成度': '⭐⭐⭐⭐⭐ (官方支持)',
'功能完整性': '⭐⭐⭐⭐⭐ (全面覆盖)',
'社区活跃度': '⭐⭐⭐⭐⭐ (高度活跃)',
'学习曲线': '⭐⭐⭐ (中等难度)',
'性能表现': '⭐⭐⭐⭐ (优化良好)'
},
'react-i18next': {
'Vue集成度': '⭐⭐⭐ (需要适配)',
'功能完整性': '⭐⭐⭐⭐⭐ (功能丰富)',
'社区活跃度': '⭐⭐⭐⭐ (活跃)',
'学习曲线': '⭐⭐⭐⭐ (较复杂)',
'性能表现': '⭐⭐⭐⭐ (良好)'
},
'自定义方案': {
'Vue集成度': '⭐⭐⭐⭐⭐ (完全可控)',
'功能完整性': '⭐⭐⭐ (需要自实现)',
'社区活跃度': '⭐ (无社区)',
'学习曲线': '⭐⭐⭐⭐⭐ (复杂)',
'性能表现': '⭐⭐⭐⭐⭐ (最优)'
}
};
}
/** 选择建议 */
static getSelectionGuide() {
return {
'选择vue-i18n当': [
'项目使用Vue框架',
'需要官方稳定支持',
'团队熟悉Vue生态',
'需要丰富插件生态'
],
'选择其他方案当': [
'多框架技术栈',
'特殊定制需求',
'性能极致要求',
'已有技术积累'
]
};
}
}
1.3 性能基准对比
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、技术背景
2.1 国际化技术架构演进
graph TB
A[国际化技术演进] --> B[硬编码文本]
A --> C[配置文件分离]
A --> D[框架集成方案]
A --> E[全栈国际化]
B --> B1[维护困难]
B --> B2[无法动态切换]
C --> C1[文本外部化]
C --> C2[基础多语言]
D --> D1[vue-i18n]
D --> D2[react-intl]
D --> D3[angular i18n]
E --> E1[前后端统一]
E --> E2[动态内容]
E --> E3[实时翻译]
D1 --> F[现代国际化方案]
E1 --> F
F --> G[类型安全]
F --> H[动态加载]
F --> I[工具链集成]
2.2 核心概念解析
class I18nCoreConcepts {
constructor() {
this.concepts = {
'本地化 (Localization)': {
'定义': '适应特定地区或语言的过程',
'包含': '语言、日期、货币、数字格式等',
'示例': '中文简繁体、美式英式英语'
},
'国际化 (Internationalization)': {
'定义': '设计支持多语言的过程',
'原则': '代码与文本分离、布局弹性',
'标记': '通常简写为 i18n (i + 18字母 + n)'
},
'语言环境 (Locale)': {
'定义': '特定语言和地区的组合',
'格式': '语言代码-国家代码',
'示例': 'zh-CN、en-US、ja-JP'
},
'消息格式 (Message Format)': {
'定义': '包含变量的文本模板',
'语法': '插值、复数、性别、选择等',
'示例': '{name}有{count}个苹果'
}
};
}
getImplementationLevels() {
return {
'Level 1: 基础文本替换': [
'静态文本多语言',
'简单变量插值',
'基础语言切换'
],
'Level 2: 高级格式处理': [
'日期时间本地化',
'数字货币格式化',
'复数规则处理'
],
'Level 3: 动态内容国际化': [
'API响应本地化',
'用户生成内容',
'实时翻译集成'
],
'Level 4: 全栈国际化': [
'SSR同构渲染',
'数据库内容本地化',
'工作流国际化'
]
};
}
}
三、环境准备与项目配置
3.1 安装与基础配置
// package.json 依赖配置
{
"dependencies": {
"vue": "^3.3.0",
"vue-i18n": "^9.0.0",
"@intlify/unplugin-vue-i18n": "^0.8.0",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"vite": "^4.0.0",
"typescript": "^5.0.0",
"@intlify/vite-plugin-vue-i18n": "^6.0.0"
}
}
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueI18n from '@intlify/vite-plugin-vue-i18n';
import { resolve } from 'path';
export default defineConfig({
plugins: [
vue(),
vueI18n({
include: resolve(__dirname, './src/locales/**'),
compositionOnly: false,
fullInstall: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
},
build: {
target: 'es2015',
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'vue-i18n'],
'i18n-locales': ['src/locales/**']
}
}
}
}
});
3.2 TypeScript 配置支持
// tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"lib": ["es2020", "dom"],
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
},
"types": ["vite/client", "vue-i18n"]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue",
"src/locales/**/*.json"
]
}
3.3 vue-i18n 核心配置
// src/plugins/i18n.ts
import { createI18n } from 'vue-i18n';
import type { I18nOptions, LocaleMessages } from 'vue-i18n';
// 支持的语言列表
export const supportedLocales = [
{ code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
{ code: 'en-US', name: 'English', flag: '🇺🇸' },
{ code: 'ja-JP', name: '日本語', flag: '🇯🇵' },
{ code: 'ko-KR', name: '한국어', flag: '🇰🇷' },
{ code: 'fr-FR', name: 'Français', flag: '🇫🇷' },
{ code: 'de-DE', name: 'Deutsch', flag: '🇩🇪' }
] as const;
export type SupportedLocale = typeof supportedLocales[number]['code'];
// 默认语言配置
const DEFAULT_LOCALE: SupportedLocale = 'zh-CN';
const FALLBACK_LOCALE: SupportedLocale = 'en-US';
// 语言包结构定义
interface AppMessages {
common: {
buttons: {
confirm: string;
cancel: string;
save: string;
delete: string;
};
messages: {
success: string;
error: string;
warning: string;
};
};
navigation: {
home: string;
about: string;
contact: string;
};
// 更多命名空间...
}
// 创建 i18n 实例
export const i18n = createI18n<I18nOptions<AppMessages>>({
legacy: false, // 使用 Composition API
locale: getBrowserLocale() || DEFAULT_LOCALE,
fallbackLocale: FALLBACK_LOCALE,
messages: {} as LocaleMessages<AppMessages>,
datetimeFormats: {
'zh-CN': {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}
},
'en-US': {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}
}
},
numberFormats: {
'zh-CN': {
currency: {
style: 'currency',
currency: 'CNY',
currencyDisplay: 'symbol'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}
},
'en-US': {
currency: {
style: 'currency',
currency: 'USD',
currencyDisplay: 'symbol'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}
}
}
});
// 工具函数
export function getBrowserLocale(): SupportedLocale | null {
const browserLang = navigator.language;
// 精确匹配
const exactMatch = supportedLocales.find(locale =>
locale.code === browserLang
);
if (exactMatch) return exactMatch.code;
// 语言代码匹配 (如 zh 匹配 zh-CN)
const languageMatch = supportedLocales.find(locale =>
locale.code.startsWith(browserLang.split('-')[0])
);
if (languageMatch) return languageMatch.code;
return null;
}
export function setLocale(locale: SupportedLocale): void {
i18n.global.locale.value = locale;
document.documentElement.lang = locale;
localStorage.setItem('user-locale', locale);
}
export function getCurrentLocale(): SupportedLocale {
return i18n.global.locale.value as SupportedLocale;
}
// 语言包加载器
export async function loadLocaleMessages(locale: SupportedLocale): Promise<void> {
// 如果已经加载,直接返回
if (i18n.global.getLocaleMessage(locale).common) {
return;
}
try {
const messages = await import(`@/locales/${locale}.json`);
i18n.global.setLocaleMessage(locale, messages.default);
} catch (error) {
console.error(`Failed to load locale messages for ${locale}:`, error);
// 回退到默认语言
if (locale !== FALLBACK_LOCALE) {
await loadLocaleMessages(FALLBACK_LOCALE);
setLocale(FALLBACK_LOCALE);
}
}
}
// 初始化语言设置
export async function setupI18n(): Promise<void> {
// 从本地存储或浏览器设置获取语言
const savedLocale = localStorage.getItem('user-locale') as SupportedLocale;
const locale = savedLocale || getBrowserLocale() || DEFAULT_LOCALE;
await loadLocaleMessages(locale);
setLocale(locale);
}
export default i18n;
四、语言包设计与管理
4.1 结构化语言包设计
// src/locales/zh-CN.json
{
"common": {
"buttons": {
"confirm": "确认",
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"submit": "提交",
"reset": "重置"
},
"messages": {
"success": "操作成功",
"error": "操作失败",
"warning": "警告",
"info": "信息",
"loading": "加载中...",
"noData": "暂无数据"
},
"validation": {
"required": "{field}为必填项",
"email": "请输入有效的邮箱地址",
"minLength": "{field}长度不能少于{min}个字符",
"maxLength": "{field}长度不能超过{max}个字符"
}
},
"navigation": {
"home": "首页",
"about": "关于我们",
"contact": "联系我们",
"products": "产品",
"services": "服务",
"pricing": "价格"
},
"auth": {
"login": "登录",
"logout": "退出登录",
"register": "注册",
"username": "用户名",
"password": "密码",
"forgotPassword": "忘记密码?"
},
"user": {
"profile": "个人资料",
"settings": "设置",
"dashboard": "仪表板",
"notifications": "通知",
"messages": "消息"
},
"errors": {
"network": "网络连接错误",
"timeout": "请求超时",
"serverError": "服务器错误",
"notFound": "页面未找到",
"unauthorized": "未授权访问"
}
}
4.2 英文语言包示例
// src/locales/en-US.json
{
"common": {
"buttons": {
"confirm": "Confirm",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"submit": "Submit",
"reset": "Reset"
},
"messages": {
"success": "Operation successful",
"error": "Operation failed",
"warning": "Warning",
"info": "Information",
"loading": "Loading...",
"noData": "No data available"
},
"validation": {
"required": "{field} is required",
"email": "Please enter a valid email address",
"minLength": "{field} must be at least {min} characters",
"maxLength": "{field} cannot exceed {max} characters"
}
},
"navigation": {
"home": "Home",
"about": "About Us",
"contact": "Contact",
"products": "Products",
"services": "Services",
"pricing": "Pricing"
}
}
4.3 高级消息格式配置
// 复数处理示例
{
"cart": {
"items": "购物车 | {count} 件商品",
"itemCount": "购物车中有 {count} 件商品",
"itemCount_plural": "购物车中有 {count} 件商品",
"itemCount_zero": "购物车为空"
}
}
// 性别处理示例
{
"user": {
"welcome": "{gender, select, male {欢迎先生} female {欢迎女士} other {欢迎}} {name}"
}
}
// 选择格式示例
{
"notification": {
"type": "{type, select, email {邮件通知} sms {短信通知} push {推送通知} other {其他通知}}"
}
}
五、Vue组件国际化实现
5.1 Composition API 使用方式
<template>
<div class="international-component">
<!-- 基础文本翻译 -->
<h1>{{ t('page.title') }}</h1>
<p>{{ t('page.description') }}</p>
<!-- 带参数的翻译 -->
<div class="user-info">
<p>{{ t('user.welcome', { name: userName, time: currentTime }) }}</p>
<p>{{ t('user.messageCount', { count: unreadMessages }) }}</p>
</div>
<!-- 数字格式化 -->
<div class="price-info">
<span>{{ n(productPrice, 'currency') }}</span>
<span>{{ t('product.discount', { discount: n(discountRate, 'percent') }) }}</span>
</div>
<!-- 日期时间格式化 -->
<div class="time-info">
<span>{{ d(createdAt, 'short') }}</span>
<span>{{ t('product.updatedAt', { time: d(updatedAt, 'relative') }) }}</span>
</div>
<!-- 复数处理 -->
<div class="cart-info">
<p>{{ tc('cart.items', itemCount) }}</p>
<p>{{ t('cart.totalItems', { count: itemCount }) }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { t, tc, n, d, locale } = useI18n();
// 响应式数据
const userName = ref('张三');
const unreadMessages = ref(5);
const productPrice = ref(2999.99);
const discountRate = ref(0.15);
const createdAt = ref(new Date('2024-01-15'));
const updatedAt = ref(new Date());
const itemCount = ref(3);
// 计算属性
const currentTime = computed(() => {
return new Intl.DateTimeFormat(locale.value, {
hour: '2-digit',
minute: '2-digit'
}).format(new Date());
});
// 监听语言变化
watch(locale, (newLocale) => {
console.log('语言切换至:', newLocale);
// 可以在这里执行语言相关的操作,如重新加载数据
});
</script>
<style scoped>
.international-component {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.user-info, .price-info, .time-info, .cart-info {
margin: 15px 0;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
</style>
5.2 Options API 使用方式
<template>
<div class="options-api-demo">
<!-- 基本用法 -->
<h1>{{ $t('page.title') }}</h1>
<!-- 命名空间用法 -->
<p>{{ $t('auth.login.title') }}</p>
<!-- 带参数 -->
<button @click="showNotification">
{{ $t('buttons.notify', { count: notificationCount }) }}
</button>
<!-- 复数形式 -->
<p>{{ $tc('messages.itemCount', itemCount) }}</p>
<!-- 数字格式化 -->
<p>{{ $n(1234.56, 'currency') }}</p>
<!-- 日期格式化 -->
<p>{{ $d(new Date(), 'short') }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'OptionsApiI18nDemo',
data() {
return {
notificationCount: 3,
itemCount: 5,
price: 1234.56,
currentDate: new Date()
};
},
computed: {
// 计算属性中使用翻译
welcomeMessage(): string {
return this.$t('user.welcome', { name: '张三' });
},
// 格式化价格
formattedPrice(): string {
return this.$n(this.price, 'currency');
}
},
methods: {
showNotification() {
// 方法中使用翻译
const message = this.$t('notifications.newMessage', {
count: this.notificationCount
});
alert(message);
},
// 动态加载语言包
async switchLanguage(locale: string) {
try {
// 加载新的语言包
const messages = await import(`@/locales/${locale}.json`);
this.$i18n.setLocaleMessage(locale, messages.default);
this.$i18n.locale = locale;
} catch (error) {
console.error('语言切换失败:', error);
}
}
},
mounted() {
// 监听语言变化
this.$watch('$i18n.locale', (newLocale: string) => {
console.log('语言已切换至:', newLocale);
this.updatePageTitle();
});
}
});
</script>
5.3 语言切换组件
<template>
<div class="language-switcher">
<div class="switcher-dropdown">
<button
class="switcher-trigger"
@click="toggleDropdown"
:aria-label="t('language.switcher')"
>
<span class="current-locale">
<span class="flag">{{ currentLocale.flag }}</span>
<span class="name">{{ currentLocale.name }}</span>
</span>
<span class="dropdown-icon">▼</span>
</button>
<transition name="dropdown-slide">
<div v-show="isOpen" class="dropdown-menu">
<div
v-for="locale in availableLocales"
:key="locale.code"
class="locale-option"
:class="{ active: isActiveLocale(locale.code) }"
@click="switchLocale(locale.code)"
>
<span class="flag">{{ locale.flag }}</span>
<span class="name">{{ locale.name }}</span>
<span v-if="isActiveLocale(locale.code)" class="checkmark">✓</span>
</div>
</div>
</transition>
</div>
<!-- 键盘导航提示 -->
<div class="keyboard-hint" v-if="isOpen">
{{ t('language.keyboardHint') }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { supportedLocales, type SupportedLocale, setLocale, loadLocaleMessages } from '@/plugins/i18n';
const { t, locale } = useI18n();
const isOpen = ref(false);
const focusedIndex = ref(-1);
// 计算属性
const currentLocale = computed(() => {
return supportedLocales.find(l => l.code === locale.value) || supportedLocales[0];
});
const availableLocales = computed(() => {
return supportedLocales.filter(l => l.code !== locale.value);
});
// 方法
const toggleDropdown = () => {
isOpen.value = !isOpen.value;
if (isOpen.value) {
focusedIndex.value = -1;
}
};
const isActiveLocale = (code: SupportedLocale) => {
return code === locale.value;
};
const switchLocale = async (newLocale: SupportedLocale) => {
if (newLocale === locale.value) return;
try {
// 显示加载状态
const loadingMessage = t('language.switching');
showLoadingIndicator(loadingMessage);
// 加载语言包
await loadLocaleMessages(newLocale);
// 设置新语言
setLocale(newLocale);
// 关闭下拉菜单
isOpen.value = false;
// 显示成功提示
showSuccessMessage(t('language.switched'));
} catch (error) {
console.error('语言切换失败:', error);
showErrorMessage(t('language.switchFailed'));
}
};
// 键盘导航支持
const handleKeydown = (event: KeyboardEvent) => {
if (!isOpen.value) return;
switch (event.key) {
case 'Escape':
isOpen.value = false;
event.preventDefault();
break;
case 'ArrowDown':
focusedIndex.value = (focusedIndex.value + 1) % availableLocales.value.length;
event.preventDefault();
break;
case 'ArrowUp':
focusedIndex.value = focusedIndex.value <= 0
? availableLocales.value.length - 1
: focusedIndex.value - 1;
event.preventDefault();
break;
case 'Enter':
case ' ':
if (focusedIndex.value >= 0) {
const locale = availableLocales.value[focusedIndex.value];
switchLocale(locale.code);
event.preventDefault();
}
break;
}
};
// 点击外部关闭
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.language-switcher')) {
isOpen.value = false;
}
};
// 生命周期
onMounted(() => {
document.addEventListener('keydown', handleKeydown);
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('click', handleClickOutside);
});
// 工具函数
const showLoadingIndicator = (message: string) => {
// 实现加载指示器
console.log('Loading:', message);
};
const showSuccessMessage = (message: string) => {
// 实现成功提示
console.log('Success:', message);
};
const showErrorMessage = (message: string) => {
// 实现错误提示
console.error('Error:', message);
};
</script>
<style scoped>
.language-switcher {
position: relative;
display: inline-block;
}
.switcher-dropdown {
position: relative;
}
.switcher-trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: white;
cursor: pointer;
transition: all 0.3s;
min-width: 120px;
}
.switcher-trigger:hover {
border-color: #c0c4cc;
}
.switcher-trigger:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.current-locale {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
}
.flag {
font-size: 16px;
}
.name {
font-size: 14px;
color: #606266;
}
.dropdown-icon {
color: #c0c4cc;
transition: transform 0.3s;
}
.switcher-dropdown.open .dropdown-icon {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
}
.locale-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.3s;
position: relative;
}
.locale-option:hover {
background-color: #f5f7fa;
}
.locale-option.active {
background-color: #ecf5ff;
color: #409eff;
}
.locale-option .checkmark {
margin-left: auto;
color: #409eff;
font-weight: bold;
}
/* 动画效果 */
.dropdown-slide-enter-active,
.dropdown-slide-leave-active {
transition: all 0.3s ease;
transform-origin: top center;
}
.dropdown-slide-enter-from {
opacity: 0;
transform: scaleY(0.8);
}
.dropdown-slide-leave-to {
opacity: 0;
transform: scaleY(0.8);
}
.keyboard-hint {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fdf6ec;
color: #e6a23c;
padding: 4px 8px;
font-size: 12px;
border-radius: 0 0 4px 4px;
margin-top: -1px;
border: 1px solid #f5dab1;
border-top: none;
}
</style>
六、高级特性与最佳实践
6.1 动态语言包加载
// src/utils/lazy-loading.ts
import type { SupportedLocale } from '@/plugins/i18n';
export class LocaleLoader {
private static loadedLocales = new Set<string>();
private static loadingPromises = new Map<string, Promise<void>>();
/**
* 预加载语言包
*/
static async preloadLocales(locales: SupportedLocale[]): Promise<void> {
const loadPromises = locales.map(locale => this.loadLocale(locale));
await Promise.allSettled(loadPromises);
}
/**
* 懒加载语言包
*/
static async loadLocale(locale: SupportedLocale): Promise<void> {
// 如果已经加载,直接返回
if (this.loadedLocales.has(locale)) {
return;
}
// 如果正在加载,返回现有的 Promise
if (this.loadingPromises.has(locale)) {
return this.loadingPromises.get(locale)!;
}
// 创建加载 Promise
const loadPromise = this.doLoadLocale(locale);
this.loadingPromises.set(locale, loadPromise);
try {
await loadPromise;
this.loadedLocales.add(locale);
this.loadingPromises.delete(locale);
} catch (error) {
this.loadingPromises.delete(locale);
throw error;
}
}
/**
* 执行语言包加载
*/
private static async doLoadLocale(locale: SupportedLocale): Promise<void> {
try {
// 动态导入语言包
const messages = await import(
/* webpackChunkName: "locale-[request]" */
`@/locales/${locale}.json`
);
// 设置到 i18n 实例
const { i18n } = await import('@/plugins/i18n');
i18n.global.setLocaleMessage(locale, messages.default);
console.log(`语言包加载成功: ${locale}`);
} catch (error) {
console.error(`语言包加载失败: ${locale}`, error);
throw new Error(`Failed to load locale: ${locale}`);
}
}
/**
* 获取已加载的语言列表
*/
static getLoadedLocales(): string[] {
return Array.from(this.loadedLocales);
}
/**
* 清理缓存(用于开发环境热重载)
*/
static clearCache(): void {
this.loadedLocales.clear();
this.loadingPromises.clear();
}
}
6.2 路由国际化配置
// src/router/i18n-routes.ts
import type { RouteRecordRaw } from 'vue-router';
import { useI18n } from 'vue-i18n';
/**
* 创建国际化路由配置
*/
export function createI18nRoutes(): RouteRecordRaw[] {
return [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
meta: {
i18n: {
title: 'navigation.home',
breadcrumb: 'home'
}
}
},
{
path: '/about',
name: 'about',
component: () => import('@/views/AboutView.vue'),
meta: {
i18n: {
title: 'navigation.about',
breadcrumb: 'about'
}
}
},
{
path: '/products',
name: 'products',
component: () => import('@/views/ProductsView.vue'),
meta: {
i18n: {
title: 'navigation.products',
breadcrumb: 'products'
}
},
children: [
{
path: ':id',
name: 'product-detail',
component: () => import('@/views/ProductDetail.vue'),
meta: {
i18n: {
title: 'products.detail',
breadcrumb: 'productDetail'
}
}
}
]
}
];
}
/**
* 路由守卫 - 语言处理
*/
export function setupI18nRouterGuard(router: any) {
router.beforeEach(async (to: any, from: any, next: any) => {
// 从路由参数中获取语言设置
const routeLocale = to.params.locale as string;
if (routeLocale) {
const { locale, t } = useI18n();
try {
// 切换语言
await LocaleLoader.loadLocale(routeLocale as SupportedLocale);
locale.value = routeLocale;
// 更新页面标题
if (to.meta.i18n?.title) {
document.title = t(to.meta.i18n.title);
}
} catch (error) {
console.error('路由语言处理失败:', error);
}
}
next();
});
}
/**
* 生成带语言前缀的路由
*/
export function createLocalizedPath(path: string, locale: string): string {
return `/${locale}${path.startsWith('/') ? path : `/${path}`}`;
}
6.3 类型安全的国际化
// src/types/i18n.d.ts
import type { AppMessages } from '@/plugins/i18n';
declare module 'vue-i18n' {
// 扩展 DefineLocaleMessage 以提供类型安全
export interface DefineLocaleMessage extends AppMessages {}
// 扩展自定义格式化函数类型
export interface CustomDateTimeFormat {
relative: {
style: 'long' | 'short' | 'narrow';
};
}
}
// 组件内类型安全的使用
import type { UseI18nOptions } from 'vue-i18n';
// 类型安全的翻译函数
export function createTypedT() {
const { t } = useI18n();
return {
// 基础翻译
t: t as <K extends keyof AppMessages>(key: K, params?: any) => string,
// 带默认值的翻译
td: <K extends keyof AppMessages>(
key: K,
defaultValue: string,
params?: any
) => string,
// 条件翻译
tc: <K extends keyof AppMessages>(
key: K,
choice: number,
params?: any
) => string
};
}
// 在组件中使用
export function useTypedI18n() {
const { locale, availableLocales, t, d, n } = useI18n();
const typedT = createTypedT();
return {
locale,
availableLocales,
t: typedT.t,
d,
n,
// 类型安全的翻译组合
translatePageTitle: (pageKey: keyof AppMessages['pages']) => {
return typedT.t(`pages.${pageKey}`);
},
// 类型安全的错误消息
translateError: (errorKey: keyof AppMessages['errors']) => {
return typedT.t(`errors.${errorKey}`);
}
};
}
七、实际应用案例
7.1 企业级管理系统国际化
<template>
<div class="enterprise-admin">
<!-- 顶部导航 -->
<header class="admin-header">
<nav class="main-nav">
<router-link
v-for="item in navItems"
:key="item.to"
:to="item.to"
class="nav-link"
>
{{ t(item.text) }}
</router-link>
</nav>
<div class="header-actions">
<!-- 语言切换 -->
<LanguageSwitcher />
<!-- 用户菜单 -->
<UserMenu />
</div>
</header>
<!-- 主内容区 -->
<main class="admin-main">
<!-- 面包屑导航 -->
<nav class="breadcrumb">
<span
v-for="(crumb, index) in breadcrumbs"
:key="index"
class="breadcrumb-item"
>
<router-link
v-if="crumb.to"
:to="crumb.to"
class="breadcrumb-link"
>
{{ t(crumb.text) }}
</router-link>
<span v-else class="breadcrumb-current">
{{ t(crumb.text) }}
</span>
<span v-if="index < breadcrumbs.length - 1" class="separator">/</span>
</span>
</nav>
<!-- 页面标题 -->
<h1 class="page-title">{{ pageTitle }}</h1>
<!-- 动态内容 -->
<router-view />
</main>
<!-- 全局通知 -->
<NotificationCenter />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
import UserMenu from '@/components/UserMenu.vue';
import NotificationCenter from '@/components/NotificationCenter.vue';
const { t } = useI18n();
const route = useRoute();
// 导航项配置
const navItems = computed(() => [
{ to: '/dashboard', text: 'navigation.dashboard' },
{ to: '/users', text: 'navigation.users' },
{ to: '/products', text: 'navigation.products' },
{ to: '/orders', text: 'navigation.orders' },
{ to: '/reports', text: 'navigation.reports' },
{ to: '/settings', text: 'navigation.settings' }
]);
// 面包屑导航
const breadcrumbs = computed(() => {
const crumbs: Array<{ text: string; to?: string }> = [];
const matched = route.matched;
matched.forEach((routeRecord, index) => {
if (routeRecord.meta?.i18n?.breadcrumb) {
crumbs.push({
text: routeRecord.meta.i18n.breadcrumb,
to: index < matched.length - 1 ? routeRecord.path : undefined
});
}
});
return crumbs;
});
// 页面标题
const pageTitle = computed(() => {
const i18nTitle = route.meta?.i18n?.title;
return i18nTitle ? t(i18nTitle) : 'Enterprise Admin';
});
</script>
<style scoped>
.enterprise-admin {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
height: 64px;
background: #001529;
color: white;
}
.main-nav {
display: flex;
gap: 32px;
}
.nav-link {
color: rgba(255, 255, 255, 0.65);
text-decoration: none;
padding: 8px 0;
transition: color 0.3s;
}
.nav-link:hover,
.nav-link.router-link-active {
color: white;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.admin-main {
flex: 1;
padding: 24px;
background: #f0f2f5;
}
.breadcrumb {
margin-bottom: 16px;
font-size: 14px;
}
.breadcrumb-item {
display: inline-flex;
align-items: center;
}
.breadcrumb-link {
color: #1890ff;
text-decoration: none;
}
.breadcrumb-link:hover {
color: #40a9ff;
}
.breadcrumb-current {
color: rgba(0, 0, 0, 0.45);
}
.separator {
margin: 0 8px;
color: rgba(0, 0, 0, 0.45);
}
.page-title {
margin: 0 0 24px 0;
font-size: 20px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
</style>
7.2 电商平台国际化
<template>
<div class="ecommerce-platform">
<!-- 产品列表 -->
<div class="products-grid">
<div
v-for="product in products"
:key="product.id"
class="product-card"
>
<img :src="product.image" :alt="product.name" class="product-image">
<div class="product-info">
<h3 class="product-name">{{ product.name }}</h3>
<p class="product-description">{{ product.description }}</p>
<div class="product-price">
<span class="current-price">
{{ n(product.price, 'currency') }}
</span>
<span v-if="product.originalPrice" class="original-price">
{{ n(product.originalPrice, 'currency') }}
</span>
<span v-if="product.discount" class="discount">
{{ t('product.save', { percent: product.discount }) }}
</span>
</div>
<div class="product-meta">
<span class="rating">
⭐ {{ product.rating }}
<span class="review-count">
({{ t('product.reviews', { count: product.reviewCount }) }})
</span>
</span>
<span class="stock" :class="{ low: product.stock < 10 }">
{{ t('product.stock', { count: product.stock }) }}
</span>
</div>
<div class="product-actions">
<button
class="add-to-cart-btn"
:disabled="product.stock === 0"
@click="addToCart(product)"
>
{{ product.stock > 0 ? t('buttons.addToCart') : t('product.outOfStock') }}
</button>
<button class="wishlist-btn" @click="addToWishlist(product)">
♡
</button>
</div>
</div>
</div>
</div>
<!-- 购物车侧边栏 -->
<CartSidebar
:items="cartItems"
:total="cartTotal"
@update-quantity="updateQuantity"
@remove-item="removeItem"
/>
<!-- 多语言搜索 -->
<div class="search-section">
<input
v-model="searchQuery"
:placeholder="t('search.placeholder')"
class="search-input"
@input="handleSearch"
>
<div class="search-filters">
<select v-model="sortBy" @change="handleSort" class="filter-select">
<option value="name">{{ t('filters.sortByName') }}</option>
<option value="price">{{ t('filters.sortByPrice') }}</option>
<option value="rating">{{ t('filters.sortByRating') }}</option>
</select>
<select v-model="priceRange" @change="handleFilter" class="filter-select">
<option value="all">{{ t('filters.allPrices') }}</option>
<option value="0-50">{{ t('filters.under50') }}</option>
<option value="50-100">{{ t('filters.50to100') }}</option>
<option value="100-500">{{ t('filters.100to500') }}</option>
<option value="500+">{{ t('filters.over500') }}</option>
</select>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import CartSidebar from '@/components/CartSidebar.vue';
const { t, n, locale } = useI18n();
// 产品数据
const products = ref([
{
id: 1,
name: 'product.names.smartphone',
description: 'product.descriptions.smartphone',
price: 599.99,
originalPrice: 699.99,
discount: 14,
rating: 4.5,
reviewCount: 1247,
stock: 25,
image: '/images/phone.jpg',
category: 'electronics'
},
// 更多产品...
]);
// 购物车
const cartItems = ref([]);
const searchQuery = ref('');
const sortBy = ref('name');
const priceRange = ref('all');
// 计算属性
const cartTotal = computed(() => {
return cartItems.value.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
});
const filteredProducts = computed(() => {
let filtered = products.value;
// 搜索过滤
if (searchQuery.value) {
filtered = filtered.filter(product =>
t(product.name).toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
// 价格范围过滤
if (priceRange.value !== 'all') {
const [min, max] = priceRange.value.split('-').map(Number);
filtered = filtered.filter(product => {
if (priceRange.value.endsWith('+')) {
return product.price >= min;
}
return product.price >= min && product.price <= max;
});
}
// 排序
filtered.sort((a, b) => {
switch (sortBy.value) {
case 'price':
return a.price - b.price;
case 'rating':
return b.rating - a.rating;
case 'name':
default:
return t(a.name).localeCompare(t(b.name), locale.value);
}
});
return filtered;
});
// 方法
const addToCart = (product: any) => {
const existingItem = cartItems.value.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity++;
} else {
cartItems.value.push({
...product,
quantity: 1
});
}
// 显示添加成功消息
showToast(t('cart.addSuccess', { product: t(product.name) }));
};
const updateQuantity = (productId: number, quantity: number) => {
const item = cartItems.value.find(item => item.id === productId);
if (item) {
item.quantity = quantity;
}
};
const removeItem = (productId: number) => {
cartItems.value = cartItems.value.filter(item => item.id !== productId);
};
const handleSearch = () => {
// 防抖搜索逻辑
console.log('Searching:', searchQuery.value);
};
const handleSort = () => {
console.log('Sorting by:', sortBy.value);
};
const handleFilter = () => {
console.log('Filtering by:', priceRange.value);
};
const showToast = (message: string) => {
// 实现 toast 通知
console.log('Toast:', message);
};
</script>
<style scoped>
.ecommerce-platform {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
transition: transform 0.3s, box-shadow 0.3s;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-info {
padding: 16px;
}
.product-name {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.product-description {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
line-height: 1.4;
}
.product-price {
margin-bottom: 12px;
}
.current-price {
font-size: 18px;
font-weight: 700;
color: #e53935;
}
.original-price {
font-size: 14px;
color: #999;
text-decoration: line-through;
margin-left: 8px;
}
.discount {
font-size: 12px;
color: #4caf50;
margin-left: 8px;
background: #e8f5e8;
padding: 2px 6px;
border-radius: 4px;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
font-size: 12px;
color: #666;
}
.rating {
display: flex;
align-items: center;
}
.review-count {
margin-left: 4px;
}
.stock.low {
color: #f44336;
font-weight: 600;
}
.product-actions {
display: flex;
gap: 8px;
}
.add-to-cart-btn {
flex: 1;
background: #ff5722;
color: white;
border: none;
padding: 10px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: background 0.3s;
}
.add-to-cart-btn:hover:not(:disabled) {
background: #e64a19;
}
.add-to-cart-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.wishlist-btn {
background: #f5f5f5;
border: none;
padding: 10px;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.wishlist-btn:hover {
background: #e0e0e0;
}
.search-section {
margin-bottom: 24px;
display: flex;
gap: 16px;
align-items: center;
}
.search-input {
flex: 1;
padding: 10px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.filter-select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
</style>
八、测试与质量保证
8.1 国际化测试策略
// tests/unit/i18n.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { createI18n } from 'vue-i18n';
import { nextTick } from 'vue';
describe('Vue I18n 测试套件', () => {
describe('语言包完整性测试', () => {
it('应该包含所有必需的语言键', async () => {
const zhMessages = await import('@/locales/zh-CN.json');
const enMessages = await import('@/locales/en-US.json');
// 检查共同键是否存在
const requiredKeys = [
'common.buttons.confirm',
'common.buttons.cancel',
'navigation.home',
'navigation.about',
'errors.network'
];
requiredKeys.forEach(key => {
expect(zhMessages.default).toHaveProperty(key);
expect(enMessages.default).toHaveProperty(key);
});
});
it('应该保持语言键结构一致', () => {
const compareStructure = (obj1: any, obj2: any, path: string = '') => {
for (const key in obj1) {
const currentPath = path ? `${path}.${key}` : key;
if (typeof obj1[key] === 'object') {
expect(obj2).toHaveProperty(key);
compareStructure(obj1[key], obj2[key], currentPath);
} else {
expect(obj2).toHaveProperty(key, `Missing key: ${currentPath}`);
}
}
};
// 实际测试中需要加载所有语言包进行比较
});
});
describe('组件国际化测试', () => {
let i18n: any;
beforeEach(() => {
i18n = createI18n({
legacy: false,
locale: 'en-US',
messages: {
'en-US': {
greeting: 'Hello, {name}!',
items: '{count} item | {count} items'
}
}
});
});
it('应该正确渲染翻译文本', async () => {
const wrapper = mount({
template: '<p>{{ $t("greeting", { name: "John" }) }}</p>',
i18n
});
await nextTick();
expect(wrapper.text()).toBe('Hello, John!');
});
it('应该处理复数形式', async () => {
const wrapper = mount({
template: '<p>{{ $tc("items", itemCount) }}</p>',
data() {
return { itemCount: 1 };
},
i18n
});
await nextTick();
expect(wrapper.text()).toBe('1 item');
await wrapper.setData({ itemCount: 5 });
await nextTick();
expect(wrapper.text()).toBe('5 items');
});
it('应该响应语言切换', async () => {
const wrapper = mount({
template: '<p>{{ $t("greeting") }}</p>',
i18n
});
// 切换语言
i18n.global.locale.value = 'zh-CN';
await nextTick();
// 验证语言切换生效
expect(i18n.global.locale.value).toBe('zh-CN');
});
});
describe('格式化功能测试', () => {
it('应该正确格式化数字', () => {
const i18n = createI18n({
legacy: false,
locale: 'en-US',
numberFormats: {
'en-US': {
currency: {
style: 'currency',
currency: 'USD'
}
}
}
});
const formatted = i18n.global.n(1234.56, 'currency');
expect(formatted).toMatch(/\$1,234\.56/);
});
it('应该正确格式化日期', () => {
const i18n = createI18n({
legacy: false,
locale: 'en-US',
datetimeFormats: {
'en-US': {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
}
}
}
});
const date = new Date('2024-01-15');
const formatted = i18n.global.d(date, 'short');
expect(formatted).toMatch(/Jan 15, 2024/);
});
});
});
8.2 端到端测试
// tests/e2e/i18n.spec.ts
import { test, expect } from '@playwright/test';
test.describe('国际化端到端测试', () => {
test('应该正确显示默认语言', async ({ page }) => {
await page.goto('/');
// 验证页面标题
await expect(page.locator('h1')).toContainText('欢迎');
// 验证导航菜单
await expect(page.locator('nav a').first()).toContainText('首页');
});
test('应该支持语言切换', async ({ page }) => {
await page.goto('/');
// 点击语言切换器
await page.click('.language-switcher');
// 选择英语
await page.click('text=English');
// 验证页面内容已切换为英文
await expect(page.locator('h1')).toContainText('Welcome');
await expect(page.locator('nav a').first()).toContainText('Home');
// 验证 URL 包含语言参数
await expect(page).toHaveURL(/.*locale=en-US/);
});
test('应该保持语言设置', async ({ page }) => {
// 第一次访问并设置语言
await page.goto('/');
await page.click('.language-switcher');
await page.click('text=English');
// 刷新页面验证设置保持
await page.reload();
await expect(page.locator('h1')).toContainText('Welcome');
// 导航到其他页面验证设置保持
await page.click('text=About');
await expect(page.locator('h1')).toContainText('About Us');
});
test('应该处理语言包加载失败', async ({ page }) => {
// 模拟语言包加载失败
await page.route('**/locales/fr-FR.json', route => {
route.fulfill({
status: 404,
body: 'Not Found'
});
});
await page.goto('/');
// 尝试切换到不存在的语言
await page.click('.language-switcher');
await page.click('text=Français');
// 验证回退到默认语言
await expect(page.locator('h1')).toContainText('欢迎');
// 验证显示错误提示
await expect(page.locator('.error-message')).toContainText('语言加载失败');
});
});
九、部署与优化
9.1 生产环境配置
// vite.config.prod.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueI18n from '@intlify/vite-plugin-vue-i18n';
import { resolve } from 'path';
export default defineConfig({
plugins: [
vue(),
vueI18n({
include: resolve(__dirname, './src/locales/**'),
compositionOnly: false,
runtimeOnly: false // 生产环境关闭运行时编译
})
],
build: {
target: 'es2015',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
// 按语言包分割代码
'i18n-zh': ['src/locales/zh-CN.json'],
'i18n-en': ['src/locales/en-US.json'],
'i18n-ja': ['src/locales/ja-JP.json'],
// 核心库
'vue-vendor': ['vue', 'vue-router', 'vue-i18n']
},
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
}
},
// CDN配置
base: process.env.NODE_ENV === 'production' ? '/
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)