工程化与框架系列(28)--前端国际化实现

发布于:2025-03-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

前端国际化实现 🌍

引言

前端国际化(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' });

最佳实践与建议

  1. 文本管理

    • 使用键值对管理文本
    • 支持参数替换
    • 处理复数形式
    • 维护翻译文档
  2. 格式处理

    • 使用Intl API
    • 处理时区问题
    • 支持不同数字系统
    • 考虑货币转换
  3. 布局适配

    • 支持RTL布局
    • 处理文本长度变化
    • 适配不同字体
    • 考虑文化差异
  4. 性能优化

    • 按需加载语言包
    • 缓存翻译结果
    • 优化重渲染
    • 减少格式化开销

总结

前端国际化需要考虑以下方面:

  1. 文本翻译管理
  2. 日期时间处理
  3. 货币数字格式化
  4. 布局方向适配
  5. 性能优化策略

通过合理的架构设计和优化措施,可以构建出优秀的国际化应用。

学习资源

  1. Intl API文档
  2. i18n最佳实践
  3. RTL布局指南
  4. 区域设置标准
  5. 国际化测试方法

如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻