工程化与框架系列(28)--前端国际化实现
前端国际化实现 🌍
引言
前端国际化(i18n)是现代Web应用中的重要组成部分,它能够让应用支持多语言和多地区的用户使用。本文将深入探讨前端国际化的实现方案和最佳实践,包括文本翻译、日期时间格式化、货币处理等方面。
国际化概述
前端国际化主要包括以下方面:
- 文本翻译:界面文字的多语言支持
- 日期时间:不同地区的日期时间格式
- 货币处理:不同地区的货币格式
- 数字格式:不同地区的数字表示方式
- 文字方向:从左到右(LTR)和从右到左(RTL)的布局支持
国际化工具实现
国际化管理器
// 国际化管理器类
class I18nManager {
private static instance: I18nManager;
private currentLocale: string;
private messages: Map<string, Record<string, string>>;
private numberFormatter: Intl.NumberFormat;
private dateFormatter: Intl.DateTimeFormat;
private currencyFormatter: Intl.NumberFormat;
private constructor() {
this.currentLocale = 'en-US';
this.messages = new Map();
// 初始化格式化器
this.numberFormatter = new Intl.NumberFormat(this.currentLocale);
this.dateFormatter = new Intl.DateTimeFormat(this.currentLocale);
this.currencyFormatter = new Intl.NumberFormat(this.currentLocale, {
style: 'currency',
currency: 'USD'
});
// 初始化事件分发
this.initializeEventDispatcher();
}
// 获取单例实例
static getInstance(): I18nManager {
if (!I18nManager.instance) {
I18nManager.instance = new I18nManager();
}
return I18nManager.instance;
}
// 设置语言环境
setLocale(locale: string): void {
this.currentLocale = locale;
// 更新格式化器
this.numberFormatter = new Intl.NumberFormat(locale);
this.dateFormatter = new Intl.DateTimeFormat(locale);
this.currencyFormatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: this.getCurrencyCode(locale)
});
// 触发语言变更事件
this.dispatchLocaleChange();
}
// 获取当前语言环境
getLocale(): string {
return this.currentLocale;
}
// 加载语言包
loadMessages(locale: string, messages: Record<string, string>): void {
this.messages.set(locale, messages);
}
// 获取翻译文本
translate(key: string, params?: Record<string, any>): string {
const messages = this.messages.get(this.currentLocale);
if (!messages) {
return key;
}
let text = messages[key] || key;
// 替换参数
if (params) {
Object.entries(params).forEach(([key, value]) => {
text = text.replace(`{${key}}`, String(value));
});
}
return text;
}
// 格式化数字
formatNumber(value: number): string {
return this.numberFormatter.format(value);
}
// 格式化日期
formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
if (options) {
return new Intl.DateTimeFormat(this.currentLocale, options).format(date);
}
return this.dateFormatter.format(date);
}
// 格式化货币
formatCurrency(value: number): string {
return this.currencyFormatter.format(value);
}
// 获取文字方向
getTextDirection(): 'ltr' | 'rtl' {
return ['ar', 'he'].includes(this.currentLocale.split('-')[0])
? 'rtl'
: 'ltr';
}
// 初始化事件分发
private initializeEventDispatcher(): void {
document.addEventListener('DOMContentLoaded', () => {
this.updateDocumentDirection();
});
}
// 更新文档方向
private updateDocumentDirection(): void {
document.documentElement.dir = this.getTextDirection();
document.documentElement.lang = this.currentLocale;
}
// 触发语言变更事件
private dispatchLocaleChange(): void {
const event = new CustomEvent('localechange', {
detail: { locale: this.currentLocale }
});
document.dispatchEvent(event);
// 更新文档方向
this.updateDocumentDirection();
}
// 获取货币代码
private getCurrencyCode(locale: string): string {
const region = locale.split('-')[1] || 'US';
const currencyMap: Record<string, string> = {
'US': 'USD',
'GB': 'GBP',
'EU': 'EUR',
'CN': 'CNY',
'JP': 'JPY'
// 添加更多货币映射
};
return currencyMap[region] || 'USD';
}
}
// 使用示例
const i18n = I18nManager.getInstance();
// 加载英文语言包
i18n.loadMessages('en-US', {
'greeting': 'Hello, {name}!',
'farewell': 'Goodbye!',
'items_count': 'You have {count} items.'
});
// 加载中文语言包
i18n.loadMessages('zh-CN', {
'greeting': '你好,{name}!',
'farewell': '再见!',
'items_count': '你有 {count} 个物品。'
});
// 切换语言
i18n.setLocale('zh-CN');
// 使用翻译
console.log(i18n.translate('greeting', { name: 'John' })); // 输出:你好,John!
console.log(i18n.translate('items_count', { count: 5 })); // 输出:你有 5 个物品。
// 格式化数字和日期
console.log(i18n.formatNumber(1234567.89)); // 输出:1,234,567.89
console.log(i18n.formatDate(new Date())); // 输出:2024/2/20
console.log(i18n.formatCurrency(99.99)); // 输出:¥99.99
组件国际化
// 国际化组件装饰器
function withI18n<T extends { new (...args: any[]): any }>(
Component: T
) {
return class extends Component {
private i18n: I18nManager;
private localeChangeHandler: () => void;
constructor(...args: any[]) {
super(...args);
this.i18n = I18nManager.getInstance();
this.localeChangeHandler = this.onLocaleChange.bind(this);
// 监听语言变更事件
document.addEventListener('localechange', this.localeChangeHandler);
}
// 组件销毁时移除事件监听
disconnectedCallback() {
document.removeEventListener('localechange', this.localeChangeHandler);
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
}
// 语言变更处理
private onLocaleChange(): void {
this.requestUpdate();
}
// 翻译辅助方法
protected t(key: string, params?: Record<string, any>): string {
return this.i18n.translate(key, params);
}
// 格式化数字
protected formatNumber(value: number): string {
return this.i18n.formatNumber(value);
}
// 格式化日期
protected formatDate(date: Date): string {
return this.i18n.formatDate(date);
}
// 格式化货币
protected formatCurrency(value: number): string {
return this.i18n.formatCurrency(value);
}
};
}
// 国际化文本组件
@withI18n
class I18nText extends HTMLElement {
private key: string;
private params: Record<string, any>;
constructor() {
super();
this.key = '';
this.params = {};
}
// 观察的属性
static get observedAttributes() {
return ['key', 'params'];
}
// 属性变化处理
attributeChangedCallback(
name: string,
oldValue: string,
newValue: string
) {
if (name === 'key') {
this.key = newValue;
} else if (name === 'params') {
try {
this.params = JSON.parse(newValue);
} catch (e) {
this.params = {};
}
}
this.updateContent();
}
// 更新内容
private updateContent(): void {
this.textContent = this.t(this.key, this.params);
}
}
// 注册组件
customElements.define('i18n-text', I18nText);
// 国际化日期组件
@withI18n
class I18nDate extends HTMLElement {
private date: Date;
private format: Intl.DateTimeFormatOptions;
constructor() {
super();
this.date = new Date();
this.format = {};
}
// 观察的属性
static get observedAttributes() {
return ['value', 'format'];
}
// 属性变化处理
attributeChangedCallback(
name: string,
oldValue: string,
newValue: string
) {
if (name === 'value') {
this.date = new Date(newValue);
} else if (name === 'format') {
try {
this.format = JSON.parse(newValue);
} catch (e) {
this.format = {};
}
}
this.updateContent();
}
// 更新内容
private updateContent(): void {
this.textContent = this.formatDate(this.date);
}
}
// 注册组件
customElements.define('i18n-date', I18nDate);
// 使用示例
const template = `
<div>
<i18n-text key="greeting" params='{"name":"John"}'></i18n-text>
<i18n-date value="2024-02-20"></i18n-date>
</div>
`;
路由国际化
// 国际化路由管理器
class I18nRouter {
private static instance: I18nRouter;
private i18n: I18nManager;
private routes: Map<string, I18nRoute>;
private currentRoute: I18nRoute | null;
private constructor() {
this.i18n = I18nManager.getInstance();
this.routes = new Map();
this.currentRoute = null;
this.initializeRouter();
}
// 获取单例实例
static getInstance(): I18nRouter {
if (!I18nRouter.instance) {
I18nRouter.instance = new I18nRouter();
}
return I18nRouter.instance;
}
// 注册路由
registerRoute(route: I18nRoute): void {
this.routes.set(route.name, route);
}
// 获取路由URL
getRouteUrl(
name: string,
params?: Record<string, string>
): string {
const route = this.routes.get(name);
if (!route) {
throw new Error(`Route "${name}" not found`);
}
const locale = this.i18n.getLocale();
let path = route.paths[locale] || route.paths['en-US'];
// 替换路径参数
if (params) {
Object.entries(params).forEach(([key, value]) => {
path = path.replace(`:${key}`, value);
});
}
return `/${locale}${path}`;
}
// 导航到路由
navigate(
name: string,
params?: Record<string, string>
): void {
const url = this.getRouteUrl(name, params);
window.history.pushState(null, '', url);
this.handleRoute();
}
// 初始化路由器
private initializeRouter(): void {
// 监听popstate事件
window.addEventListener('popstate', () => {
this.handleRoute();
});
// 监听语言变更
document.addEventListener('localechange', () => {
this.updateRoute();
});
// 处理初始路由
this.handleRoute();
}
// 处理路由
private handleRoute(): void {
const path = window.location.pathname;
const [, locale, ...segments] = path.split('/');
// 设置语言
if (locale && locale !== this.i18n.getLocale()) {
this.i18n.setLocale(locale);
}
// 查找匹配的路由
const matchedRoute = this.findMatchingRoute(segments.join('/'));
if (matchedRoute) {
this.currentRoute = matchedRoute;
this.renderRoute(matchedRoute);
}
}
// 更新当前路由
private updateRoute(): void {
if (this.currentRoute) {
const params = this.extractRouteParams();
this.navigate(this.currentRoute.name, params);
}
}
// 查找匹配的路由
private findMatchingRoute(path: string): I18nRoute | null {
const locale = this.i18n.getLocale();
for (const route of this.routes.values()) {
const routePath = route.paths[locale] || route.paths['en-US'];
if (this.matchPath(path, routePath)) {
return route;
}
}
return null;
}
// 匹配路径
private matchPath(path: string, pattern: string): boolean {
const pathSegments = path.split('/');
const patternSegments = pattern.split('/');
if (pathSegments.length !== patternSegments.length) {
return false;
}
return patternSegments.every((segment, index) => {
if (segment.startsWith(':')) {
return true;
}
return segment === pathSegments[index];
});
}
// 提取路由参数
private extractRouteParams(): Record<string, string> {
if (!this.currentRoute) {
return {};
}
const locale = this.i18n.getLocale();
const routePath = this.currentRoute.paths[locale] || this.currentRoute.paths['en-US'];
const currentPath = window.location.pathname.split('/').slice(2).join('/');
const params: Record<string, string> = {};
const pathSegments = currentPath.split('/');
const patternSegments = routePath.split('/');
patternSegments.forEach((segment, index) => {
if (segment.startsWith(':')) {
const paramName = segment.slice(1);
params[paramName] = pathSegments[index];
}
});
return params;
}
// 渲染路由
private renderRoute(route: I18nRoute): void {
const params = this.extractRouteParams();
route.component.render(params);
}
}
// 路由配置接口
interface I18nRoute {
name: string;
paths: Record<string, string>;
component: {
render: (params: Record<string, string>) => void;
};
}
// 使用示例
const router = I18nRouter.getInstance();
// 注册路由
router.registerRoute({
name: 'home',
paths: {
'en-US': '/home',
'zh-CN': '/首页'
},
component: {
render: () => {
document.getElementById('app')!.innerHTML = `
<h1><i18n-text key="home_title"></i18n-text></h1>
`;
}
}
});
router.registerRoute({
name: 'product',
paths: {
'en-US': '/product/:id',
'zh-CN': '/产品/:id'
},
component: {
render: (params) => {
document.getElementById('app')!.innerHTML = `
<h1>
<i18n-text key="product_title" params='{"id":"${params.id}"}'></i18n-text>
</h1>
`;
}
}
});
// 导航到路由
router.navigate('home');
router.navigate('product', { id: '123' });
最佳实践与建议
-
文本管理
- 使用键值对管理文本
- 支持参数替换
- 处理复数形式
- 维护翻译文档
-
格式处理
- 使用Intl API
- 处理时区问题
- 支持不同数字系统
- 考虑货币转换
-
布局适配
- 支持RTL布局
- 处理文本长度变化
- 适配不同字体
- 考虑文化差异
-
性能优化
- 按需加载语言包
- 缓存翻译结果
- 优化重渲染
- 减少格式化开销
总结
前端国际化需要考虑以下方面:
- 文本翻译管理
- 日期时间处理
- 货币数字格式化
- 布局方向适配
- 性能优化策略
通过合理的架构设计和优化措施,可以构建出优秀的国际化应用。
学习资源
- Intl API文档
- i18n最佳实践
- RTL布局指南
- 区域设置标准
- 国际化测试方法
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻