前端国际化是全球化应用的关键要素,通过完善的i18n解决方案,可以让应用轻松支持多语言,提升用户体验和市场覆盖面。
随着全球数字化进程的加速,构建支持多语言的国际化应用已成为现代前端开发的必备技能。前端国际化(Internationalization,简称i18n)不仅仅是简单的文字翻译,更涉及文化适应、布局调整、日期时间格式、数字格式等多个方面。本文将深入探讨前端国际化的完整解决方案,涵盖从基础概念到高级实践的全方位内容。
国际化(i18n)是使应用程序能够适应不同语言和地区的过程,而本地化(l10n)是针对特定地区进行实际的翻译和适配。
// 国际化核心概念示例
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}} كتاب'
}
}
};// 区域设置管理器
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));// 翻译管理器
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国际化上下文
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国际化配置 - 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 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>// 动态翻译加载器
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>
);
};// 翻译管理系统
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 '<';
case '>': return '>';
case '&': return '&';
case '\'': return ''';
case '"': return '"';
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解决方案,我们可以构建真正全球化的产品。本文涵盖了国际化的核心概念、技术实现、框架集成以及高级特性,为开发者提供了全面的国际化实现指南。
关键要点包括:
随着全球化进程的加速,前端国际化的重要性日益凸显。掌握这些技术和最佳实践,将有助于构建更优质的国际化产品。