Skip to content

国际化概念 - Web应用国际化完整指南

1. 国际化基础概念

1.1 什么是国际化

国际化(Internationalization, i18n) 是设计和开发应用程序以支持多种语言和地区的过程。"i18n"是因为"internationalization"首字母i和末字母n之间有18个字母。

相关概念:

  • i18n (国际化): 设计应用支持多语言的架构
  • L10n (本地化): 为特定地区定制内容和格式
  • g11n (全球化): i18n + L10n的综合过程

1.2 为什么需要国际化

typescript
// 业务需求
const i18nBenefits = {
  business: [
    '拓展全球市场',
    '提升用户体验',
    '增加用户覆盖',
    '提高品牌认知度'
  ],
  
  technical: [
    '统一代码库',
    '易于维护',
    '降低成本',
    '提高可扩展性'
  ],
  
  compliance: [
    '满足法律要求',
    '符合地区标准',
    '尊重文化差异'
  ]
};

1.3 国际化vs本地化

typescript
// 国际化 (i18n) - 架构层面
const i18nConcerns = {
  architecture: '设计支持多语言的架构',
  content: '文本外部化',
  formats: '日期、数字、货币格式化',
  encoding: 'Unicode支持',
  layout: '支持RTL/LTR文本方向'
};

// 本地化 (L10n) - 内容层面
const l10nConcerns = {
  translation: '翻译文本内容',
  cultural: '文化适配',
  legal: '法律合规',
  regional: '地区特定功能',
  images: '本地化图片和媒体'
};

2. 核心概念

2.1 Locale(语言环境)

typescript
// Locale标识符格式
// [language]-[script]-[region]-[variant]

const localeExamples = {
  'zh-CN': '简体中文(中国)',
  'zh-TW': '繁体中文(台湾)',
  'en-US': '英语(美国)',
  'en-GB': '英语(英国)',
  'fr-FR': '法语(法国)',
  'fr-CA': '法语(加拿大)',
  'ja-JP': '日语(日本)',
  'ko-KR': '韩语(韩国)',
  'ar-SA': '阿拉伯语(沙特)',
  'pt-BR': '葡萄牙语(巴西)'
};

// Locale对象
interface Locale {
  language: string;      // 语言代码 (ISO 639)
  script?: string;       // 书写系统 (ISO 15924)
  region?: string;       // 地区代码 (ISO 3166)
  variant?: string;      // 变体
}

function parseLocale(localeString: string): Locale {
  const [language, region] = localeString.split('-');
  return {
    language,
    region
  };
}

// 使用
const locale = parseLocale('zh-CN');
console.log(locale); // { language: 'zh', region: 'CN' }

2.2 文本方向

typescript
// LTR (Left-to-Right) vs RTL (Right-to-Left)
const textDirections = {
  ltr: ['en', 'zh', 'ja', 'ko', 'fr', 'de', 'es', 'ru'],
  rtl: ['ar', 'he', 'fa', 'ur']
};

function getTextDirection(locale: string): 'ltr' | 'rtl' {
  const language = locale.split('-')[0];
  return textDirections.rtl.includes(language) ? 'rtl' : 'ltr';
}

// React组件中使用
export function App() {
  const locale = useLocale();
  const direction = getTextDirection(locale);
  
  return (
    <div dir={direction}>
      <h1>Welcome</h1>
    </div>
  );
}

// CSS适配
const styles = {
  container: {
    marginLeft: direction === 'ltr' ? '20px' : '0',
    marginRight: direction === 'rtl' ? '20px' : '0'
  }
};

2.3 翻译键值

typescript
// 翻译资源结构
const translations = {
  'en-US': {
    common: {
      welcome: 'Welcome',
      login: 'Login',
      logout: 'Logout'
    },
    errors: {
      notFound: 'Page not found',
      serverError: 'Server error'
    },
    user: {
      profile: 'Profile',
      settings: 'Settings'
    }
  },
  'zh-CN': {
    common: {
      welcome: '欢迎',
      login: '登录',
      logout: '登出'
    },
    errors: {
      notFound: '页面未找到',
      serverError: '服务器错误'
    },
    user: {
      profile: '个人资料',
      settings: '设置'
    }
  }
};

// 命名空间组织
interface TranslationNamespace {
  common: Record<string, string>;
  errors: Record<string, string>;
  validation: Record<string, string>;
  forms: Record<string, string>;
}

// 类型安全的翻译键
type TranslationKey = 
  | `common.${keyof TranslationNamespace['common']}`
  | `errors.${keyof TranslationNamespace['errors']}`
  | `validation.${keyof TranslationNamespace['validation']}`
  | `forms.${keyof TranslationNamespace['forms']}`;

3. 数据格式化

3.1 日期和时间

typescript
// 日期格式化差异
const dateFormats = {
  'en-US': 'MM/DD/YYYY',    // 12/25/2024
  'en-GB': 'DD/MM/YYYY',    // 25/12/2024
  'zh-CN': 'YYYY年MM月DD日', // 2024年12月25日
  'ja-JP': 'YYYY年MM月DD日', // 2024年12月25日
  'de-DE': 'DD.MM.YYYY'     // 25.12.2024
};

// 使用Intl.DateTimeFormat
function formatDate(date: Date, locale: string): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
}

// 示例
const date = new Date('2024-12-25');
formatDate(date, 'en-US'); // December 25, 2024
formatDate(date, 'zh-CN'); // 2024年12月25日
formatDate(date, 'ja-JP'); // 2024年12月25日
formatDate(date, 'de-DE'); // 25. Dezember 2024

// 相对时间
function formatRelativeTime(
  date: Date,
  locale: string
): string {
  const rtf = new Intl.RelativeTimeFormat(locale, {
    numeric: 'auto'
  });
  
  const now = new Date();
  const diffInSeconds = (date.getTime() - now.getTime()) / 1000;
  const diffInMinutes = diffInSeconds / 60;
  const diffInHours = diffInMinutes / 60;
  const diffInDays = diffInHours / 24;
  
  if (Math.abs(diffInDays) >= 1) {
    return rtf.format(Math.round(diffInDays), 'day');
  } else if (Math.abs(diffInHours) >= 1) {
    return rtf.format(Math.round(diffInHours), 'hour');
  } else {
    return rtf.format(Math.round(diffInMinutes), 'minute');
  }
}

// 使用
formatRelativeTime(new Date(Date.now() - 3600000), 'en-US'); // 1 hour ago
formatRelativeTime(new Date(Date.now() - 3600000), 'zh-CN'); // 1小时前

3.2 数字格式化

typescript
// 数字格式差异
const numberFormats = {
  'en-US': '1,234,567.89',
  'de-DE': '1.234.567,89',
  'fr-FR': '1 234 567,89',
  'zh-CN': '1,234,567.89'
};

// 使用Intl.NumberFormat
function formatNumber(
  value: number,
  locale: string,
  options?: Intl.NumberFormatOptions
): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

// 示例
const number = 1234567.89;
formatNumber(number, 'en-US'); // 1,234,567.89
formatNumber(number, 'de-DE'); // 1.234.567,89
formatNumber(number, 'fr-FR'); // 1 234 567,89

// 百分比
formatNumber(0.157, 'en-US', { style: 'percent' }); // 15.7%
formatNumber(0.157, 'zh-CN', { style: 'percent' }); // 15.7%

// 科学计数法
formatNumber(123456, 'en-US', { notation: 'scientific' }); // 1.23E5

3.3 货币格式化

typescript
// 货币格式
function formatCurrency(
  amount: number,
  currency: string,
  locale: string
): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount);
}

// 示例
const amount = 1234.56;

formatCurrency(amount, 'USD', 'en-US'); // $1,234.56
formatCurrency(amount, 'EUR', 'de-DE'); // 1.234,56 €
formatCurrency(amount, 'JPY', 'ja-JP'); // ¥1,235
formatCurrency(amount, 'CNY', 'zh-CN'); // ¥1,234.56
formatCurrency(amount, 'GBP', 'en-GB'); // £1,234.56

// 货币符号位置
const currencyFormats = {
  'USD': '$1,234.56',    // 符号在前
  'EUR': '1.234,56 €',   // 符号在后
  'JPY': '¥1,235',       // 无小数
  'CNY': '¥1,234.56'     // 符号在前
};

3.4 单位和度量

typescript
// 单位转换
const unitSystems = {
  metric: {
    distance: 'km',
    weight: 'kg',
    temperature: '°C'
  },
  imperial: {
    distance: 'miles',
    weight: 'lbs',
    temperature: '°F'
  }
};

function getUnitSystem(locale: string): 'metric' | 'imperial' {
  const imperialCountries = ['US', 'LR', 'MM'];
  const region = locale.split('-')[1];
  return imperialCountries.includes(region) ? 'imperial' : 'metric';
}

// 使用Intl.NumberFormat的单位格式化
function formatUnit(
  value: number,
  unit: string,
  locale: string
): string {
  return new Intl.NumberFormat(locale, {
    style: 'unit',
    unit
  }).format(value);
}

// 示例
formatUnit(100, 'kilometer', 'en-US'); // 100 km
formatUnit(100, 'kilometer', 'zh-CN'); // 100公里
formatUnit(72, 'fahrenheit', 'en-US'); // 72°F
formatUnit(22, 'celsius', 'zh-CN');    // 22°C

4. 复数规则

4.1 复数形式

typescript
// 不同语言的复数规则
const pluralRules = {
  en: {
    zero: 'no items',
    one: '1 item',
    other: '{{count}} items'
  },
  zh: {
    other: '{{count}}个项目'  // 中文无复数变化
  },
  ru: {
    one: '{{count}} элемент',
    few: '{{count}} элемента',
    many: '{{count}} элементов',
    other: '{{count}} элемента'
  },
  ar: {
    zero: 'لا عناصر',
    one: 'عنصر واحد',
    two: 'عنصران',
    few: '{{count}} عناصر',
    many: '{{count}} عنصرًا',
    other: '{{count}} عنصر'
  }
};

// 使用Intl.PluralRules
function getPluralForm(
  count: number,
  locale: string
): Intl.LDMLPluralRule {
  const pr = new Intl.PluralRules(locale);
  return pr.select(count);
}

// 示例
getPluralForm(0, 'en-US');  // 'other'
getPluralForm(1, 'en-US');  // 'one'
getPluralForm(2, 'en-US');  // 'other'
getPluralForm(1, 'ru-RU');  // 'one'
getPluralForm(2, 'ru-RU');  // 'few'
getPluralForm(5, 'ru-RU');  // 'many'
getPluralForm(0, 'ar-SA');  // 'zero'
getPluralForm(2, 'ar-SA');  // 'two'

// 实现复数翻译函数
function translatePlural(
  key: string,
  count: number,
  locale: string,
  translations: any
): string {
  const pluralForm = getPluralForm(count, locale);
  const template = translations[locale][key][pluralForm] 
    || translations[locale][key].other;
  
  return template.replace('{{count}}', count.toString());
}

// 使用
translatePlural('items', 0, 'en-US', pluralRules); // "0 items"
translatePlural('items', 1, 'en-US', pluralRules); // "1 item"
translatePlural('items', 5, 'en-US', pluralRules); // "5 items"
translatePlural('items', 1, 'ru-RU', pluralRules); // "1 элемент"
translatePlural('items', 2, 'ru-RU', pluralRules); // "2 элемента"
translatePlural('items', 5, 'ru-RU', pluralRules); // "5 элементов"

4.2 序数

typescript
// 序数格式化
function formatOrdinal(
  number: number,
  locale: string
): string {
  const pr = new Intl.PluralRules(locale, { type: 'ordinal' });
  const rule = pr.select(number);
  
  const suffixes: Record<string, Record<Intl.LDMLPluralRule, string>> = {
    'en-US': {
      one: 'st',
      two: 'nd',
      few: 'rd',
      other: 'th',
      zero: 'th',
      many: 'th'
    },
    'zh-CN': {
      other: '第',
      one: '第',
      two: '第',
      few: '第',
      zero: '第',
      many: '第'
    }
  };
  
  const suffix = suffixes[locale][rule] || suffixes[locale].other;
  
  if (locale === 'zh-CN') {
    return `${suffix}${number}`;
  }
  
  return `${number}${suffix}`;
}

// 使用
formatOrdinal(1, 'en-US');  // 1st
formatOrdinal(2, 'en-US');  // 2nd
formatOrdinal(3, 'en-US');  // 3rd
formatOrdinal(4, 'en-US');  // 4th
formatOrdinal(21, 'en-US'); // 21st
formatOrdinal(1, 'zh-CN');  // 第1
formatOrdinal(2, 'zh-CN');  // 第2

5. 字符串处理

5.1 插值和变量

typescript
// 简单插值
const translations = {
  'en-US': {
    greeting: 'Hello, {{name}}!',
    itemCount: 'You have {{count}} items'
  },
  'zh-CN': {
    greeting: '你好,{{name}}!',
    itemCount: '你有{{count}}个项目'
  }
};

function interpolate(
  template: string,
  variables: Record<string, any>
): string {
  return template.replace(
    /\{\{(\w+)\}\}/g,
    (_, key) => variables[key]?.toString() || ''
  );
}

// 使用
interpolate(translations['en-US'].greeting, { name: 'John' }); 
// "Hello, John!"

interpolate(translations['zh-CN'].greeting, { name: '张三' }); 
// "你好,张三!"

// 复杂插值 - 支持格式化
interface InterpolationOptions {
  variables: Record<string, any>;
  locale: string;
  formatters?: Record<string, (value: any, locale: string) => string>;
}

function interpolateWithFormat(
  template: string,
  options: InterpolationOptions
): string {
  const { variables, locale, formatters = {} } = options;
  
  return template.replace(
    /\{\{(\w+)(?::(\w+))?\}\}/g,
    (_, key, formatter) => {
      const value = variables[key];
      
      if (formatter && formatters[formatter]) {
        return formatters[formatter](value, locale);
      }
      
      return value?.toString() || '';
    }
  );
}

// 使用
const formatters = {
  date: (value: Date, locale: string) => 
    new Intl.DateTimeFormat(locale).format(value),
  currency: (value: number, locale: string) => 
    new Intl.NumberFormat(locale, { 
      style: 'currency', 
      currency: 'USD' 
    }).format(value)
};

interpolateWithFormat(
  'Payment of {{amount:currency}} due on {{date:date}}',
  {
    variables: { 
      amount: 99.99, 
      date: new Date('2024-12-25') 
    },
    locale: 'en-US',
    formatters
  }
); // "Payment of $99.99 due on 12/25/2024"

5.2 性别和上下文

typescript
// 基于性别的翻译
const genderTranslations = {
  'en-US': {
    invitation: {
      male: 'He invited you to join',
      female: 'She invited you to join',
      other: 'They invited you to join'
    }
  },
  'es-ES': {
    invitation: {
      male: 'Él te invitó a unirte',
      female: 'Ella te invitó a unirte',
      other: 'Te invitaron a unirte'
    }
  }
};

function translateWithGender(
  key: string,
  gender: 'male' | 'female' | 'other',
  locale: string
): string {
  return genderTranslations[locale][key][gender];
}

// 基于上下文的翻译
const contextTranslations = {
  'en-US': {
    save: {
      button: 'Save',
      progress: 'Saving...',
      success: 'Saved successfully'
    }
  },
  'zh-CN': {
    save: {
      button: '保存',
      progress: '保存中...',
      success: '保存成功'
    }
  }
};

function translateWithContext(
  key: string,
  context: string,
  locale: string
): string {
  return contextTranslations[locale][key][context];
}

6. 国际化架构模式

6.1 资源文件组织

typescript
// 方式1: 按语言组织
translations/
  en-US/
    common.json
    errors.json
    forms.json
  zh-CN/
    common.json
    errors.json
    forms.json

// 方式2: 按功能组织
translations/
  common/
    en-US.json
    zh-CN.json
  errors/
    en-US.json
    zh-CN.json

// 方式3: 混合方式
translations/
  locales/
    en-US.json        # 所有英文翻译
    zh-CN.json        # 所有中文翻译
  namespaces/
    common.json       # 通用翻译键
    forms.json        # 表单翻译键

6.2 延迟加载

typescript
// 按需加载翻译资源
class I18nLoader {
  private cache = new Map<string, any>();
  
  async loadNamespace(
    locale: string,
    namespace: string
  ): Promise<any> {
    const key = `${locale}:${namespace}`;
    
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }
    
    const translations = await import(
      `../translations/${locale}/${namespace}.json`
    );
    
    this.cache.set(key, translations.default);
    return translations.default;
  }
  
  async loadLocale(locale: string): Promise<any> {
    const namespaces = ['common', 'errors', 'forms'];
    
    const translations = await Promise.all(
      namespaces.map(ns => this.loadNamespace(locale, ns))
    );
    
    return namespaces.reduce((acc, ns, i) => ({
      ...acc,
      [ns]: translations[i]
    }), {});
  }
}

// React中使用
function useTranslationLoader(locale: string) {
  const [translations, setTranslations] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const loader = new I18nLoader();
    
    loader.loadLocale(locale).then(t => {
      setTranslations(t);
      setLoading(false);
    });
  }, [locale]);
  
  return { translations, loading };
}

6.3 回退策略

typescript
// 语言回退链
class LocaleFallback {
  private fallbackChain: string[];
  
  constructor(locale: string) {
    this.fallbackChain = this.buildFallbackChain(locale);
  }
  
  private buildFallbackChain(locale: string): string[] {
    const chain: string[] = [locale];
    
    // zh-Hans-CN -> zh-Hans -> zh -> en
    const parts = locale.split('-');
    
    for (let i = parts.length - 1; i > 0; i--) {
      chain.push(parts.slice(0, i).join('-'));
    }
    
    // 添加默认语言
    if (!chain.includes('en')) {
      chain.push('en');
    }
    
    return chain;
  }
  
  translate(
    key: string,
    translations: Record<string, any>
  ): string | undefined {
    for (const locale of this.fallbackChain) {
      const value = this.getNestedValue(translations[locale], key);
      if (value !== undefined) {
        return value;
      }
    }
    
    return undefined;
  }
  
  private getNestedValue(obj: any, path: string): any {
    return path.split('.').reduce((acc, part) => acc?.[part], obj);
  }
}

// 使用
const fallback = new LocaleFallback('zh-Hans-CN');
const translation = fallback.translate('common.welcome', translations);

7. 实践示例

7.1 完整的i18n工具类

typescript
// i18n.ts
export class I18n {
  private currentLocale: string;
  private translations: Record<string, any>;
  private fallback: LocaleFallback;
  
  constructor(locale: string, translations: Record<string, any>) {
    this.currentLocale = locale;
    this.translations = translations;
    this.fallback = new LocaleFallback(locale);
  }
  
  t(key: string, options?: {
    variables?: Record<string, any>;
    count?: number;
    gender?: string;
    context?: string;
  }): string {
    let template = this.fallback.translate(key, this.translations);
    
    if (!template) {
      console.warn(`Translation missing for key: ${key}`);
      return key;
    }
    
    // 处理复数
    if (options?.count !== undefined) {
      const pluralForm = getPluralForm(options.count, this.currentLocale);
      template = template[pluralForm] || template.other || template;
    }
    
    // 处理性别
    if (options?.gender) {
      template = template[options.gender] || template;
    }
    
    // 处理上下文
    if (options?.context) {
      template = template[options.context] || template;
    }
    
    // 插值
    if (options?.variables) {
      template = interpolate(template, options.variables);
    }
    
    return template;
  }
  
  formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
    return new Intl.DateTimeFormat(this.currentLocale, options).format(date);
  }
  
  formatNumber(value: number, options?: Intl.NumberFormatOptions): string {
    return new Intl.NumberFormat(this.currentLocale, options).format(value);
  }
  
  formatCurrency(amount: number, currency: string): string {
    return new Intl.NumberFormat(this.currentLocale, {
      style: 'currency',
      currency
    }).format(amount);
  }
  
  setLocale(locale: string): void {
    this.currentLocale = locale;
    this.fallback = new LocaleFallback(locale);
  }
  
  getLocale(): string {
    return this.currentLocale;
  }
}

// 使用
const i18n = new I18n('zh-CN', translations);

i18n.t('common.welcome'); // "欢迎"
i18n.t('common.greeting', { variables: { name: '张三' } }); // "你好,张三!"
i18n.t('items', { count: 5 }); // "5个项目"
i18n.formatDate(new Date()); // "2024年1月15日"
i18n.formatCurrency(99.99, 'CNY'); // "¥99.99"

7.2 React集成

typescript
// I18nContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface I18nContextValue {
  locale: string;
  setLocale: (locale: string) => void;
  t: (key: string, options?: any) => string;
  formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string;
  formatNumber: (value: number, options?: Intl.NumberFormatOptions) => string;
  formatCurrency: (amount: number, currency: string) => string;
}

const I18nContext = createContext<I18nContextValue | null>(null);

export function I18nProvider({ 
  children, 
  initialLocale = 'en-US',
  translations 
}: {
  children: ReactNode;
  initialLocale?: string;
  translations: Record<string, any>;
}) {
  const [locale, setLocaleState] = useState(initialLocale);
  const [i18n] = useState(() => new I18n(locale, translations));
  
  const setLocale = (newLocale: string) => {
    i18n.setLocale(newLocale);
    setLocaleState(newLocale);
    // 保存到localStorage
    localStorage.setItem('locale', newLocale);
  };
  
  const value: I18nContextValue = {
    locale,
    setLocale,
    t: i18n.t.bind(i18n),
    formatDate: i18n.formatDate.bind(i18n),
    formatNumber: i18n.formatNumber.bind(i18n),
    formatCurrency: i18n.formatCurrency.bind(i18n)
  };
  
  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
}

export function useI18n() {
  const context = useContext(I18nContext);
  if (!context) {
    throw new Error('useI18n must be used within I18nProvider');
  }
  return context;
}

// 使用
function App() {
  return (
    <I18nProvider initialLocale="zh-CN" translations={translations}>
      <Dashboard />
    </I18nProvider>
  );
}

function Dashboard() {
  const { t, formatDate, formatCurrency } = useI18n();
  
  return (
    <div>
      <h1>{t('common.welcome')}</h1>
      <p>{formatDate(new Date())}</p>
      <p>{formatCurrency(99.99, 'CNY')}</p>
    </div>
  );
}

8. 性能优化

8.1 翻译资源分割

typescript
// 代码分割加载翻译
const translationLoaders = {
  'en-US': {
    common: () => import('./translations/en-US/common.json'),
    forms: () => import('./translations/en-US/forms.json'),
    dashboard: () => import('./translations/en-US/dashboard.json')
  },
  'zh-CN': {
    common: () => import('./translations/zh-CN/common.json'),
    forms: () => import('./translations/zh-CN/forms.json'),
    dashboard: () => import('./translations/zh-CN/dashboard.json')
  }
};

async function loadNamespaces(
  locale: string,
  namespaces: string[]
): Promise<Record<string, any>> {
  const results = await Promise.all(
    namespaces.map(ns => translationLoaders[locale][ns]())
  );
  
  return namespaces.reduce((acc, ns, i) => ({
    ...acc,
    [ns]: results[i].default
  }), {});
}

8.2 缓存策略

typescript
// 翻译缓存
class TranslationCache {
  private cache = new Map<string, string>();
  
  get(key: string, locale: string): string | undefined {
    return this.cache.get(`${locale}:${key}`);
  }
  
  set(key: string, locale: string, value: string): void {
    this.cache.set(`${locale}:${key}`, value);
  }
  
  clear(): void {
    this.cache.clear();
  }
  
  clearLocale(locale: string): void {
    for (const [key] of this.cache) {
      if (key.startsWith(`${locale}:`)) {
        this.cache.delete(key);
      }
    }
  }
}

// 在I18n类中使用
class I18nWithCache extends I18n {
  private cache = new TranslationCache();
  
  t(key: string, options?: any): string {
    const cacheKey = `${key}:${JSON.stringify(options)}`;
    const cached = this.cache.get(cacheKey, this.currentLocale);
    
    if (cached) {
      return cached;
    }
    
    const result = super.t(key, options);
    this.cache.set(cacheKey, this.currentLocale, result);
    
    return result;
  }
}

9. 测试

9.1 翻译完整性测试

typescript
// 检查翻译完整性
function validateTranslations(
  baseLocale: string,
  targetLocale: string,
  translations: Record<string, any>
): {
  missing: string[];
  extra: string[];
} {
  const missing: string[] = [];
  const extra: string[] = [];
  
  const baseKeys = getAllKeys(translations[baseLocale]);
  const targetKeys = getAllKeys(translations[targetLocale]);
  
  // 检查缺失的键
  baseKeys.forEach(key => {
    if (!targetKeys.includes(key)) {
      missing.push(key);
    }
  });
  
  // 检查多余的键
  targetKeys.forEach(key => {
    if (!baseKeys.includes(key)) {
      extra.push(key);
    }
  });
  
  return { missing, extra };
}

function getAllKeys(obj: any, prefix = ''): string[] {
  const keys: string[] = [];
  
  for (const [key, value] of Object.entries(obj)) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    
    if (typeof value === 'object' && value !== null) {
      keys.push(...getAllKeys(value, fullKey));
    } else {
      keys.push(fullKey);
    }
  }
  
  return keys;
}

// 使用
const validation = validateTranslations('en-US', 'zh-CN', translations);
console.log('Missing keys:', validation.missing);
console.log('Extra keys:', validation.extra);

9.2 单元测试

typescript
// i18n.test.ts
describe('I18n', () => {
  const translations = {
    'en-US': {
      common: {
        welcome: 'Welcome',
        greeting: 'Hello, {{name}}!'
      }
    },
    'zh-CN': {
      common: {
        welcome: '欢迎',
        greeting: '你好,{{name}}!'
      }
    }
  };
  
  it('should translate simple keys', () => {
    const i18n = new I18n('en-US', translations);
    expect(i18n.t('common.welcome')).toBe('Welcome');
  });
  
  it('should handle interpolation', () => {
    const i18n = new I18n('en-US', translations);
    expect(i18n.t('common.greeting', { variables: { name: 'John' } }))
      .toBe('Hello, John!');
  });
  
  it('should switch locales', () => {
    const i18n = new I18n('en-US', translations);
    expect(i18n.t('common.welcome')).toBe('Welcome');
    
    i18n.setLocale('zh-CN');
    expect(i18n.t('common.welcome')).toBe('欢迎');
  });
  
  it('should format dates correctly', () => {
    const i18n = new I18n('en-US', translations);
    const date = new Date('2024-01-15');
    
    expect(i18n.formatDate(date)).toBe('1/15/2024');
  });
});

10. 最佳实践

typescript
const i18nBestPractices = {
  architecture: [
    '从项目开始就考虑国际化',
    '使用标准的locale标识符',
    '实现回退语言机制',
    '分离翻译资源和代码',
    '按需加载翻译资源'
  ],
  
  translation: [
    '使用有意义的翻译键',
    '避免硬编码文本',
    '考虑文本长度变化',
    '提供上下文信息',
    '使用专业翻译服务'
  ],
  
  formatting: [
    '使用Intl API进行格式化',
    '正确处理复数形式',
    '考虑文本方向(LTR/RTL)',
    '格式化日期、数字、货币',
    '处理时区问题'
  ],
  
  performance: [
    '缓存翻译结果',
    '延迟加载翻译资源',
    '代码分割',
    '压缩翻译文件',
    '使用CDN分发'
  ],
  
  testing: [
    '检查翻译完整性',
    '测试所有支持的语言',
    '验证格式化输出',
    '测试RTL布局',
    '自动化测试流程'
  ]
};

11. 总结

国际化(i18n)的关键要点:

  1. 理解核心概念: Locale、文本方向、复数规则
  2. 数据格式化: 日期、数字、货币的本地化
  3. 翻译管理: 键值组织、插值、回退策略
  4. 性能优化: 资源分割、缓存、延迟加载
  5. 架构设计: 可扩展的i18n架构
  6. 质量保证: 翻译完整性检查、自动化测试
  7. 用户体验: 流畅的语言切换、准确的本地化

通过正确理解和实施国际化概念,可以构建真正面向全球用户的应用程序。