第42天:Element Plus 国际化深入应用
学习目标
- 深入理解 Element Plus 国际化系统架构
- 掌握多语言配置和动态切换技术
- 学会创建自定义语言包和本地化策略
- 实现复杂场景下的国际化解决方案
- 优化国际化性能和用户体验
1. Element Plus 国际化系统深度解析
1.1 国际化架构设计
typescript
// 国际化系统架构
interface I18nSystem {
// 语言包管理
localeManager: LocaleManager
// 消息格式化
messageFormatter: MessageFormatter
// 日期时间格式化
dateTimeFormatter: DateTimeFormatter
// 数字格式化
numberFormatter: NumberFormatter
// 复数规则处理
pluralRules: PluralRules
// 语言检测
languageDetector: LanguageDetector
}
// 语言包管理器
class LocaleManager {
private locales: Map<string, LocaleData> = new Map()
private currentLocale: string = 'zh-CN'
private fallbackLocale: string = 'en'
private observers: Set<LocaleObserver> = new Set()
// 注册语言包
registerLocale(locale: string, data: LocaleData): void {
this.locales.set(locale, data)
this.notifyObservers('localeRegistered', { locale, data })
}
// 设置当前语言
setCurrentLocale(locale: string): void {
if (this.locales.has(locale)) {
const oldLocale = this.currentLocale
this.currentLocale = locale
this.notifyObservers('localeChanged', { oldLocale, newLocale: locale })
} else {
console.warn(`Locale ${locale} not found, falling back to ${this.fallbackLocale}`)
this.setCurrentLocale(this.fallbackLocale)
}
}
// 获取消息
getMessage(key: string, params?: Record<string, any>): string {
const locale = this.locales.get(this.currentLocale)
if (!locale) {
return this.getFallbackMessage(key, params)
}
const message = this.getNestedValue(locale.messages, key)
if (message === undefined) {
return this.getFallbackMessage(key, params)
}
return this.formatMessage(message, params)
}
// 获取嵌套值
private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
// 格式化消息
private formatMessage(template: string, params?: Record<string, any>): string {
if (!params) return template
return template.replace(/\{([^}]+)\}/g, (match, key) => {
const value = params[key]
return value !== undefined ? String(value) : match
})
}
// 获取回退消息
private getFallbackMessage(key: string, params?: Record<string, any>): string {
const fallbackLocale = this.locales.get(this.fallbackLocale)
if (!fallbackLocale) {
return key
}
const message = this.getNestedValue(fallbackLocale.messages, key)
return message ? this.formatMessage(message, params) : key
}
// 通知观察者
private notifyObservers(event: string, data: any): void {
this.observers.forEach(observer => {
observer.onLocaleEvent?.(event, data)
})
}
// 添加观察者
addObserver(observer: LocaleObserver): void {
this.observers.add(observer)
}
// 移除观察者
removeObserver(observer: LocaleObserver): void {
this.observers.delete(observer)
}
}
// 语言包数据结构
interface LocaleData {
name: string
messages: Record<string, any>
dateTime: DateTimeFormats
number: NumberFormats
currency: CurrencyFormats
pluralRule: (n: number) => string
}
// 日期时间格式
interface DateTimeFormats {
short: string
medium: string
long: string
full: string
dateOnly: string
timeOnly: string
relative: RelativeTimeFormats
}
// 相对时间格式
interface RelativeTimeFormats {
past: Record<string, string>
future: Record<string, string>
now: string
}
// 数字格式
interface NumberFormats {
decimal: Intl.NumberFormatOptions
currency: Intl.NumberFormatOptions
percent: Intl.NumberFormatOptions
scientific: Intl.NumberFormatOptions
}
// 货币格式
interface CurrencyFormats {
symbol: string
code: string
format: Intl.NumberFormatOptions
}
// 语言观察者
interface LocaleObserver {
onLocaleEvent?(event: string, data: any): void
}
1.2 高级消息格式化器
typescript
// 消息格式化器
class MessageFormatter {
private localeManager: LocaleManager
private interpolationRegex = /\{([^}]+)\}/g
private pluralRegex = /\{([^}]+)\s*,\s*plural\s*,\s*([^}]+)\}/g
private selectRegex = /\{([^}]+)\s*,\s*select\s*,\s*([^}]+)\}/g
constructor(localeManager: LocaleManager) {
this.localeManager = localeManager
}
// 格式化消息
format(key: string, params?: Record<string, any>): string {
let message = this.localeManager.getMessage(key)
if (!params) return message
// 处理复数
message = this.handlePlural(message, params)
// 处理选择
message = this.handleSelect(message, params)
// 处理插值
message = this.handleInterpolation(message, params)
return message
}
// 处理复数
private handlePlural(message: string, params: Record<string, any>): string {
return message.replace(this.pluralRegex, (match, variable, options) => {
const count = params[variable]
if (typeof count !== 'number') return match
const pluralOptions = this.parsePluralOptions(options)
const rule = this.getPluralRule(count)
return pluralOptions[rule] || pluralOptions.other || match
})
}
// 处理选择
private handleSelect(message: string, params: Record<string, any>): string {
return message.replace(this.selectRegex, (match, variable, options) => {
const value = params[variable]
if (value === undefined) return match
const selectOptions = this.parseSelectOptions(options)
return selectOptions[String(value)] || selectOptions.other || match
})
}
// 处理插值
private handleInterpolation(message: string, params: Record<string, any>): string {
return message.replace(this.interpolationRegex, (match, expression) => {
const value = this.evaluateExpression(expression, params)
return value !== undefined ? String(value) : match
})
}
// 解析复数选项
private parsePluralOptions(options: string): Record<string, string> {
const result: Record<string, string> = {}
const pairs = options.split(/\s+(?=\w+\s*{)/)
pairs.forEach(pair => {
const match = pair.match(/(\w+)\s*\{([^}]*)\}/)
if (match) {
result[match[1]] = match[2].trim()
}
})
return result
}
// 解析选择选项
private parseSelectOptions(options: string): Record<string, string> {
const result: Record<string, string> = {}
const pairs = options.split(/\s+(?=\w+\s*{)/)
pairs.forEach(pair => {
const match = pair.match(/(\w+)\s*\{([^}]*)\}/)
if (match) {
result[match[1]] = match[2].trim()
}
})
return result
}
// 获取复数规则
private getPluralRule(count: number): string {
const locale = this.localeManager.getCurrentLocale()
const localeData = this.localeManager.getLocaleData(locale)
if (localeData?.pluralRule) {
return localeData.pluralRule(count)
}
// 默认英语复数规则
return count === 1 ? 'one' : 'other'
}
// 计算表达式
private evaluateExpression(expression: string, params: Record<string, any>): any {
// 简单的表达式计算,支持基本的数学运算和函数调用
const trimmed = expression.trim()
// 直接变量引用
if (params.hasOwnProperty(trimmed)) {
return params[trimmed]
}
// 函数调用
const functionMatch = trimmed.match(/(\w+)\(([^)]*)\)/)
if (functionMatch) {
const [, funcName, args] = functionMatch
return this.callFunction(funcName, args, params)
}
// 数学表达式
try {
return this.evaluateMathExpression(trimmed, params)
} catch {
return undefined
}
}
// 调用函数
private callFunction(name: string, args: string, params: Record<string, any>): any {
const argValues = args.split(',').map(arg => {
const trimmed = arg.trim()
return params[trimmed] !== undefined ? params[trimmed] : trimmed
})
switch (name) {
case 'number':
return this.formatNumber(argValues[0], argValues[1])
case 'date':
return this.formatDate(argValues[0], argValues[1])
case 'currency':
return this.formatCurrency(argValues[0], argValues[1])
default:
return undefined
}
}
// 格式化数字
private formatNumber(value: any, format?: string): string {
const num = Number(value)
if (isNaN(num)) return String(value)
const locale = this.localeManager.getCurrentLocale()
const options = this.getNumberFormatOptions(format)
return new Intl.NumberFormat(locale, options).format(num)
}
// 格式化日期
private formatDate(value: any, format?: string): string {
const date = new Date(value)
if (isNaN(date.getTime())) return String(value)
const locale = this.localeManager.getCurrentLocale()
const options = this.getDateFormatOptions(format)
return new Intl.DateTimeFormat(locale, options).format(date)
}
// 格式化货币
private formatCurrency(value: any, currency?: string): string {
const num = Number(value)
if (isNaN(num)) return String(value)
const locale = this.localeManager.getCurrentLocale()
const options: Intl.NumberFormatOptions = {
style: 'currency',
currency: currency || 'USD'
}
return new Intl.NumberFormat(locale, options).format(num)
}
// 获取数字格式选项
private getNumberFormatOptions(format?: string): Intl.NumberFormatOptions {
switch (format) {
case 'percent':
return { style: 'percent' }
case 'scientific':
return { notation: 'scientific' }
default:
return { style: 'decimal' }
}
}
// 获取日期格式选项
private getDateFormatOptions(format?: string): Intl.DateTimeFormatOptions {
switch (format) {
case 'short':
return { dateStyle: 'short' }
case 'medium':
return { dateStyle: 'medium' }
case 'long':
return { dateStyle: 'long' }
case 'full':
return { dateStyle: 'full' }
case 'time':
return { timeStyle: 'medium' }
default:
return { dateStyle: 'medium', timeStyle: 'short' }
}
}
// 计算数学表达式
private evaluateMathExpression(expression: string, params: Record<string, any>): number {
// 替换变量
let expr = expression
Object.keys(params).forEach(key => {
const value = params[key]
if (typeof value === 'number') {
expr = expr.replace(new RegExp(`\\b${key}\\b`, 'g'), String(value))
}
})
// 简单的数学表达式计算(仅支持基本运算)
const sanitized = expr.replace(/[^0-9+\-*/().\s]/g, '')
return Function(`"use strict"; return (${sanitized})`)() as number
}
}
1.3 语言检测器
typescript
// 语言检测器
class LanguageDetector {
private detectionOrder: DetectionMethod[] = [
'querystring',
'cookie',
'localStorage',
'sessionStorage',
'navigator',
'htmlTag'
]
private options: DetectorOptions = {
order: this.detectionOrder,
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
lookupSessionStorage: 'i18nextLng',
caches: ['localStorage', 'cookie'],
excludeCacheFor: ['cimode']
}
// 检测语言
detect(): string | undefined {
for (const method of this.options.order) {
const detected = this.detectFromMethod(method)
if (detected) {
return this.normalizeLanguage(detected)
}
}
return undefined
}
// 从特定方法检测
private detectFromMethod(method: DetectionMethod): string | undefined {
switch (method) {
case 'querystring':
return this.detectFromQueryString()
case 'cookie':
return this.detectFromCookie()
case 'localStorage':
return this.detectFromLocalStorage()
case 'sessionStorage':
return this.detectFromSessionStorage()
case 'navigator':
return this.detectFromNavigator()
case 'htmlTag':
return this.detectFromHtmlTag()
default:
return undefined
}
}
// 从查询字符串检测
private detectFromQueryString(): string | undefined {
if (typeof window === 'undefined') return undefined
const params = new URLSearchParams(window.location.search)
return params.get(this.options.lookupQuerystring) || undefined
}
// 从Cookie检测
private detectFromCookie(): string | undefined {
if (typeof document === 'undefined') return undefined
const name = this.options.lookupCookie
const value = document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`))
?.split('=')[1]
return value ? decodeURIComponent(value) : undefined
}
// 从localStorage检测
private detectFromLocalStorage(): string | undefined {
if (typeof window === 'undefined' || !window.localStorage) return undefined
try {
return window.localStorage.getItem(this.options.lookupLocalStorage) || undefined
} catch {
return undefined
}
}
// 从sessionStorage检测
private detectFromSessionStorage(): string | undefined {
if (typeof window === 'undefined' || !window.sessionStorage) return undefined
try {
return window.sessionStorage.getItem(this.options.lookupSessionStorage) || undefined
} catch {
return undefined
}
}
// 从浏览器检测
private detectFromNavigator(): string | undefined {
if (typeof navigator === 'undefined') return undefined
const languages = navigator.languages || [navigator.language]
return languages[0]
}
// 从HTML标签检测
private detectFromHtmlTag(): string | undefined {
if (typeof document === 'undefined') return undefined
return document.documentElement.getAttribute('lang') || undefined
}
// 标准化语言代码
private normalizeLanguage(lang: string): string {
// 移除地区代码,只保留语言代码
const normalized = lang.toLowerCase().split('-')[0]
// 语言代码映射
const mapping: Record<string, string> = {
'zh': 'zh-CN',
'en': 'en-US',
'ja': 'ja-JP',
'ko': 'ko-KR',
'fr': 'fr-FR',
'de': 'de-DE',
'es': 'es-ES',
'it': 'it-IT',
'pt': 'pt-BR',
'ru': 'ru-RU'
}
return mapping[normalized] || lang
}
// 缓存语言设置
cacheUserLanguage(lng: string): void {
this.options.caches.forEach(cache => {
if (this.options.excludeCacheFor.includes(lng)) return
switch (cache) {
case 'localStorage':
this.cacheToLocalStorage(lng)
break
case 'sessionStorage':
this.cacheToSessionStorage(lng)
break
case 'cookie':
this.cacheToCookie(lng)
break
}
})
}
// 缓存到localStorage
private cacheToLocalStorage(lng: string): void {
if (typeof window === 'undefined' || !window.localStorage) return
try {
window.localStorage.setItem(this.options.lookupLocalStorage, lng)
} catch {
// 忽略错误
}
}
// 缓存到sessionStorage
private cacheToSessionStorage(lng: string): void {
if (typeof window === 'undefined' || !window.sessionStorage) return
try {
window.sessionStorage.setItem(this.options.lookupSessionStorage, lng)
} catch {
// 忽略错误
}
}
// 缓存到Cookie
private cacheToCookie(lng: string): void {
if (typeof document === 'undefined') return
const expires = new Date()
expires.setFullYear(expires.getFullYear() + 1)
document.cookie = `${this.options.lookupCookie}=${encodeURIComponent(lng)}; expires=${expires.toUTCString()}; path=/`
}
}
// 检测方法类型
type DetectionMethod = 'querystring' | 'cookie' | 'localStorage' | 'sessionStorage' | 'navigator' | 'htmlTag'
// 检测器选项
interface DetectorOptions {
order: DetectionMethod[]
lookupQuerystring: string
lookupCookie: string
lookupLocalStorage: string
lookupSessionStorage: string
caches: string[]
excludeCacheFor: string[]
}
2. 高级国际化组件实现
2.1 国际化提供者组件
vue
<!-- I18nProvider.vue - 国际化提供者 -->
<template>
<div class="i18n-provider">
<slot />
</div>
</template>
<script setup lang="ts">
import { provide, reactive, ref, onMounted, watch } from 'vue'
import { LocaleManager, MessageFormatter, LanguageDetector } from './i18nSystem'
import type { LocaleData } from './types'
// Props
interface Props {
locale?: string
fallbackLocale?: string
messages?: Record<string, LocaleData>
detectBrowserLanguage?: boolean
persistLanguage?: boolean
}
const props = withDefaults(defineProps<Props>(), {
locale: 'zh-CN',
fallbackLocale: 'en-US',
messages: () => ({}),
detectBrowserLanguage: true,
persistLanguage: true
})
// 创建国际化系统实例
const localeManager = new LocaleManager()
const messageFormatter = new MessageFormatter(localeManager)
const languageDetector = new LanguageDetector()
// 响应式状态
const state = reactive({
currentLocale: props.locale,
availableLocales: Object.keys(props.messages),
isLoading: false,
error: null as string | null
})
// 初始化
onMounted(async () => {
try {
state.isLoading = true
// 注册语言包
Object.entries(props.messages).forEach(([locale, data]) => {
localeManager.registerLocale(locale, data)
})
// 检测浏览器语言
let detectedLocale = props.locale
if (props.detectBrowserLanguage) {
const detected = languageDetector.detect()
if (detected && props.messages[detected]) {
detectedLocale = detected
}
}
// 设置当前语言
await setLocale(detectedLocale)
} catch (error) {
state.error = error instanceof Error ? error.message : 'Unknown error'
console.error('I18n initialization failed:', error)
} finally {
state.isLoading = false
}
})
// 设置语言
const setLocale = async (locale: string) => {
if (!props.messages[locale]) {
console.warn(`Locale ${locale} not found`)
return
}
state.currentLocale = locale
localeManager.setCurrentLocale(locale)
// 持久化语言设置
if (props.persistLanguage) {
languageDetector.cacheUserLanguage(locale)
}
// 更新HTML lang属性
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('lang', locale)
}
}
// 获取消息
const t = (key: string, params?: Record<string, any>): string => {
return messageFormatter.format(key, params)
}
// 获取复数消息
const tc = (key: string, count: number, params?: Record<string, any>): string => {
return messageFormatter.format(key, { ...params, count })
}
// 检查键是否存在
const te = (key: string): boolean => {
return localeManager.hasMessage(key)
}
// 获取当前语言
const getLocale = (): string => {
return state.currentLocale
}
// 获取可用语言列表
const getAvailableLocales = (): string[] => {
return state.availableLocales
}
// 监听语言变化
watch(
() => props.locale,
(newLocale) => {
if (newLocale !== state.currentLocale) {
setLocale(newLocale)
}
}
)
// 提供给子组件
provide('i18n', {
t,
tc,
te,
setLocale,
getLocale,
getAvailableLocales,
state: readonly(state)
})
// 暴露给父组件
defineExpose({
setLocale,
getLocale,
getAvailableLocales,
t,
tc,
te
})
</script>
<style scoped>
.i18n-provider {
height: 100%;
}
</style>
2.2 语言切换器组件
vue
<!-- LanguageSwitcher.vue - 语言切换器 -->
<template>
<div class="language-switcher">
<el-dropdown
:trigger="trigger"
:placement="placement"
@command="handleLanguageChange"
>
<el-button :type="buttonType" :size="size" class="language-trigger">
<el-icon v-if="showIcon"><Globe /></el-icon>
<span v-if="showText" class="language-text">
{{ currentLanguageDisplay }}
</span>
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu class="language-menu">
<div v-if="showSearch && availableLanguages.length > searchThreshold" class="language-search">
<el-input
v-model="searchQuery"
:placeholder="t('i18n.search')"
size="small"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="language-list">
<el-dropdown-item
v-for="lang in filteredLanguages"
:key="lang.code"
:command="lang.code"
:class="{
'is-active': lang.code === currentLocale,
'is-disabled': lang.disabled
}"
:disabled="lang.disabled"
>
<div class="language-item">
<div class="language-flag" v-if="showFlags">
<img
:src="lang.flag"
:alt="lang.name"
class="flag-icon"
v-if="lang.flag"
/>
<span v-else class="flag-placeholder">{{ lang.code.slice(0, 2).toUpperCase() }}</span>
</div>
<div class="language-info">
<div class="language-name">{{ lang.name }}</div>
<div v-if="showNativeName" class="language-native">{{ lang.nativeName }}</div>
</div>
<el-icon v-if="lang.code === currentLocale" class="check-icon">
<Check />
</el-icon>
</div>
</el-dropdown-item>
</div>
<el-divider v-if="showManageOption" />
<el-dropdown-item v-if="showManageOption" class="manage-languages" @click="showManageDialog = true">
<el-icon><Setting /></el-icon>
<span>{{ t('i18n.manageLanguages') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 语言管理对话框 -->
<LanguageManager
v-model="showManageDialog"
:available-languages="availableLanguages"
@update="handleLanguagesUpdate"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue'
import { ElMessage } from 'element-plus'
import { Globe, ArrowDown, Search, Check, Setting } from '@element-plus/icons-vue'
import LanguageManager from './LanguageManager.vue'
import type { LanguageInfo } from './types'
// Props
interface Props {
trigger?: 'hover' | 'click'
placement?: string
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text' | 'default'
size?: 'large' | 'default' | 'small'
showIcon?: boolean
showText?: boolean
showFlags?: boolean
showNativeName?: boolean
showSearch?: boolean
showManageOption?: boolean
searchThreshold?: number
languages?: LanguageInfo[]
}
const props = withDefaults(defineProps<Props>(), {
trigger: 'click',
placement: 'bottom-end',
buttonType: 'default',
size: 'default',
showIcon: true,
showText: true,
showFlags: true,
showNativeName: true,
showSearch: true,
showManageOption: false,
searchThreshold: 5,
languages: () => []
})
// Emits
interface Emits {
(e: 'change', locale: string): void
(e: 'error', error: string): void
}
const emit = defineEmits<Emits>()
// 注入国际化
const i18n = inject('i18n') as any
const { t, setLocale, getLocale, getAvailableLocales } = i18n
// 响应式数据
const searchQuery = ref('')
const showManageDialog = ref(false)
// 计算属性
const currentLocale = computed(() => getLocale())
const availableLanguages = computed(() => {
const locales = getAvailableLocales()
return props.languages.length > 0
? props.languages.filter(lang => locales.includes(lang.code))
: locales.map(code => ({
code,
name: getLanguageName(code),
nativeName: getNativeLanguageName(code),
flag: getLanguageFlag(code),
disabled: false
}))
})
const filteredLanguages = computed(() => {
if (!searchQuery.value) return availableLanguages.value
const query = searchQuery.value.toLowerCase()
return availableLanguages.value.filter(lang =>
lang.name.toLowerCase().includes(query) ||
lang.nativeName?.toLowerCase().includes(query) ||
lang.code.toLowerCase().includes(query)
)
})
const currentLanguageDisplay = computed(() => {
const current = availableLanguages.value.find(lang => lang.code === currentLocale.value)
return current?.name || currentLocale.value
})
// 事件处理
const handleLanguageChange = async (locale: string) => {
try {
await setLocale(locale)
emit('change', locale)
ElMessage.success(t('i18n.languageChanged', { language: getLanguageName(locale) }))
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
emit('error', message)
ElMessage.error(t('i18n.languageChangeError'))
}
}
const handleLanguagesUpdate = (languages: LanguageInfo[]) => {
// 处理语言列表更新
showManageDialog.value = false
}
// 辅助函数
const getLanguageName = (code: string): string => {
const names: Record<string, string> = {
'zh-CN': '简体中文',
'zh-TW': '繁體中文',
'en-US': 'English',
'ja-JP': '日本語',
'ko-KR': '한국어',
'fr-FR': 'Français',
'de-DE': 'Deutsch',
'es-ES': 'Español',
'it-IT': 'Italiano',
'pt-BR': 'Português',
'ru-RU': 'Русский',
'ar-SA': 'العربية',
'hi-IN': 'हिन्दी',
'th-TH': 'ไทย',
'vi-VN': 'Tiếng Việt'
}
return names[code] || code
}
const getNativeLanguageName = (code: string): string => {
const nativeNames: Record<string, string> = {
'zh-CN': '中文',
'zh-TW': '中文',
'en-US': 'English',
'ja-JP': '日本語',
'ko-KR': '한국어',
'fr-FR': 'Français',
'de-DE': 'Deutsch',
'es-ES': 'Español',
'it-IT': 'Italiano',
'pt-BR': 'Português',
'ru-RU': 'Русский',
'ar-SA': 'العربية',
'hi-IN': 'हिन्दी',
'th-TH': 'ไทย',
'vi-VN': 'Tiếng Việt'
}
return nativeNames[code] || code
}
const getLanguageFlag = (code: string): string => {
const flags: Record<string, string> = {
'zh-CN': '/flags/cn.svg',
'zh-TW': '/flags/tw.svg',
'en-US': '/flags/us.svg',
'ja-JP': '/flags/jp.svg',
'ko-KR': '/flags/kr.svg',
'fr-FR': '/flags/fr.svg',
'de-DE': '/flags/de.svg',
'es-ES': '/flags/es.svg',
'it-IT': '/flags/it.svg',
'pt-BR': '/flags/br.svg',
'ru-RU': '/flags/ru.svg',
'ar-SA': '/flags/sa.svg',
'hi-IN': '/flags/in.svg',
'th-TH': '/flags/th.svg',
'vi-VN': '/flags/vn.svg'
}
return flags[code] || ''
}
</script>
<style scoped>
.language-switcher {
display: inline-block;
}
.language-trigger {
display: flex;
align-items: center;
gap: 8px;
}
.language-text {
font-size: 14px;
}
.language-menu {
min-width: 200px;
max-width: 300px;
}
.language-search {
padding: 8px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.language-list {
max-height: 300px;
overflow-y: auto;
}
.language-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.language-flag {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 18px;
}
.flag-icon {
width: 24px;
height: 18px;
object-fit: cover;
border-radius: 2px;
}
.flag-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 18px;
background-color: var(--el-fill-color-light);
border-radius: 2px;
font-size: 10px;
font-weight: bold;
color: var(--el-text-color-secondary);
}
.language-info {
flex: 1;
min-width: 0;
}
.language-name {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
line-height: 1.2;
}
.language-native {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.2;
margin-top: 2px;
}
.check-icon {
color: var(--el-color-primary);
font-size: 16px;
}
.is-active .language-item {
color: var(--el-color-primary);
}
.is-active .language-name {
color: var(--el-color-primary);
font-weight: 600;
}
.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.manage-languages {
display: flex;
align-items: center;
gap: 8px;
color: var(--el-text-color-secondary);
}
.manage-languages:hover {
color: var(--el-color-primary);
}
</style>
实践练习
练习1:多语言表单验证
typescript
// 实现多语言表单验证消息
// 1. 创建验证规则国际化
// 2. 动态切换验证消息语言
// 3. 支持自定义验证消息
// 4. 处理复杂验证场景
练习2:RTL语言支持
scss
// 实现RTL(从右到左)语言支持
// 1. 自动检测RTL语言
// 2. 动态切换布局方向
// 3. 适配组件样式
// 4. 处理图标和动画
练习3:懒加载语言包
typescript
// 实现语言包懒加载
// 1. 按需加载语言包
// 2. 缓存已加载的语言包
// 3. 处理加载失败情况
// 4. 优化加载性能
练习4:国际化管理后台
vue
// 构建国际化管理系统
// 1. 语言包在线编辑
// 2. 翻译进度管理
// 3. 批量导入导出
// 4. 翻译质量检查
学习资源
官方文档
国际化标准
工具和服务
作业
- 国际化系统:实现完整的国际化管理系统
- 多语言表单:创建支持多语言的复杂表单
- RTL支持:实现阿拉伯语等RTL语言支持
- 性能优化:优化语言包加载和切换性能
- 翻译工具:开发翻译辅助工具
下一步学习
明天我们将学习「Element Plus 全局配置与命名空间」,包括:
- 全局配置系统
- 命名空间管理
- 样式隔离
- 配置继承机制