0%

前端国际化完整解决方案——多语言应用开发

前端国际化是全球化应用的关键要素,通过完善的i18n解决方案,可以让应用轻松支持多语言,提升用户体验和市场覆盖面。

介绍

  随着全球数字化进程的加速,构建支持多语言的国际化应用已成为现代前端开发的必备技能。前端国际化(Internationalization,简称i18n)不仅仅是简单的文字翻译,更涉及文化适应、布局调整、日期时间格式、数字格式等多个方面。本文将深入探讨前端国际化的完整解决方案,涵盖从基础概念到高级实践的全方位内容。

国际化基础概念

什么是国际化和本地化

国际化(i18n)是使应用程序能够适应不同语言和地区的过程,而本地化(l10n)是针对特定地区进行实际的翻译和适配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 国际化核心概念示例
class I18nConcepts {
constructor() {
this.supportedLocales = ['en', 'zh-CN', 'ja', 'es', 'fr', 'de'];
this.currentLocale = 'en';
this.translations = {};
}

// 国际化 vs 本地化示例
getInternationalizedValue(key, locale = this.currentLocale) {
// 国际化:提供占位符和格式化模式
const internationalized = this.translations[locale]?.[key];

// 本地化:根据区域设置进行具体格式化
const localized = this.localizeValue(internationalized, locale);

return localized;
}

localizeValue(value, locale) {
// 根据区域设置进行格式化
if (typeof value === 'object') {
return this.formatLocalizedObject(value, locale);
}
return value;
}

formatLocalizedObject(obj, locale) {
const formatter = new Intl.NumberFormat(locale);
const dateFormatter = new Intl.DateTimeFormat(locale);

return {
...obj,
formattedNumber: obj.number ? formatter.format(obj.number) : undefined,
formattedDate: obj.date ? dateFormatter.format(new Date(obj.date)) : undefined
};
}

// 示例:不同语言的复数处理
getPluralTranslation(key, count, locale = this.currentLocale) {
// 不同语言的复数规则不同
const pluralRules = new Intl.PluralRules(locale);
const pluralCategory = pluralRules.select(count);

return this.translations[locale]?.[`${key}_${pluralCategory}`] ||
this.translatableStrings[locale]?.[key] ||
key;
}
}

// 不同语言的复数规则示例
const pluralExamples = {
en: {
// English: one, other
books: { one: '1 book', other: '{{count}} books' }
},
zh: {
// Chinese: no distinction needed (simplified example)
books: { other: '{{count}} 本书' }
},
ar: {
// Arabic: zero, one, two, few, many, other
books: {
zero: 'لا توجد كتب',
one: 'كتاب واحد',
two: 'كتابان',
few: '{{count}} كتب قليلة',
many: '{{count}} كتابًا',
other: '{{count}} كتاب'
}
}
};

语言标签和区域设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// 区域设置管理器
class LocaleManager {
constructor() {
this.localeData = {
'en': { name: 'English', dir: 'ltr', script: 'Latn', region: 'US' },
'zh-CN': { name: '简体中文', dir: 'ltr', script: 'Hans', region: 'CN' },
'zh-TW': { name: '繁體中文', dir: 'ltr', script: 'Hant', region: 'TW' },
'ja': { name: '日本語', dir: 'ltr', script: 'Jpan', region: 'JP' },
'ar': { name: 'العربية', dir: 'rtl', script: 'Arab', region: 'SA' },
'he': { name: 'עברית', dir: 'rtl', script: 'Hebr', region: 'IL' }
};

this.supportedLocales = Object.keys(this.localeData);
}

// 获取最佳匹配的语言
getBestMatch(locales) {
const userLocales = Array.isArray(locales) ? locales : [locales];

for (const userLocale of userLocales) {
// 精确匹配
if (this.supportedLocales.includes(userLocale)) {
return userLocale;
}

// 语言代码匹配(如 en-US -> en)
const langCode = userLocale.split('-')[0];
const match = this.supportedLocales.find(supported =>
supported.split('-')[0] === langCode
);

if (match) {
return match;
}
}

// 默认返回英语
return 'en';
}

// 获取区域设置的 RTL/LTR 信息
isRightToLeft(locale) {
return this.localeData[locale]?.dir === 'rtl';
}

// 获取区域设置的完整信息
getLocaleInfo(locale) {
return this.localeData[locale] || this.localeData['en'];
}

// 生成 HTML 属性
getHtmlAttributes(locale) {
const info = this.getLocaleInfo(locale);
return {
lang: locale,
dir: info.dir,
'data-locale': locale,
'data-dir': info.dir
};
}

// 获取格式化选项
getFormattingOptions(locale) {
return {
number: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
},
currency: {
style: 'currency',
currency: this.getCurrencyCode(locale)
},
date: {
year: 'numeric',
month: 'long',
day: 'numeric'
},
time: {
hour: '2-digit',
minute: '2-digit'
}
};
}

getCurrencyCode(locale) {
// 根据区域设置返回货币代码
const localeToCurrency = {
'en': 'USD',
'zh-CN': 'CNY',
'ja': 'JPY',
'es': 'EUR',
'fr': 'EUR',
'de': 'EUR'
};

return localeToCurrency[locale] || 'USD';
}

// 检测用户首选语言
detectUserLocale() {
if (typeof navigator !== 'undefined') {
// 浏览器环境
const userLanguages = navigator.languages || [navigator.language];
return this.getBestMatch(userLanguages);
}

// Node.js 环境
const envLocale = process.env.LOCALE || process.env.LANG;
if (envLocale) {
return this.getBestMatch(envLocale.split('.')[0]);
}

return 'en';
}
}

// 使用示例
const localeManager = new LocaleManager();
const userLocale = localeManager.detectUserLocale();
console.log('Detected locale:', userLocale);
console.log('HTML attributes:', localeManager.getHtmlAttributes(userLocale));

核心技术实现

翻译管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// 翻译管理器
class TranslationManager {
constructor(options = {}) {
this.translations = {};
this.currentLocale = 'en';
this.fallbackLocale = 'en';
this.cache = new Map();
this.loadingPromises = new Map();

this.options = {
debug: false,
cacheEnabled: true,
interpolation: {
prefix: '{{',
suffix: '}}'
},
...options
};
}

// 加载翻译资源
async loadTranslations(locale, translationData) {
if (typeof translationData === 'string') {
// 加载远程资源
const response = await fetch(translationData);
this.translations[locale] = await response.json();
} else {
// 直接使用对象
this.translations[locale] = translationData;
}

// 预填充缓存
this.preloadCache(locale);
}

// 预填充缓存
preloadCache(locale) {
const translations = this.translations[locale] || {};
this.walkTranslations(translations, (key, value) => {
const cacheKey = `${locale}:${key}`;
this.cache.set(cacheKey, value);
});
}

// 递归遍历翻译对象
walkTranslations(obj, callback, prefix = '') {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;

if (typeof value === 'string') {
callback(fullKey, value);
} else if (typeof value === 'object' && value !== null) {
this.walkTranslations(value, callback, fullKey);
}
}
}

// 翻译方法
translate(key, params = {}, locale = this.currentLocale) {
const cacheKey = `${locale}:${key}`;

// 检查缓存
if (this.options.cacheEnabled && this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
return this.interpolate(cached, params);
}

// 查找翻译
let translation = this.findTranslation(key, locale);

// 如果未找到,尝试回退语言
if (!translation && locale !== this.fallbackLocale) {
translation = this.findTranslation(key, this.fallbackLocale);
}

// 如果仍然未找到,返回原始键
if (!translation) {
translation = key;
if (this.options.debug) {
console.warn(`Translation key not found: ${key}`);
}
}

// 插值处理
const interpolated = this.interpolate(translation, params);

// 缓存结果
if (this.options.cacheEnabled) {
this.cache.set(cacheKey, translation);
}

return interpolated;
}

// 查找翻译
findTranslation(key, locale) {
const translations = this.translations[locale];
if (!translations) return null;

// 支持嵌套键访问
const keys = key.split('.');
let current = translations;

for (const k of keys) {
if (current && typeof current === 'object') {
current = current[k];
} else {
return null;
}
}

return typeof current === 'string' ? current : null;
}

// 字符串插值
interpolate(template, params) {
if (!template || typeof template !== 'string') {
return template;
}

if (!params || Object.keys(params).length === 0) {
return template;
}

let result = template;

for (const [key, value] of Object.entries(params)) {
const placeholder = `${this.options.interpolation.prefix}${key}${this.options.interpolation.suffix}`;
const replacement = String(value);
result = result.split(placeholder).join(replacement);
}

return result;
}

// 批量翻译
translateBatch(keys, params = {}, locale = this.currentLocale) {
return keys.reduce((acc, key) => {
acc[key] = this.translate(key, params, locale);
return acc;
}, {});
}

// 获取当前语言的所有翻译
getAllTranslations(locale = this.currentLocale) {
return this.translations[locale] || {};
}

// 添加或更新翻译
addTranslations(newTranslations, locale = this.currentLocale) {
if (!this.translations[locale]) {
this.translations[locale] = {};
}

// 深度合并
this.translations[locale] = this.deepMerge(
this.translations[locale],
newTranslations
);

// 清除相关缓存
this.clearCacheForLocale(locale);
}

deepMerge(target, source) {
const output = { ...target };

for (const key in source) {
if (source.hasOwnProperty(key)) {
if (
source[key] &&
typeof source[key] === 'object' &&
!Array.isArray(source[key]) &&
target[key] &&
typeof target[key] === 'object' &&
!Array.isArray(target[key])
) {
output[key] = this.deepMerge(target[key], source[key]);
} else {
output[key] = source[key];
}
}
}

return output;
}

// 切换语言
async setLocale(locale) {
const oldLocale = this.currentLocale;
this.currentLocale = locale;

// 可以在这里加载特定语言的资源
if (!this.translations[locale]) {
await this.loadLocaleResources(locale);
}

// 触发语言切换事件
this.emit('localeChanged', { oldLocale, newLocale: locale });
}

// 加载特定语言资源
async loadLocaleResources(locale) {
// 实现具体的资源加载逻辑
const resourceUrl = `/locales/${locale}.json`;
await this.loadTranslations(locale, resourceUrl);
}

// 清除缓存
clearCache() {
this.cache.clear();
}

clearCacheForLocale(locale) {
for (const key of this.cache.keys()) {
if (key.startsWith(`${locale}:`)) {
this.cache.delete(key);
}
}
}

// 事件系统
on(event, callback) {
if (!this.eventHandlers) this.eventHandlers = {};
if (!this.eventHandlers[event]) this.eventHandlers[event] = [];
this.eventHandlers[event].push(callback);
}

emit(event, data) {
if (this.eventHandlers && this.eventHandlers[event]) {
this.eventHandlers[event].forEach(callback => callback(data));
}
}
}

// 使用示例
const translator = new TranslationManager({ debug: true });

// 加载翻译
await translator.loadTranslations('en', {
welcome: 'Welcome to our application',
greeting: 'Hello, {{name}}!',
items_count: {
one: 'There is {{count}} item',
other: 'There are {{count}} items'
}
});

await translator.loadTranslations('zh-CN', {
welcome: '欢迎使用我们的应用',
greeting: '你好,{{name}}!',
items_count: {
other: '共有 {{count}} 个项目'
}
});

// 使用翻译
console.log(translator.translate('welcome')); // 'Welcome to our application'
console.log(translator.translate('greeting', { name: 'Alice' })); // 'Hello, Alice!'

React国际化组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// React国际化上下文
import React, { createContext, useContext, useState, useEffect } from 'react';

const I18nContext = createContext();

export const I18nProvider = ({ children, initialLocale = 'en', translations = {} }) => {
const [currentLocale, setCurrentLocale] = useState(initialLocale);
const [translationManager] = useState(() => new TranslationManager());

// 初始化翻译资源
useEffect(() => {
const loadInitialTranslations = async () => {
for (const [locale, data] of Object.entries(translations)) {
await translationManager.loadTranslations(locale, data);
}
translationManager.currentLocale = initialLocale;
};

loadInitialTranslations();
}, [translations, initialLocale]);

// 切换语言
const changeLocale = async (locale) => {
await translationManager.setLocale(locale);
setCurrentLocale(locale);
};

// 翻译函数
const t = (key, params = {}) => {
return translationManager.translate(key, params, currentLocale);
};

// 获取当前语言信息
const getLocaleInfo = () => {
return {
currentLocale,
isRtl: localeManager.isRightToLeft(currentLocale),
direction: localeManager.getLocaleInfo(currentLocale).dir
};
};

const value = {
t,
currentLocale,
changeLocale,
getLocaleInfo,
translationManager
};

return (
<I18nContext.Provider value={value}>
<div dir={getLocaleInfo().direction} data-locale={currentLocale}>
{children}
</div>
</I18nContext.Provider>
);
};

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

// 翻译组件
export const Trans = ({ i18nKey, params = {}, fallback = i18nKey }) => {
const { t } = useI18n();

return t(i18nKey, params) || fallback;
};

// 语言切换组件
export const LanguageSwitcher = () => {
const { currentLocale, changeLocale } = useI18n();
const { supportedLocales } = localeManager;

return (
<select
value={currentLocale}
onChange={(e) => changeLocale(e.target.value)}
className="language-switcher"
>
{supportedLocales.map(locale => (
<option key={locale} value={locale}>
{localeManager.getLocaleInfo(locale).name}
</option>
))}
</select>
);
};

// 数字格式化组件
export const NumberFormat = ({ value, locale, options = {} }) => {
const { currentLocale } = useI18n();
const displayLocale = locale || currentLocale;

const formatted = new Intl.NumberFormat(displayLocale, options).format(value);

return <span>{formatted}</span>;
};

// 日期格式化组件
export const DateFormat = ({ date, locale, options = {} }) => {
const { currentLocale } = useI18n();
const displayLocale = locale || currentLocale;

const formatted = new Intl.DateTimeFormat(displayLocale, options).format(new Date(date));

return <span>{formatted}</span>;
};

// 货币格式化组件
export const CurrencyFormat = ({ value, currency, locale }) => {
const { currentLocale } = useI18n();
const displayLocale = locale || currentLocale;
const displayCurrency = currency || localeManager.getCurrencyCode(displayLocale);

const formatted = new Intl.NumberFormat(displayLocale, {
style: 'currency',
currency: displayCurrency
}).format(value);

return <span>{formatted}</span>;
};

// 使用示例
const App = () => {
const translations = {
en: {
welcome: 'Welcome to our app',
user_greeting: 'Hello, {{name}}!',
item_count: {
one: 'There is 1 item',
other: 'There are {{count}} items'
}
},
zh-CN: {
welcome: '欢迎使用我们的应用',
user_greeting: '你好,{{name}}!',
item_count: {
other: '共有 {{count}} 个项目'
}
}
};

return (
<I18nProvider initialLocale="en" translations={translations}>
<div>
<header>
<h1><Trans i18nKey="welcome" /></h1>
<LanguageSwitcher />
</header>

<main>
<UserProfile />
<ItemCount items={5} />
</main>
</div>
</I18nProvider>
);
};

const UserProfile = () => {
const { t } = useI18n();
const [name] = useState('Alice');

return (
<div>
<h2><Trans i18nKey="user_greeting" params={{ name }} /></h2>
<DateFormat date={new Date()} options={{ year: 'numeric', month: 'long', day: 'numeric' }} />
</div>
);
};

const ItemCount = ({ items }) => {
const { t } = useI18n();
const pluralKey = items === 1 ? 'item_count.one' : 'item_count.other';

return (
<p>
<Trans
i18nKey={pluralKey}
params={{ count: <NumberFormat value={items} /> }}
/>
</p>
);
};

框架集成方案

Next.js 国际化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Next.js国际化配置 - next.config.js
const path = require('path');

/** @type {import('next').NextConfig} */
const nextConfig = {
i18n: {
locales: ['en', 'zh-CN', 'ja', 'es', 'fr'],
defaultLocale: 'en',
domains: [
{
domain: 'example.com',
defaultLocale: 'en'
},
{
domain: 'jp.example.com',
defaultLocale: 'ja'
}
]
},
webpack: (config, { isServer }) => {
// 配置国际化资源处理
config.module.rules.push({
test: /\.ftl$/,
use: 'raw-loader' // 用于加载Fluent格式的翻译文件
});

return config;
}
};

module.exports = nextConfig;

// Next.js页面国际化示例
// pages/index.js
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';

const HomePage = () => {
const { t } = useTranslation('common');

return (
<div>
<h1>{t('welcome_title')}</h1>
<p>{t('welcome_description')}</p>
</div>
);
};

export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, ['common', 'footer']))
}
});

export default HomePage;

// 自定义App组件处理国际化
// pages/_app.js
import { appWithTranslation } from 'next-i18next';
import nextI18NextConfig from '../next-i18next.config.js';

const MyApp = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};

export default appWithTranslation(MyApp, nextI18NextConfig);

// Next.js国际化配置文件 - next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'zh-CN', 'ja', 'es', 'fr']
},
localePath: typeof window === 'undefined'
? require('path').resolve('./public/locales')
: '/locales',
reloadOnPrerender: process.env.NODE_ENV === 'development'
};

// API路由中的国际化
// pages/api/translate.js
export default async function handler(req, res) {
const { text, targetLang, sourceLang = 'en' } = req.body;

try {
// 使用翻译服务进行翻译
const translatedText = await translateService.translate({
text,
targetLang,
sourceLang
});

res.status(200).json({ translatedText });
} catch (error) {
res.status(500).json({ error: error.message });
}
}

Vue国际化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// Vue 3 国际化插件
import { createI18n } from 'vue-i18n';

// 翻译资源
const messages = {
en: {
message: {
hello: 'Hello, {name}!',
items: 'You have {count} items',
items_0: 'You have no items',
items_1: 'You have 1 item',
items_n: 'You have {count} items'
}
},
zh: {
message: {
hello: '你好,{name}!',
items: '你有 {count} 个项目',
items_0: '你没有项目',
items_1: '你有 1 个项目',
items_n: '你有 {count} 个项目'
}
}
};

// 创建i18n实例
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages,
pluralRules: {
// 自定义复数规则
zh: (choice) => {
if (choice === 0) return 0; // 零
if (choice === 1) return 1; // 一
return 2; // 其他
}
}
});

// Vue应用
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);
app.use(i18n);
app.mount('#app');

// Vue组件中的使用
// Component.vue
<template>
<div>
<h1>{{ $t('message.hello', { name: userName }) }}</h1>
<p>{{ $tc('message.items', itemCount, { count: itemCount }) }}</p>

<select v-model="$i18n.locale">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
</template>

<script>
import { ref } from 'vue';

export default {
setup() {
const userName = ref('Alice');
const itemCount = ref(5);

return {
userName,
itemCount
};
}
};
</script>

高级特性实现

动态加载和代码分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 动态翻译加载器
class DynamicTranslationLoader {
constructor() {
this.loadedChunks = new Set();
this.translationChunks = new Map();
}

// 动态加载翻译块
async loadTranslationChunk(chunkName, locale) {
if (this.isChunkLoaded(chunkName, locale)) {
return this.getChunk(chunkName, locale);
}

const chunkKey = `${locale}-${chunkName}`;

// 检查是否有加载中的Promise
if (this.translationChunks.has(chunkKey)) {
return this.translationChunks.get(chunkKey);
}

// 动态导入翻译文件
const loadPromise = import(
/* webpackChunkName: "translations-[request]" */
`../locales/${locale}/${chunkName}.json`
).then(module => {
this.translationChunks.delete(chunkKey);
this.loadedChunks.add(chunkKey);
return module.default;
}).catch(error => {
console.error(`Failed to load translation chunk: ${chunkName}`, error);
return null;
});

this.translationChunks.set(chunkKey, loadPromise);
return loadPromise;
}

isChunkLoaded(chunkName, locale) {
return this.loadedChunks.has(`${locale}-${chunkName}`);
}

getChunk(chunkName, locale) {
return this.translationChunks.get(`${locale}-${chunkName}`);
}

// 预加载常用翻译块
async preloadCommonChunks(locales = ['en']) {
const commonChunks = ['common', 'navigation', 'footer'];

const loadPromises = locales.flatMap(locale =>
commonChunks.map(chunk => this.loadTranslationChunk(chunk, locale))
);

return Promise.all(loadPromises);
}

// 按需加载翻译
async loadOnDemand(requiredChunks, locale) {
const loadPromises = requiredChunks.map(chunk =>
this.loadTranslationChunk(chunk, locale)
);

return await Promise.all(loadPromises);
}
}

// 与组件系统的集成
const TranslationBoundary = ({
children,
requiredChunks = [],
locale
}) => {
const [loading, setLoading] = useState(true);
const [loaded, setLoaded] = useState(false);

useEffect(() => {
const loadTranslations = async () => {
setLoading(true);
await dynamicLoader.loadOnDemand(requiredChunks, locale);
setLoading(false);
setLoaded(true);
};

loadTranslations();
}, [requiredChunks, locale]);

if (loading) {
return <div>加载翻译资源中...</div>;
}

return children;
};

// 使用示例
const ProductPage = () => {
return (
<TranslationBoundary requiredChunks={['products', 'catalog']} locale="zh-CN">
<ProductContent />
</TranslationBoundary>
);
};

翻译编辑和管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
// 翻译管理系统
class TranslationEditor {
constructor(apiClient) {
this.apiClient = apiClient;
this.translations = new Map();
this.changes = new Set();
this.syncQueue = [];
}

// 获取翻译数据
async fetchTranslations(locale, namespace = 'common') {
const response = await this.apiClient.get(`/translations/${locale}/${namespace}`);
const data = await response.json();

const key = `${locale}:${namespace}`;
this.translations.set(key, data);

return data;
}

// 更新翻译
updateTranslation(locale, namespace, key, value) {
const translationKey = `${locale}:${namespace}`;
const translations = this.translations.get(translationKey) || {};

// 深度设置嵌套键
this.setNestedValue(translations, key, value);
this.translations.set(translationKey, translations);

// 记录变更
const change = {
locale,
namespace,
key,
oldValue: this.getNestedValue(translations, key),
newValue: value,
timestamp: Date.now()
};

this.changes.add(change);
}

setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;

for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}

current[keys[keys.length - 1]] = value;
}

getNestedValue(obj, path) {
const keys = path.split('.');
let current = obj;

for (const key of keys) {
if (!current || typeof current !== 'object') {
return undefined;
}
current = current[key];
}

return current;
}

// 批量同步翻译变更
async syncChanges() {
if (this.changes.size === 0) return;

const changesArray = Array.from(this.changes);
this.syncQueue.push(...changesArray);

// 批量提交变更
try {
await this.apiClient.post('/translations/batch-update', {
changes: changesArray
});

// 清空变更记录
this.changes.clear();

console.log(`Successfully synced ${changesArray.length} translation changes`);
} catch (error) {
console.error('Failed to sync translation changes:', error);
throw error;
}
}

// 翻译质量检查
validateTranslations(locale, namespace) {
const translations = this.translations.get(`${locale}:${namespace}`);
const issues = [];

this.walkTranslations(translations, (key, value) => {
if (typeof value === 'string') {
// 检查占位符匹配
const placeholders = this.extractPlaceholders(value);
const expectedParams = this.findExpectedParams(key);

if (placeholders.length !== expectedParams.length) {
issues.push({
key,
type: 'placeholder_mismatch',
message: `Placeholder mismatch in key: ${key}`,
placeholders,
expectedParams
});
}

// 检查特殊字符
if (this.hasDangerousCharacters(value)) {
issues.push({
key,
type: 'dangerous_characters',
message: `Dangerous characters in translation: ${key}`,
value
});
}
}
});

return issues;
}

extractPlaceholders(text) {
const regex = /{{(\w+)}}/g;
const matches = [];
let match;

while ((match = regex.exec(text)) !== null) {
matches.push(match[1]);
}

return matches;
}

findExpectedParams(key) {
// 基于键名推断期望的参数
const paramPatterns = {
'greeting': ['name'],
'welcome': ['user', 'site'],
'items_count': ['count', 'type']
};

for (const [pattern, params] of Object.entries(paramPatterns)) {
if (key.includes(pattern)) {
return params;
}
}

return [];
}

hasDangerousCharacters(text) {
// 检查可能的XSS字符
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/vbscript:/i,
/on\w+=/i
];

return dangerousPatterns.some(pattern => pattern.test(text));
}

// 导出翻译
exportTranslations(locale, format = 'json') {
const namespaces = Array.from(this.translations.keys())
.filter(key => key.startsWith(locale))
.map(key => key.split(':')[1]);

const exportData = {};

for (const namespace of namespaces) {
exportData[namespace] = this.translations.get(`${locale}:${namespace}`);
}

switch (format) {
case 'json':
return JSON.stringify(exportData, null, 2);
case 'csv':
return this.toCsvFormat(exportData);
case 'xliff':
return this.toXliffFormat(exportData, locale);
default:
throw new Error(`Unsupported export format: ${format}`);
}
}

toCsvFormat(data) {
const rows = [];
rows.push(['Key', 'Value']);

for (const [namespace, translations] of Object.entries(data)) {
this.walkTranslations(translations, (key, value) => {
if (typeof value === 'string') {
rows.push([`${namespace}.${key}`, value]);
}
});
}

return rows.map(row => row.map(field => `"${field}"`).join(',')).join('\n');
}

toXliffFormat(data, locale) {
let xliff = `<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file original="" source-language="en" target-language="${locale}">
<body>`;

for (const [namespace, translations] of Object.entries(data)) {
this.walkTranslations(translations, (key, value) => {
if (typeof value === 'string') {
xliff += `
<trans-unit id="${namespace}.${key}">
<source></source>
<target>${this.escapeXml(value)}</target>
</trans-unit>`;
}
});
}

xliff += `
</body>
</file>
</xliff>`;

return xliff;
}

escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
default: return c;
}
});
}

// 翻译统计
getStatistics(locale) {
const namespaces = Array.from(this.translations.keys())
.filter(key => key.startsWith(locale))
.map(key => key.split(':')[1]);

const stats = {
totalKeys: 0,
translatedKeys: 0,
completionRate: 0,
namespaces: {}
};

for (const namespace of namespaces) {
const translations = this.translations.get(`${locale}:${namespace}`);
const allKeys = [];
const translatedKeys = [];

this.walkTranslations(translations, (key, value) => {
allKeys.push(key);
if (value && typeof value === 'string' && value.trim() !== '') {
translatedKeys.push(key);
}
});

stats.namespaces[namespace] = {
total: allKeys.length,
translated: translatedKeys.length,
completionRate: allKeys.length > 0 ?
(translatedKeys.length / allKeys.length * 100) : 0
};

stats.totalKeys += allKeys.length;
stats.translatedKeys += translatedKeys.length;
}

stats.completionRate = stats.totalKeys > 0 ?
(stats.translatedKeys / stats.totalKeys * 100) : 0;

return stats;
}
}

// 翻译编辑器UI组件
const TranslationEditorUI = () => {
const [currentLocale, setCurrentLocale] = useState('en');
const [currentNamespace, setCurrentNamespace] = useState('common');
const [translations, setTranslations] = useState({});
const [searchTerm, setSearchTerm] = useState('');
const [editor, setEditor] = useState(null);

useEffect(() => {
const initEditor = async () => {
const editorInstance = new TranslationEditor(apiClient);
await editorInstance.fetchTranslations(currentLocale, currentNamespace);
setEditor(editorInstance);
};

initEditor();
}, [currentLocale, currentNamespace]);

const filteredTranslations = useMemo(() => {
if (!translations) return {};

return Object.entries(translations).reduce((acc, [key, value]) => {
if (key.toLowerCase().includes(searchTerm.toLowerCase()) ||
(typeof value === 'string' &&
value.toLowerCase().includes(searchTerm.toLowerCase()))) {
acc[key] = value;
}
return acc;
}, {});
}, [translations, searchTerm]);

return (
<div className="translation-editor">
<div className="editor-controls">
<select
value={currentLocale}
onChange={(e) => setCurrentLocale(e.target.value)}
>
<option value="en">English</option>
<option value="zh-CN">简体中文</option>
<option value="ja">日本語</option>
</select>

<select
value={currentNamespace}
onChange={(e) => setCurrentNamespace(e.target.value)}
>
<option value="common">Common</option>
<option value="navigation">Navigation</option>
<option value="footer">Footer</option>
</select>

<input
type="text"
placeholder="Search translations..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>

<div className="translations-list">
{Object.entries(filteredTranslations).map(([key, value]) => (
<TranslationItem
key={key}
keyName={key}
initialValue={value}
onSave={(newValue) => {
editor.updateTranslation(currentLocale, currentNamespace, key, newValue);
}}
/>
))}
</div>

<div className="editor-actions">
<button onClick={() => editor.syncChanges()}>
Save Changes
</button>
<button onClick={() => {
const stats = editor.getStatistics(currentLocale);
console.log('Translation statistics:', stats);
}}>
Show Statistics
</button>
</div>
</div>
);
};

const TranslationItem = ({ keyName, initialValue, onSave }) => {
const [editing, setEditing] = useState(false);
const [value, setValue] = useState(initialValue);

const handleSave = () => {
onSave(value);
setEditing(false);
};

if (editing) {
return (
<div className="translation-edit">
<label>{keyName}:</label>
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={handleSave}
autoFocus
/>
<button onClick={handleSave}></button>
<button onClick={() => {
setValue(initialValue);
setEditing(false);
}}>✗</button>
</div>
);
}

return (
<div className="translation-view" onClick={() => setEditing(true)}>
<strong>{keyName}:</strong>
<span>{value}</span>
</div>
);
};

前端国际化是一个系统性工程,需要从架构设计、技术选型到运营维护的全方位考虑。合理的国际化策略不仅能提升用户体验,还能为产品的全球化扩张奠定坚实基础。

总结

  前端国际化是现代Web应用开发的重要组成部分,通过完善的i18n解决方案,我们可以构建真正全球化的产品。本文涵盖了国际化的核心概念、技术实现、框架集成以及高级特性,为开发者提供了全面的国际化实现指南。

  关键要点包括:

  1. 架构设计:选择合适的国际化架构,支持动态加载和代码分割
  2. 技术实现:实现高效的翻译管理、格式化和缓存机制
  3. 框架集成:与React、Vue、Next.js等主流框架深度集成
  4. 运营维护:建立翻译编辑、质量检查和统计分析体系
  5. 性能优化:实现懒加载、缓存和CDN加速等优化策略

  随着全球化进程的加速,前端国际化的重要性日益凸显。掌握这些技术和最佳实践,将有助于构建更优质的国际化产品。

bulb