Appearance
语言切换实现 - 完整的多语言切换解决方案
1. 语言切换概述
1.1 语言切换的核心需求
typescript
const languageSwitchRequirements = {
functionality: [
'流畅的语言切换',
'持久化用户选择',
'自动检测用户语言',
'支持URL语言参数',
'无刷新切换'
],
ux: [
'直观的UI设计',
'清晰的当前语言显示',
'快速访问切换器',
'移动端友好'
],
technical: [
'状态管理',
'路由集成',
'SEO优化',
'性能优化'
]
};1.2 常见实现方式
typescript
const implementations = {
dropdown: '下拉选择器 - 最常见',
flags: '国旗图标 - 视觉直观',
text: '文本链接 - 简洁明了',
modal: '弹窗选择 - 适合多语言',
automatic: '自动检测 - 无需手动'
};2. 基础实现
2.1 简单语言切换器
tsx
// LanguageSwitcher.tsx
import { useState } from 'react';
interface Language {
code: string;
name: string;
nativeName: string;
}
const languages: Language[] = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
{ code: 'ko', name: 'Korean', nativeName: '한국어' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
{ code: 'fr', name: 'French', nativeName: 'Français' }
];
export function SimpleLanguageSwitcher() {
const [currentLang, setCurrentLang] = useState('en');
const handleChange = (langCode: string) => {
setCurrentLang(langCode);
// 触发语言切换逻辑
changeLanguage(langCode);
};
return (
<select
value={currentLang}
onChange={(e) => handleChange(e.target.value)}
className="border rounded px-3 py-2"
>
{languages.map(lang => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
);
}2.2 下拉菜单样式
tsx
// DropdownLanguageSwitcher.tsx
import { useState, useRef, useEffect } from 'react';
export function DropdownLanguageSwitcher() {
const [isOpen, setIsOpen] = useState(false);
const [currentLang, setCurrentLang] = useState('en');
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const getCurrentLanguage = () =>
languages.find(l => l.code === currentLang);
const handleSelect = (langCode: string) => {
setCurrentLang(langCode);
setIsOpen(false);
changeLanguage(langCode);
};
return (
<div ref={dropdownRef} className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-4 py-2 border rounded hover:bg-gray-50"
>
<span>{getCurrentLanguage()?.nativeName}</span>
<svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}>
<path d="M7 10l5 5 5-5z" />
</svg>
</button>
{isOpen && (
<div className="absolute top-full left-0 mt-1 bg-white border rounded shadow-lg min-w-[200px] z-50">
{languages.map(lang => (
<button
key={lang.code}
onClick={() => handleSelect(lang.code)}
className={`w-full text-left px-4 py-2 hover:bg-gray-100 ${
lang.code === currentLang ? 'bg-blue-50 text-blue-600' : ''
}`}
>
<div className="font-medium">{lang.nativeName}</div>
<div className="text-sm text-gray-500">{lang.name}</div>
</button>
))}
</div>
)}
</div>
);
}2.3 国旗图标切换器
tsx
// FlagLanguageSwitcher.tsx
const languageFlags: Record<string, string> = {
'en': '🇺🇸',
'zh': '🇨🇳',
'ja': '🇯🇵',
'ko': '🇰🇷',
'es': '🇪🇸',
'fr': '🇫🇷',
'de': '🇩🇪',
'it': '🇮🇹'
};
export function FlagLanguageSwitcher() {
const [currentLang, setCurrentLang] = useState('en');
return (
<div className="flex gap-2">
{Object.entries(languageFlags).map(([code, flag]) => (
<button
key={code}
onClick={() => {
setCurrentLang(code);
changeLanguage(code);
}}
className={`text-2xl transition-opacity ${
code === currentLang ? 'opacity-100' : 'opacity-40 hover:opacity-70'
}`}
title={languages.find(l => l.code === code)?.name}
>
{flag}
</button>
))}
</div>
);
}3. React-i18next集成
3.1 基础集成
tsx
// LanguageSwitcher.tsx
import { useTranslation } from 'react-i18next';
export function I18nextLanguageSwitcher() {
const { i18n, t } = useTranslation();
const changeLanguage = async (lng: string) => {
await i18n.changeLanguage(lng);
// 保存到localStorage
localStorage.setItem('i18nextLng', lng);
// 更新HTML lang属性
document.documentElement.lang = lng;
// 更新文本方向
document.documentElement.dir = ['ar', 'he', 'fa'].includes(lng) ? 'rtl' : 'ltr';
};
return (
<select
value={i18n.language}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value="en">{t('languages.english')}</option>
<option value="zh">{t('languages.chinese')}</option>
<option value="ja">{t('languages.japanese')}</option>
</select>
);
}3.2 动态加载翻译
tsx
import { useTranslation } from 'react-i18next';
export function DynamicLanguageSwitcher() {
const { i18n } = useTranslation();
const [loading, setLoading] = useState(false);
const changeLanguage = async (lng: string) => {
setLoading(true);
try {
// 检查是否已加载
if (!i18n.hasResourceBundle(lng, 'translation')) {
// 动态加载翻译资源
const resources = await import(`../locales/${lng}/translation.json`);
i18n.addResourceBundle(lng, 'translation', resources.default);
}
await i18n.changeLanguage(lng);
} catch (error) {
console.error('Failed to load language:', error);
} finally {
setLoading(false);
}
};
return (
<div>
{loading && <div className="loader">Loading...</div>}
<select
value={i18n.language}
onChange={(e) => changeLanguage(e.target.value)}
disabled={loading}
>
{languages.map(lang => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
</div>
);
}4. React-Intl集成
4.1 Context实现
tsx
// IntlContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
import { IntlProvider } from 'react-intl';
interface IntlContextValue {
locale: string;
changeLocale: (locale: string) => void;
}
const IntlContext = createContext<IntlContextValue | null>(null);
export function IntlWrapper({ children }: { children: ReactNode }) {
const [locale, setLocale] = useState('en');
const [messages, setMessages] = useState<any>({});
const changeLocale = async (newLocale: string) => {
// 动态加载消息
const msgs = await import(`../translations/${newLocale}.json`);
setMessages(msgs.default);
setLocale(newLocale);
// 持久化
localStorage.setItem('locale', newLocale);
};
useEffect(() => {
const savedLocale = localStorage.getItem('locale') || 'en';
changeLocale(savedLocale);
}, []);
return (
<IntlContext.Provider value={{ locale, changeLocale }}>
<IntlProvider locale={locale} messages={messages}>
{children}
</IntlProvider>
</IntlContext.Provider>
);
}
export function useLocale() {
const context = useContext(IntlContext);
if (!context) throw new Error('useLocale must be used within IntlWrapper');
return context;
}
// 使用
export function IntlLanguageSwitcher() {
const { locale, changeLocale } = useLocale();
return (
<select value={locale} onChange={(e) => changeLocale(e.target.value)}>
{languages.map(lang => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
);
}5. Next.js集成
5.1 使用next-i18next
tsx
// components/LanguageSwitcher.tsx
import { useRouter } from 'next/router';
import Link from 'next/link';
export function NextLanguageSwitcher() {
const router = useRouter();
const { locales, locale: currentLocale, asPath } = router;
return (
<div className="flex gap-2">
{locales?.map(locale => (
<Link
key={locale}
href={asPath}
locale={locale}
className={locale === currentLocale ? 'font-bold' : ''}
>
{languages.find(l => l.code === locale)?.nativeName}
</Link>
))}
</div>
);
}
// 下拉样式
export function NextDropdownSwitcher() {
const router = useRouter();
const changeLanguage = (locale: string) => {
router.push(router.asPath, router.asPath, { locale });
};
return (
<select
value={router.locale}
onChange={(e) => changeLanguage(e.target.value)}
>
{router.locales?.map(locale => (
<option key={locale} value={locale}>
{languages.find(l => l.code === locale)?.nativeName}
</option>
))}
</select>
);
}5.2 动态路由处理
tsx
// 保留查询参数和hash
export function NextAdvancedSwitcher() {
const router = useRouter();
const changeLanguage = async (locale: string) => {
const { pathname, asPath, query } = router;
// 保留当前路径和查询参数
await router.push(
{ pathname, query },
asPath,
{ locale }
);
};
return (
<select
value={router.locale}
onChange={(e) => changeLanguage(e.target.value)}
>
{router.locales?.map(locale => (
<option key={locale} value={locale}>
{locale.toUpperCase()}
</option>
))}
</select>
);
}6. 持久化存储
6.1 LocalStorage存储
typescript
// utils/languageStorage.ts
const LANGUAGE_KEY = 'preferred_language';
export const languageStorage = {
get: (): string | null => {
try {
return localStorage.getItem(LANGUAGE_KEY);
} catch {
return null;
}
},
set: (language: string): void => {
try {
localStorage.setItem(LANGUAGE_KEY, language);
} catch (error) {
console.error('Failed to save language:', error);
}
},
remove: (): void => {
try {
localStorage.removeItem(LANGUAGE_KEY);
} catch (error) {
console.error('Failed to remove language:', error);
}
}
};
// 使用
export function PersistentLanguageSwitcher() {
const [language, setLanguage] = useState(() =>
languageStorage.get() || 'en'
);
const handleChange = (newLang: string) => {
setLanguage(newLang);
languageStorage.set(newLang);
changeLanguage(newLang);
};
return (
<select value={language} onChange={(e) => handleChange(e.target.value)}>
{languages.map(lang => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
);
}6.2 Cookie存储
typescript
// utils/languageCookie.ts
export const languageCookie = {
get: (): string | null => {
const match = document.cookie.match(/(?:^|;\s*)language=([^;]*)/);
return match ? decodeURIComponent(match[1]) : null;
},
set: (language: string, days: number = 365): void => {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `language=${encodeURIComponent(language)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
},
remove: (): void => {
document.cookie = 'language=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
}
};7. 自动语言检测
7.1 浏览器语言检测
typescript
// utils/detectLanguage.ts
export function detectBrowserLanguage(
supportedLanguages: string[]
): string {
// 检查navigator.languages
if (navigator.languages) {
for (const lang of navigator.languages) {
const simpleLang = lang.split('-')[0];
if (supportedLanguages.includes(simpleLang)) {
return simpleLang;
}
}
}
// 回退到navigator.language
const browserLang = navigator.language.split('-')[0];
if (supportedLanguages.includes(browserLang)) {
return browserLang;
}
// 默认语言
return 'en';
}
// 使用
export function AutoDetectLanguageSwitcher() {
const supportedLanguages = ['en', 'zh', 'ja', 'ko'];
const [language, setLanguage] = useState(() => {
// 优先使用已保存的语言
const saved = languageStorage.get();
if (saved && supportedLanguages.includes(saved)) {
return saved;
}
// 自动检测
return detectBrowserLanguage(supportedLanguages);
});
useEffect(() => {
changeLanguage(language);
}, [language]);
return (
<select value={language} onChange={(e) => {
setLanguage(e.target.value);
languageStorage.set(e.target.value);
}}>
{languages.filter(l => supportedLanguages.includes(l.code)).map(lang => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
);
}7.2 地理位置检测
typescript
// 基于IP的语言检测
async function detectLanguageByIP(): Promise<string> {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
const countryToLanguage: Record<string, string> = {
'US': 'en',
'CN': 'zh',
'JP': 'ja',
'KR': 'ko',
'ES': 'es',
'FR': 'fr'
};
return countryToLanguage[data.country_code] || 'en';
} catch {
return 'en';
}
}8. URL参数集成
8.1 查询参数
tsx
// 从URL读取语言
export function URLLanguageSwitcher() {
const [searchParams, setSearchParams] = useSearchParams();
const language = searchParams.get('lang') || 'en';
const changeLanguage = (newLang: string) => {
setSearchParams({ lang: newLang });
// 同时更新应用语言
i18n.changeLanguage(newLang);
};
return (
<select value={language} onChange={(e) => changeLanguage(e.target.value)}>
{languages.map(lang => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
);
}8.2 路径前缀
tsx
// /en/about, /zh/about
import { useParams, useNavigate } from 'react-router-dom';
export function PathPrefixLanguageSwitcher() {
const { lang } = useParams<{ lang: string }>();
const navigate = useNavigate();
const changeLanguage = (newLang: string) => {
const currentPath = window.location.pathname;
const newPath = currentPath.replace(`/${lang}/`, `/${newLang}/`);
navigate(newPath);
};
return (
<select value={lang} onChange={(e) => changeLanguage(e.target.value)}>
{languages.map(l => (
<option key={l.code} value={l.code}>
{l.nativeName}
</option>
))}
</select>
);
}9. 移动端适配
9.1 移动端菜单
tsx
// MobileLanguageSwitcher.tsx
export function MobileLanguageSwitcher() {
const [isOpen, setIsOpen] = useState(false);
const { i18n } = useTranslation();
return (
<>
<button
onClick={() => setIsOpen(true)}
className="flex items-center gap-2 p-2"
>
<GlobeIcon className="w-5 h-5" />
<span className="text-sm">
{languages.find(l => l.code === i18n.language)?.nativeName}
</span>
</button>
{isOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50">
<div className="fixed bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold">选择语言</h3>
<button onClick={() => setIsOpen(false)}>
<CloseIcon />
</button>
</div>
<div className="space-y-2">
{languages.map(lang => (
<button
key={lang.code}
onClick={() => {
i18n.changeLanguage(lang.code);
setIsOpen(false);
}}
className={`w-full text-left p-3 rounded ${
lang.code === i18n.language ? 'bg-blue-50' : ''
}`}
>
<div className="font-medium">{lang.nativeName}</div>
<div className="text-sm text-gray-500">{lang.name}</div>
</button>
))}
</div>
</div>
</div>
)}
</>
);
}10. SEO优化
10.1 hreflang标签
tsx
// SEOLanguageLinks.tsx
import Head from 'next/head';
import { useRouter } from 'next/router';
export function SEOLanguageLinks() {
const router = useRouter();
const baseUrl = 'https://example.com';
return (
<Head>
{router.locales?.map(locale => (
<link
key={locale}
rel="alternate"
hrefLang={locale}
href={`${baseUrl}/${locale}${router.asPath}`}
/>
))}
<link
rel="alternate"
hrefLang="x-default"
href={`${baseUrl}${router.asPath}`}
/>
</Head>
);
}11. 性能优化
11.1 预加载翻译
typescript
// 预加载其他语言
useEffect(() => {
const preloadLanguages = ['zh', 'ja'];
preloadLanguages.forEach(async (lang) => {
if (lang !== i18n.language) {
try {
await import(`../locales/${lang}/translation.json`);
} catch (error) {
console.error(`Failed to preload ${lang}:`, error);
}
}
});
}, []);11.2 懒加载策略
typescript
// 只在需要时加载
const loadLanguage = async (lang: string) => {
if (!loadedLanguages.has(lang)) {
const module = await import(`../locales/${lang}.json`);
i18n.addResourceBundle(lang, 'translation', module.default);
loadedLanguages.add(lang);
}
};12. 最佳实践
typescript
const bestPractices = {
ux: [
'提供清晰的当前语言指示',
'使用母语显示语言名称',
'保持UI一致性',
'考虑移动端体验'
],
technical: [
'持久化用户选择',
'自动检测用户语言',
'支持URL参数',
'处理RTL语言'
],
seo: [
'使用hreflang标签',
'正确的URL结构',
'语言切换保持页面内容',
'sitemap包含所有语言'
],
performance: [
'延迟加载翻译',
'预加载常用语言',
'缓存翻译资源',
'优化切换速度'
]
};14. 高级语言切换策略
14.1 智能语言推荐
tsx
// 基于用户行为的语言推荐
function useLanguageRecommendation() {
const [recommendedLang, setRecommendedLang] = useState<string | null>(null);
useEffect(() => {
const factors = {
browserLang: navigator.language,
geoLocation: getUserGeoLocation(),
previousVisits: getVisitHistory(),
contentPreference: getContentPreference()
};
// 智能分析
const recommended = analyzeLanguagePreference(factors);
if (recommended !== getCurrentLanguage()) {
setRecommendedLang(recommended);
}
}, []);
return recommendedLang;
}
// 语言推荐提示
function LanguageRecommendationBanner() {
const recommended = useLanguageRecommendation();
const { switchLanguage } = useLanguage();
if (!recommended) return null;
return (
<div className="language-recommendation">
<p>我们注意到您可能更喜欢 {getLanguageName(recommended)}</p>
<button onClick={() => switchLanguage(recommended)}>
切换到 {getLanguageName(recommended)}
</button>
<button onClick={() => dismissRecommendation()}>
保持当前语言
</button>
</div>
);
}14.2 多语言A/B测试
tsx
// 语言切换效果A/B测试
import { useABTest } from './useABTest';
function LanguageSwitcher() {
const variant = useABTest('language-switcher', {
variants: ['dropdown', 'flags', 'text-only'],
weights: [0.4, 0.3, 0.3]
});
// 跟踪切换行为
const trackLanguageSwitch = (newLang: string) => {
analytics.track('language_switched', {
from: currentLanguage,
to: newLang,
variant,
timestamp: Date.now()
});
};
switch (variant) {
case 'dropdown':
return <DropdownLanguageSwitcher onSwitch={trackLanguageSwitch} />;
case 'flags':
return <FlagLanguageSwitcher onSwitch={trackLanguageSwitch} />;
case 'text-only':
return <TextLanguageSwitcher onSwitch={trackLanguageSwitch} />;
}
}14.3 语言回退策略
tsx
// 多级语言回退
const languageFallbacks = {
'zh-CN': ['zh-TW', 'zh', 'en'],
'zh-TW': ['zh-CN', 'zh', 'en'],
'en-US': ['en-GB', 'en'],
'en-GB': ['en-US', 'en'],
'fr-CA': ['fr-FR', 'fr', 'en'],
'es-MX': ['es-ES', 'es', 'en']
};
async function loadMessagesWithFallback(locale: string) {
const fallbackChain = [locale, ...(languageFallbacks[locale] || ['en'])];
for (const lang of fallbackChain) {
try {
const messages = await import(`./locales/${lang}.json`);
console.log(`Loaded messages for ${lang}`);
return messages.default;
} catch (error) {
console.warn(`Failed to load ${lang}, trying fallback...`);
}
}
throw new Error('No language file available');
}15. 性能优化深度实践
15.1 翻译文件预加载
tsx
// 预加载常用语言
function preloadCommonLanguages() {
const commonLanguages = ['en', 'zh', 'es', 'fr'];
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
commonLanguages.forEach(lang => {
import(`./locales/${lang}.json`);
});
});
}
}
// 在应用启动时调用
useEffect(() => {
preloadCommonLanguages();
}, []);15.2 翻译缓存优化
tsx
// 持久化翻译缓存
class TranslationCache {
private cache: Map<string, any> = new Map();
private dbName = 'translation-cache';
private version = 1;
async init() {
// 从IndexedDB恢复缓存
const stored = await this.loadFromDB();
if (stored) {
this.cache = new Map(Object.entries(stored));
}
}
async get(locale: string) {
if (this.cache.has(locale)) {
return this.cache.get(locale);
}
const messages = await this.fetchMessages(locale);
this.cache.set(locale, messages);
await this.saveToDB();
return messages;
}
private async loadFromDB() {
return new Promise((resolve) => {
const request = indexedDB.open(this.dbName, this.version);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction('translations', 'readonly');
const store = tx.objectStore('translations');
const getRequest = store.get('cache');
getRequest.onsuccess = () => resolve(getRequest.result);
};
});
}
private async saveToDB() {
return new Promise((resolve) => {
const request = indexedDB.open(this.dbName, this.version);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction('translations', 'readwrite');
const store = tx.objectStore('translations');
store.put(Object.fromEntries(this.cache), 'cache');
tx.oncomplete = () => resolve(true);
};
});
}
}15.3 按需加载翻译模块
tsx
// 模块化翻译加载
const translationModules = {
common: () => import('./locales/common'),
dashboard: () => import('./locales/dashboard'),
products: () => import('./locales/products'),
checkout: () => import('./locales/checkout')
};
function useModuleTranslations(modules: string[]) {
const [translations, setTranslations] = useState({});
const { locale } = useLanguage();
useEffect(() => {
Promise.all(
modules.map(module =>
translationModules[module]().then(m => m[locale])
)
).then(results => {
const merged = Object.assign({}, ...results);
setTranslations(merged);
});
}, [modules, locale]);
return translations;
}
// 使用
function Dashboard() {
const t = useModuleTranslations(['common', 'dashboard']);
return <div>{t['dashboard.title']}</div>;
}16. 企业级实践
16.1 翻译管理平台集成
tsx
// 与Crowdin/Lokalise等平台集成
import { CrowdinClient } from '@crowdin/crowdin-api-client';
class TranslationManagement {
private client: CrowdinClient;
constructor(apiKey: string) {
this.client = new CrowdinClient({ token: apiKey });
}
async syncTranslations(projectId: number) {
// 1. 上传源文件
await this.uploadSourceFiles(projectId);
// 2. 拉取翻译
const translations = await this.downloadTranslations(projectId);
// 3. 保存到本地
await this.saveTranslations(translations);
return translations;
}
async uploadSourceFiles(projectId: number) {
const sourceFiles = await this.extractSourceMessages();
for (const [filename, content] of Object.entries(sourceFiles)) {
await this.client.sourceFilesApi.createFile(projectId, {
name: filename,
content: Buffer.from(JSON.stringify(content)).toString('base64')
});
}
}
async downloadTranslations(projectId: number) {
const build = await this.client.translationsApi.buildProject(projectId);
// 等待构建完成
await this.waitForBuild(projectId, build.data.id);
// 下载翻译文件
const download = await this.client.translationsApi.downloadTranslations(
projectId,
build.data.id
);
return download.data;
}
}16.2 翻译质量保证
typescript
// 自动翻译质量检查
interface QualityCheck {
missingKeys: string[];
emptyTranslations: string[];
inconsistentVariables: string[];
lengthIssues: Array<{ key: string; ratio: number }>;
}
function checkTranslationQuality(
source: Record<string, string>,
target: Record<string, string>
): QualityCheck {
const issues: QualityCheck = {
missingKeys: [],
emptyTranslations: [],
inconsistentVariables: [],
lengthIssues: []
};
// 检查缺失的key
Object.keys(source).forEach(key => {
if (!(key in target)) {
issues.missingKeys.push(key);
}
});
// 检查空翻译
Object.entries(target).forEach(([key, value]) => {
if (!value || value.trim() === '') {
issues.emptyTranslations.push(key);
}
});
// 检查变量一致性
Object.entries(source).forEach(([key, sourceText]) => {
const targetText = target[key];
if (!targetText) return;
const sourceVars = sourceText.match(/\{[^}]+\}/g) || [];
const targetVars = targetText.match(/\{[^}]+\}/g) || [];
if (sourceVars.length !== targetVars.length) {
issues.inconsistentVariables.push(key);
}
});
// 检查长度问题
Object.entries(source).forEach(([key, sourceText]) => {
const targetText = target[key];
if (!targetText) return;
const ratio = targetText.length / sourceText.length;
if (ratio > 2 || ratio < 0.3) {
issues.lengthIssues.push({ key, ratio });
}
});
return issues;
}
// 使用
const qualityReport = checkTranslationQuality(
enMessages,
zhMessages
);
if (qualityReport.missingKeys.length > 0) {
console.error('Missing translations:', qualityReport.missingKeys);
}16.3 翻译版本管理
typescript
// Git-based翻译版本控制
import simpleGit from 'simple-git';
class TranslationVersionControl {
private git = simpleGit();
async commitTranslations(locale: string, message: string) {
await this.git.add(`locales/${locale}.json`);
await this.git.commit(`[i18n] ${message} (${locale})`);
}
async reviewChanges(locale: string) {
const diff = await this.git.diff([`locales/${locale}.json`]);
return this.parseDiff(diff);
}
async rollbackTranslation(locale: string, version: string) {
await this.git.checkout([version, `locales/${locale}.json`]);
}
private parseDiff(diff: string) {
const changes = {
added: [] as string[],
modified: [] as string[],
removed: [] as string[]
};
// 解析diff输出
const lines = diff.split('\n');
lines.forEach(line => {
if (line.startsWith('+') && !line.startsWith('+++')) {
changes.added.push(line.substring(1));
} else if (line.startsWith('-') && !line.startsWith('---')) {
changes.removed.push(line.substring(1));
}
});
return changes;
}
}17. 测试策略
17.1 语言切换测试
tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { LanguageProvider } from './LanguageContext';
describe('Language Switching', () => {
it('should switch language when clicked', async () => {
render(
<LanguageProvider>
<LanguageSwitcher />
<App />
</LanguageProvider>
);
// 初始语言
expect(screen.getByText('Hello')).toBeInTheDocument();
// 切换语言
fireEvent.click(screen.getByText('中文'));
// 验证切换后
await screen.findByText('你好');
expect(screen.queryByText('Hello')).not.toBeInTheDocument();
});
it('should persist language preference', () => {
render(
<LanguageProvider>
<LanguageSwitcher />
</LanguageProvider>
);
fireEvent.click(screen.getByText('中文'));
// 验证localStorage
expect(localStorage.getItem('preferredLanguage')).toBe('zh');
});
it('should handle invalid language gracefully', () => {
const { container } = render(
<LanguageProvider initialLanguage="invalid">
<App />
</LanguageProvider>
);
// 应该回退到默认语言
expect(container.textContent).toContain('Hello');
});
});17.2 翻译完整性测试
typescript
import fs from 'fs';
import path from 'path';
describe('Translation Completeness', () => {
const localesDir = path.join(__dirname, '../locales');
const languages = ['en', 'zh', 'fr', 'es'];
it('should have same keys across all languages', () => {
const allKeys = languages.map(lang => {
const filePath = path.join(localesDir, `${lang}.json`);
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
return Object.keys(content);
});
const baseKeys = allKeys[0];
allKeys.forEach((keys, index) => {
expect(keys.sort()).toEqual(baseKeys.sort());
});
});
it('should not have empty translations', () => {
languages.forEach(lang => {
const filePath = path.join(localesDir, `${lang}.json`);
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
Object.entries(content).forEach(([key, value]) => {
expect(value).toBeTruthy();
expect(value.toString().trim()).not.toBe('');
});
});
});
});13. 总结
语言切换实现的关键要点:
- 用户体验: 简单直观的切换界面
- 持久化: 保存用户语言偏好
- 自动检测: 智能识别用户语言
- URL集成: 支持URL参数或路径
- SEO友好: hreflang和正确的URL结构
- 性能优化: 懒加载和预加载策略
- 智能推荐: 基于用户行为的语言建议
- 质量保证: 翻译完整性和一致性检查
- 版本管理: Git-based翻译版本控制
- 企业集成: 与翻译管理平台无缝对接
通过实施完善的语言切换功能,可以为全球用户提供优质的多语言体验。