前端国际化最佳实践
随着Web应用的全球化发展,国际化(i18n)已成为现代前端开发的重要组成部分。本文将深入探讨前端国际化的实现方案、工具选择和最佳实践。
🌍 国际化基础概念
核心术语
- i18n (Internationalization):国际化,使应用支持多语言的过程
- l10n (Localization):本地化,为特定地区定制应用的过程
- Locale:地区标识符,如
zh-CN
、en-US
- RTL (Right-to-Left):从右到左的文字方向
国际化架构设计
javascript
// 国际化架构示例
const i18nArchitecture = {
// 语言检测
detection: {
browser: true, // 浏览器语言
localStorage: true, // 本地存储
cookie: true, // Cookie
subdomain: false, // 子域名
path: false // URL路径
},
// 资源加载
resources: {
lazy: true, // 懒加载
namespace: true, // 命名空间
fallback: 'en', // 回退语言
cache: true // 缓存策略
},
// 格式化
formatting: {
number: true, // 数字格式化
date: true, // 日期格式化
currency: true, // 货币格式化
plural: true // 复数规则
}
};
🛠️ 主流国际化方案
Vue.js 国际化 (Vue I18n)
javascript
// Vue I18n 配置
import { createI18n } from 'vue-i18n'
// 语言资源
const messages = {
en: {
nav: {
home: 'Home',
about: 'About',
contact: 'Contact'
},
user: {
profile: 'Profile',
settings: 'Settings',
logout: 'Logout'
},
message: {
hello: 'Hello {name}!',
welcome: 'Welcome to our website',
itemCount: 'No items | One item | {count} items'
}
},
zh: {
nav: {
home: '首页',
about: '关于',
contact: '联系我们'
},
user: {
profile: '个人资料',
settings: '设置',
logout: '退出登录'
},
message: {
hello: '你好 {name}!',
welcome: '欢迎访问我们的网站',
itemCount: '没有项目 | 一个项目 | {count} 个项目'
}
}
}
// 创建 i18n 实例
const i18n = createI18n({
locale: 'zh', // 默认语言
fallbackLocale: 'en', // 回退语言
messages,
// 数字格式化
numberFormats: {
en: {
currency: {
style: 'currency',
currency: 'USD'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2
}
},
zh: {
currency: {
style: 'currency',
currency: 'CNY'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2
}
}
},
// 日期格式化
datetimeFormats: {
en: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
}
},
zh: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
hour12: false
}
}
}
})
export default i18n
vue
<!-- Vue 组件中使用 -->
<template>
<div>
<!-- 基本翻译 -->
<h1>{{ $t('message.welcome') }}</h1>
<!-- 带参数的翻译 -->
<p>{{ $t('message.hello', { name: userName }) }}</p>
<!-- 复数处理 -->
<p>{{ $tc('message.itemCount', itemCount, { count: itemCount }) }}</p>
<!-- 数字格式化 -->
<p>{{ $n(price, 'currency') }}</p>
<!-- 日期格式化 -->
<p>{{ $d(new Date(), 'long') }}</p>
<!-- 语言切换 -->
<select v-model="$i18n.locale">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userName = ref('张三')
const itemCount = ref(5)
const price = ref(99.99)
</script>
React 国际化 (react-i18next)
javascript
// i18n 配置
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
// 语言资源
const resources = {
en: {
translation: {
nav: {
home: 'Home',
about: 'About',
contact: 'Contact'
},
user: {
profile: 'Profile',
settings: 'Settings'
},
message: {
hello: 'Hello {{name}}!',
welcome: 'Welcome to our website',
itemCount_zero: 'No items',
itemCount_one: 'One item',
itemCount_other: '{{count}} items'
}
}
},
zh: {
translation: {
nav: {
home: '首页',
about: '关于',
contact: '联系我们'
},
user: {
profile: '个人资料',
settings: '设置'
},
message: {
hello: '你好 {{name}}!',
welcome: '欢迎访问我们的网站',
itemCount_zero: '没有项目',
itemCount_one: '一个项目',
itemCount_other: '{{count}} 个项目'
}
}
}
}
i18n
.use(LanguageDetector) // 语言检测
.use(initReactI18next) // React 集成
.init({
resources,
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false // React 已经处理了 XSS
},
// 语言检测配置
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage']
}
})
export default i18n
jsx
// React 组件中使用
import React from 'react'
import { useTranslation } from 'react-i18next'
function App() {
const { t, i18n } = useTranslation()
const [userName] = useState('张三')
const [itemCount] = useState(5)
const changeLanguage = (lng) => {
i18n.changeLanguage(lng)
}
return (
<div>
{/* 基本翻译 */}
<h1>{t('message.welcome')}</h1>
{/* 带参数的翻译 */}
<p>{t('message.hello', { name: userName })}</p>
{/* 复数处理 */}
<p>{t('message.itemCount', { count: itemCount })}</p>
{/* 语言切换 */}
<select onChange={(e) => changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
)
}
// 高阶组件用法
import { withTranslation } from 'react-i18next'
class ClassComponent extends React.Component {
render() {
const { t } = this.props
return <h1>{t('message.welcome')}</h1>
}
}
export default withTranslation()(ClassComponent)
原生 JavaScript 国际化
javascript
// 轻量级 i18n 实现
class SimpleI18n {
constructor(options = {}) {
this.locale = options.locale || 'en'
this.fallbackLocale = options.fallbackLocale || 'en'
this.messages = options.messages || {}
this.formatters = this.initFormatters()
}
// 初始化格式化器
initFormatters() {
return {
number: new Intl.NumberFormat(this.locale),
currency: new Intl.NumberFormat(this.locale, {
style: 'currency',
currency: this.getCurrency()
}),
date: new Intl.DateTimeFormat(this.locale),
relativeTime: new Intl.RelativeTimeFormat(this.locale)
}
}
// 获取货币代码
getCurrency() {
const currencyMap = {
'en': 'USD',
'zh': 'CNY',
'ja': 'JPY',
'ko': 'KRW'
}
return currencyMap[this.locale.split('-')[0]] || 'USD'
}
// 翻译方法
t(key, params = {}) {
const message = this.getMessage(key)
return this.interpolate(message, params)
}
// 获取消息
getMessage(key) {
const keys = key.split('.')
let message = this.messages[this.locale]
// 尝试获取当前语言的消息
for (const k of keys) {
if (message && typeof message === 'object') {
message = message[k]
} else {
message = undefined
break
}
}
// 如果没找到,尝试回退语言
if (message === undefined) {
message = this.messages[this.fallbackLocale]
for (const k of keys) {
if (message && typeof message === 'object') {
message = message[k]
} else {
message = key // 最终回退到 key
break
}
}
}
return message || key
}
// 插值处理
interpolate(message, params) {
if (typeof message !== 'string') return message
return message.replace(/\{(\w+)\}/g, (match, key) => {
return params[key] !== undefined ? params[key] : match
})
}
// 复数处理
tc(key, count, params = {}) {
const rules = this.getPluralRules()
const rule = rules.select(count)
const pluralKey = `${key}_${rule}`
return this.t(pluralKey, { ...params, count })
}
// 获取复数规则
getPluralRules() {
return new Intl.PluralRules(this.locale)
}
// 数字格式化
n(number, format = 'number') {
return this.formatters[format]?.format(number) || number
}
// 日期格式化
d(date, options = {}) {
const formatter = new Intl.DateTimeFormat(this.locale, options)
return formatter.format(date)
}
// 切换语言
setLocale(locale) {
this.locale = locale
this.formatters = this.initFormatters()
}
}
// 使用示例
const i18n = new SimpleI18n({
locale: 'zh-CN',
fallbackLocale: 'en',
messages: {
en: {
greeting: 'Hello {name}!',
item_zero: 'No items',
item_one: 'One item',
item_other: '{count} items'
},
'zh-CN': {
greeting: '你好 {name}!',
item_zero: '没有项目',
item_one: '一个项目',
item_other: '{count} 个项目'
}
}
})
// 使用
console.log(i18n.t('greeting', { name: '张三' })) // 你好 张三!
console.log(i18n.tc('item', 5, { count: 5 })) // 5 个项目
console.log(i18n.n(1234.56, 'currency')) // ¥1,234.56
🎯 资源管理策略
文件组织结构
locales/
├── en/
│ ├── common.json # 通用翻译
│ ├── navigation.json # 导航相关
│ ├── forms.json # 表单相关
│ ├── errors.json # 错误信息
│ └── pages/
│ ├── home.json # 首页
│ ├── about.json # 关于页面
│ └── contact.json # 联系页面
├── zh-CN/
│ ├── common.json
│ ├── navigation.json
│ ├── forms.json
│ ├── errors.json
│ └── pages/
│ ├── home.json
│ ├── about.json
│ └── contact.json
└── index.js # 资源加载器
动态资源加载
javascript
// 资源加载器
class I18nResourceLoader {
constructor() {
this.cache = new Map()
this.loading = new Map()
}
// 加载语言资源
async loadLocale(locale) {
if (this.cache.has(locale)) {
return this.cache.get(locale)
}
if (this.loading.has(locale)) {
return this.loading.get(locale)
}
const loadPromise = this.fetchLocaleResources(locale)
this.loading.set(locale, loadPromise)
try {
const resources = await loadPromise
this.cache.set(locale, resources)
this.loading.delete(locale)
return resources
} catch (error) {
this.loading.delete(locale)
throw error
}
}
// 获取语言资源
async fetchLocaleResources(locale) {
const resources = {}
const modules = [
'common',
'navigation',
'forms',
'errors'
]
// 并行加载所有模块
const promises = modules.map(async (module) => {
try {
const response = await fetch(`/locales/${locale}/${module}.json`)
if (response.ok) {
resources[module] = await response.json()
}
} catch (error) {
console.warn(`Failed to load ${locale}/${module}:`, error)
}
})
await Promise.all(promises)
// 加载页面特定资源
const pageResources = await this.loadPageResources(locale)
resources.pages = pageResources
return resources
}
// 加载页面资源
async loadPageResources(locale) {
const pages = ['home', 'about', 'contact']
const pageResources = {}
for (const page of pages) {
try {
const response = await fetch(`/locales/${locale}/pages/${page}.json`)
if (response.ok) {
pageResources[page] = await response.json()
}
} catch (error) {
console.warn(`Failed to load page resource ${locale}/pages/${page}:`, error)
}
}
return pageResources
}
// 预加载资源
async preloadLocales(locales) {
const promises = locales.map(locale => this.loadLocale(locale))
return Promise.allSettled(promises)
}
// 清除缓存
clearCache(locale) {
if (locale) {
this.cache.delete(locale)
} else {
this.cache.clear()
}
}
}
// 使用示例
const resourceLoader = new I18nResourceLoader()
// 加载特定语言
const zhResources = await resourceLoader.loadLocale('zh-CN')
// 预加载多种语言
await resourceLoader.preloadLocales(['en', 'zh-CN', 'ja'])
命名空间管理
javascript
// 命名空间配置
const namespaceConfig = {
// 全局命名空间
global: {
common: ['button', 'label', 'message'],
validation: ['required', 'invalid', 'format'],
time: ['date', 'time', 'relative']
},
// 页面命名空间
pages: {
home: ['hero', 'features', 'testimonials'],
product: ['details', 'specs', 'reviews'],
checkout: ['cart', 'payment', 'shipping']
},
// 组件命名空间
components: {
header: ['navigation', 'user-menu', 'search'],
footer: ['links', 'social', 'copyright'],
modal: ['title', 'content', 'actions']
}
}
// 命名空间管理器
class NamespaceManager {
constructor(config) {
this.config = config
this.loadedNamespaces = new Set()
}
// 获取页面需要的命名空间
getPageNamespaces(page) {
const namespaces = ['global.common'] // 总是包含通用命名空间
// 添加页面特定命名空间
if (this.config.pages[page]) {
namespaces.push(`pages.${page}`)
}
return namespaces
}
// 获取组件需要的命名空间
getComponentNamespaces(components) {
const namespaces = []
components.forEach(component => {
if (this.config.components[component]) {
namespaces.push(`components.${component}`)
}
})
return namespaces
}
// 标记命名空间为已加载
markAsLoaded(namespace) {
this.loadedNamespaces.add(namespace)
}
// 检查命名空间是否已加载
isLoaded(namespace) {
return this.loadedNamespaces.has(namespace)
}
}
🌐 语言检测与切换
智能语言检测
javascript
// 语言检测器
class LanguageDetector {
constructor(options = {}) {
this.options = {
order: ['localStorage', 'cookie', 'navigator', 'htmlTag'],
lookupLocalStorage: 'i18nextLng',
lookupCookie: 'i18next',
caches: ['localStorage'],
...options
}
}
// 检测用户语言
detect() {
const detectors = {
localStorage: () => this.detectFromLocalStorage(),
cookie: () => this.detectFromCookie(),
navigator: () => this.detectFromNavigator(),
htmlTag: () => this.detectFromHtmlTag(),
subdomain: () => this.detectFromSubdomain(),
path: () => this.detectFromPath()
}
for (const method of this.options.order) {
const detected = detectors[method]?.()
if (detected) {
return this.normalizeLanguage(detected)
}
}
return null
}
// 从 localStorage 检测
detectFromLocalStorage() {
if (typeof window === 'undefined') return null
return localStorage.getItem(this.options.lookupLocalStorage)
}
// 从 Cookie 检测
detectFromCookie() {
if (typeof document === 'undefined') return null
const name = this.options.lookupCookie
const value = document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`))
?.split('=')[1]
return value ? decodeURIComponent(value) : null
}
// 从浏览器语言检测
detectFromNavigator() {
if (typeof navigator === 'undefined') return null
const languages = navigator.languages || [navigator.language]
return languages[0]
}
// 从 HTML 标签检测
detectFromHtmlTag() {
if (typeof document === 'undefined') return null
return document.documentElement.getAttribute('lang')
}
// 从子域名检测
detectFromSubdomain() {
if (typeof window === 'undefined') return null
const subdomain = window.location.hostname.split('.')[0]
const supportedLanguages = ['en', 'zh', 'ja', 'ko']
return supportedLanguages.includes(subdomain) ? subdomain : null
}
// 从路径检测
detectFromPath() {
if (typeof window === 'undefined') return null
const pathSegments = window.location.pathname.split('/')
const langSegment = pathSegments[1]
const supportedLanguages = ['en', 'zh-cn', 'ja', 'ko']
return supportedLanguages.includes(langSegment) ? langSegment : null
}
// 标准化语言代码
normalizeLanguage(lang) {
if (!lang) return null
// 语言映射表
const languageMap = {
'zh': 'zh-CN',
'zh-cn': 'zh-CN',
'zh-tw': 'zh-TW',
'en': 'en-US',
'en-us': 'en-US',
'en-gb': 'en-GB'
}
const normalized = lang.toLowerCase()
return languageMap[normalized] || lang
}
// 缓存语言选择
cacheUserLanguage(lng) {
if (this.options.caches.includes('localStorage')) {
localStorage.setItem(this.options.lookupLocalStorage, lng)
}
if (this.options.caches.includes('cookie')) {
document.cookie = `${this.options.lookupCookie}=${lng}; path=/; max-age=31536000`
}
}
}
// 使用示例
const detector = new LanguageDetector({
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage', 'cookie']
})
const detectedLanguage = detector.detect()
console.log('检测到的语言:', detectedLanguage)
语言切换组件
vue
<!-- Vue 语言切换器 -->
<template>
<div class="language-switcher">
<button
class="current-language"
@click="toggleDropdown"
:aria-expanded="isOpen"
>
<img :src="currentLanguage.flag" :alt="currentLanguage.name" />
<span>{{ currentLanguage.name }}</span>
<ChevronDownIcon :class="{ 'rotate-180': isOpen }" />
</button>
<transition name="dropdown">
<ul v-if="isOpen" class="language-dropdown">
<li
v-for="lang in availableLanguages"
:key="lang.code"
@click="switchLanguage(lang.code)"
:class="{ active: lang.code === currentLocale }"
>
<img :src="lang.flag" :alt="lang.name" />
<span>{{ lang.name }}</span>
<span class="native-name">{{ lang.nativeName }}</span>
</li>
</ul>
</transition>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const isOpen = ref(false)
const availableLanguages = [
{
code: 'en-US',
name: 'English',
nativeName: 'English',
flag: '/flags/us.svg'
},
{
code: 'zh-CN',
name: 'Chinese',
nativeName: '简体中文',
flag: '/flags/cn.svg'
},
{
code: 'ja',
name: 'Japanese',
nativeName: '日本語',
flag: '/flags/jp.svg'
},
{
code: 'ko',
name: 'Korean',
nativeName: '한국어',
flag: '/flags/kr.svg'
}
]
const currentLocale = computed(() => locale.value)
const currentLanguage = computed(() => {
return availableLanguages.find(lang => lang.code === currentLocale.value) || availableLanguages[0]
})
const toggleDropdown = () => {
isOpen.value = !isOpen.value
}
const switchLanguage = async (langCode) => {
if (langCode !== currentLocale.value) {
// 显示加载状态
const loadingToast = showLoadingToast('切换语言中...')
try {
// 切换语言
locale.value = langCode
// 缓存用户选择
localStorage.setItem('user-language', langCode)
// 更新 HTML lang 属性
document.documentElement.lang = langCode
// 更新页面标题和元数据
await updatePageMeta(langCode)
// 通知其他组件语言已切换
window.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language: langCode }
}))
} catch (error) {
console.error('语言切换失败:', error)
showErrorToast('语言切换失败,请重试')
} finally {
hideToast(loadingToast)
}
}
isOpen.value = false
}
// 点击外部关闭下拉菜单
const handleClickOutside = (event) => {
if (!event.target.closest('.language-switcher')) {
isOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.language-switcher {
position: relative;
display: inline-block;
}
.current-language {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.current-language:hover {
border-color: #3182ce;
}
.current-language img {
width: 20px;
height: 15px;
object-fit: cover;
}
.language-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 50;
max-height: 200px;
overflow-y: auto;
}
.language-dropdown li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.language-dropdown li:hover {
background-color: #f7fafc;
}
.language-dropdown li.active {
background-color: #ebf8ff;
color: #3182ce;
}
.language-dropdown img {
width: 20px;
height: 15px;
object-fit: cover;
}
.native-name {
margin-left: auto;
font-size: 0.875rem;
color: #718096;
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.rotate-180 {
transform: rotate(180deg);
}
</style>
📱 移动端适配
响应式设计
css
/* RTL 语言支持 */
[dir="rtl"] {
text-align: right;
}
[dir="rtl"] .nav-menu {
flex-direction: row-reverse;
}
[dir="rtl"] .breadcrumb::before {
content: "\\";
transform: scaleX(-1);
}
/* 多语言字体支持 */
.font-chinese {
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
.font-japanese {
font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", "Meiryo", sans-serif;
}
.font-korean {
font-family: "Malgun Gothic", "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
}
.font-arabic {
font-family: "Tahoma", "Arial Unicode MS", sans-serif;
}
/* 响应式语言切换器 */
@media (max-width: 768px) {
.language-switcher {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.language-dropdown {
bottom: 100%;
top: auto;
max-height: 150px;
}
}
移动端优化策略
javascript
// 移动端国际化优化
class MobileI18nOptimizer {
constructor() {
this.isMobile = this.detectMobile()
this.connectionType = this.getConnectionType()
}
// 检测移动设备
detectMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
}
// 获取网络连接类型
getConnectionType() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection
return connection?.effectiveType || 'unknown'
}
// 优化资源加载策略
getLoadingStrategy() {
if (!this.isMobile) {
return 'eager' // 桌面端积极加载
}
switch (this.connectionType) {
case 'slow-2g':
case '2g':
return 'minimal' // 最小化加载
case '3g':
return 'lazy' // 懒加载
case '4g':
default:
return 'normal' // 正常加载
}
}
// 根据策略加载语言资源
async loadLanguageResources(locale, strategy) {
const baseResources = ['common', 'navigation']
switch (strategy) {
case 'minimal':
// 只加载核心资源
return this.loadResources(locale, baseResources)
case 'lazy':
// 先加载基础资源,其他按需加载
const base = await this.loadResources(locale, baseResources)
this.preloadOtherResources(locale)
return base
case 'normal':
// 加载常用资源
return this.loadResources(locale, [...baseResources, 'forms', 'errors'])
case 'eager':
// 加载所有资源
return this.loadAllResources(locale)
}
}
}
🔧 性能优化
资源压缩与缓存
javascript
// 国际化资源优化器
class I18nResourceOptimizer {
constructor() {
this.compressionCache = new Map()
this.versionCache = new Map()
}
// 压缩语言资源
compressResources(resources) {
const compressed = {}
for (const [key, value] of Object.entries(resources)) {
if (typeof value === 'object') {
compressed[key] = this.compressResources(value)
} else if (typeof value === 'string') {
// 移除多余空格和换行
compressed[key] = value.trim().replace(/\s+/g, ' ')
} else {
compressed[key] = value
}
}
return compressed
}
// 资源版本控制
generateResourceHash(resources) {
const content = JSON.stringify(resources)
return this.simpleHash(content)
}
// 简单哈希函数
simpleHash(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // 转换为32位整数
}
return Math.abs(hash).toString(36)
}
// 智能缓存策略
async getCachedResources(locale, version) {
const cacheKey = `${locale}-${version}`
// 检查内存缓存
if (this.compressionCache.has(cacheKey)) {
return this.compressionCache.get(cacheKey)
}
// 检查 IndexedDB 缓存
const cached = await this.getFromIndexedDB(cacheKey)
if (cached) {
this.compressionCache.set(cacheKey, cached)
return cached
}
return null
}
// IndexedDB 操作
async getFromIndexedDB(key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('i18n-cache', 1)
request.onsuccess = (event) => {
const db = event.target.result
const transaction = db.transaction(['resources'], 'readonly')
const store = transaction.objectStore('resources')
const getRequest = store.get(key)
getRequest.onsuccess = () => {
resolve(getRequest.result?.data || null)
}
getRequest.onerror = () => {
resolve(null)
}
}
request.onerror = () => {
resolve(null)
}
})
}
async saveToIndexedDB(key, data) {
return new Promise((resolve) => {
const request = indexedDB.open('i18n-cache', 1)
request.onupgradeneeded = (event) => {
const db = event.target.result
if (!db.objectStoreNames.contains('resources')) {
db.createObjectStore('resources', { keyPath: 'key' })
}
}
request.onsuccess = (event) => {
const db = event.target.result
const transaction = db.transaction(['resources'], 'readwrite')
const store = transaction.objectStore('resources')
store.put({
key,
data,
timestamp: Date.now()
})
transaction.oncomplete = () => resolve(true)
transaction.onerror = () => resolve(false)
}
request.onerror = () => resolve(false)
})
}
}
懒加载实现
javascript
// 国际化懒加载管理器
class I18nLazyLoader {
constructor(i18n) {
this.i18n = i18n
this.loadingPromises = new Map()
this.observer = this.createIntersectionObserver()
}
// 创建交叉观察器
createIntersectionObserver() {
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target
const namespace = element.dataset.i18nNamespace
if (namespace) {
this.loadNamespace(namespace)
this.observer.unobserve(element)
}
}
})
}, {
rootMargin: '50px'
})
}
// 观察元素
observe(element, namespace) {
element.dataset.i18nNamespace = namespace
this.observer.observe(element)
}
// 加载命名空间
async loadNamespace(namespace) {
if (this.loadingPromises.has(namespace)) {
return this.loadingPromises.get(namespace)
}
const loadPromise = this.fetchNamespaceResources(namespace)
this.loadingPromises.set(namespace, loadPromise)
try {
const resources = await loadPromise
this.i18n.addResourceBundle(
this.i18n.language,
namespace,
resources,
true,
true
)
// 触发重新渲染
this.triggerUpdate(namespace)
} catch (error) {
console.error(`Failed to load namespace ${namespace}:`, error)
} finally {
this.loadingPromises.delete(namespace)
}
}
// 获取命名空间资源
async fetchNamespaceResources(namespace) {
const response = await fetch(`/locales/${this.i18n.language}/${namespace}.json`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
// 触发更新
triggerUpdate(namespace) {
// 发送自定义事件
window.dispatchEvent(new CustomEvent('i18n:namespaceLoaded', {
detail: { namespace }
}))
// 更新相关元素
const elements = document.querySelectorAll(`[data-i18n-namespace="${namespace}"]`)
elements.forEach(element => {
element.classList.add('i18n-loaded')
})
}
}
// 使用示例
const lazyLoader = new I18nLazyLoader(i18n)
// 观察需要懒加载的元素
document.querySelectorAll('[data-lazy-i18n]').forEach(element => {
const namespace = element.dataset.lazyI18n
lazyLoader.observe(element, namespace)
})
🎯 最佳实践总结
开发规范
键名规范
javascript// ✅ 好的键名 'user.profile.edit.title' 'form.validation.email.invalid' 'button.save.loading' // ❌ 避免的键名 'userProfileEditTitle' 'emailError' 'btn1'
翻译质量控制
javascript// 翻译验证器 class TranslationValidator { validate(translations) { const issues = [] // 检查缺失的翻译 const missingKeys = this.findMissingKeys(translations) if (missingKeys.length > 0) { issues.push({ type: 'missing', keys: missingKeys }) } // 检查参数不匹配 const parameterIssues = this.validateParameters(translations) issues.push(...parameterIssues) return issues } }
性能监控
javascript// 国际化性能监控 class I18nPerformanceMonitor { constructor() { this.metrics = { loadTime: new Map(), cacheHitRate: 0, translationCount: 0 } } trackLoadTime(locale, startTime) { const loadTime = Date.now() - startTime this.metrics.loadTime.set(locale, loadTime) } generateReport() { return { averageLoadTime: this.calculateAverageLoadTime(), cacheHitRate: this.metrics.cacheHitRate, totalTranslations: this.metrics.translationCount } } }
部署建议
CDN 配置
nginx# Nginx 配置示例 location /locales/ { expires 1y; add_header Cache-Control "public, immutable"; add_header Vary "Accept-Encoding"; # 启用 gzip 压缩 gzip on; gzip_types application/json; }
构建优化
javascript// Webpack 配置 module.exports = { plugins: [ new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }) ], optimization: { splitChunks: { cacheGroups: { i18n: { test: /[\\/]locales[\\/]/, name: 'i18n', chunks: 'all' } } } } }
🔮 未来趋势
AI 辅助翻译
javascript
// AI 翻译集成示例
class AITranslationHelper {
constructor(apiKey) {
this.apiKey = apiKey
}
async translateText(text, targetLanguage, context = '') {
const response = await fetch('/api/translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
text,
targetLanguage,
context,
preserveFormatting: true
})
})
return response.json()
}
async batchTranslate(texts, targetLanguage) {
// 批量翻译实现
}
}
实时协作翻译
javascript
// 实时翻译协作
class CollaborativeTranslation {
constructor(websocketUrl) {
this.ws = new WebSocket(websocketUrl)
this.setupEventHandlers()
}
setupEventHandlers() {
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
switch (data.type) {
case 'translation_updated':
this.handleTranslationUpdate(data)
break
case 'user_joined':
this.handleUserJoined(data)
break
}
}
}
updateTranslation(key, value, language) {
this.ws.send(JSON.stringify({
type: 'update_translation',
key,
value,
language,
timestamp: Date.now()
}))
}
}
前端国际化是一个复杂但重要的主题。通过合理的架构设计、工具选择和最佳实践,我们可以构建出支持多语言、高性能且易于维护的国际化应用。随着全球化的发展,掌握这些技能将变得越来越重要。
希望这份国际化最佳实践指南能帮助你构建出色的多语言Web应用!