Skip to content

语言切换实现 - 完整的多语言切换解决方案

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. 总结

语言切换实现的关键要点:

  1. 用户体验: 简单直观的切换界面
  2. 持久化: 保存用户语言偏好
  3. 自动检测: 智能识别用户语言
  4. URL集成: 支持URL参数或路径
  5. SEO友好: hreflang和正确的URL结构
  6. 性能优化: 懒加载和预加载策略
  7. 智能推荐: 基于用户行为的语言建议
  8. 质量保证: 翻译完整性和一致性检查
  9. 版本管理: Git-based翻译版本控制
  10. 企业集成: 与翻译管理平台无缝对接

通过实施完善的语言切换功能,可以为全球用户提供优质的多语言体验。

扩展阅读