Appearance
国际化概念 - 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.23E53.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°C4. 复数规则
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'); // 第25. 字符串处理
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)的关键要点:
- 理解核心概念: Locale、文本方向、复数规则
- 数据格式化: 日期、数字、货币的本地化
- 翻译管理: 键值组织、插值、回退策略
- 性能优化: 资源分割、缓存、延迟加载
- 架构设计: 可扩展的i18n架构
- 质量保证: 翻译完整性检查、自动化测试
- 用户体验: 流畅的语言切换、准确的本地化
通过正确理解和实施国际化概念,可以构建真正面向全球用户的应用程序。