Skip to content

多语言支持系统

概述

Eagle2Ae AE 扩展 v2.4.0 引入了全新的多语言支持系统(Multi-Language Support System),支持中英文动态切换,为国际用户提供更好的使用体验。该系统采用现代化的国际化架构,支持语言包热加载、动态文本更新、上下文感知翻译等高级功能。

核心特性

动态语言切换

  • 支持运行时动态切换界面语言
  • 无需重启应用即可应用语言变更
  • 提供一键切换按钮,操作简便

语言包管理

  • 支持JSON格式的语言包文件
  • 提供语言包热加载机制
  • 支持语言包的版本管理和兼容性处理

上下文感知翻译

  • 根据界面上下文提供精准翻译
  • 支持变量插值和复数形式
  • 提供默认回退机制

自动文本更新

  • 文本变更时自动更新界面元素
  • 支持批量文本更新
  • 提供更新进度反馈

国际化API

  • 完整的JavaScript API用于语言管理
  • 支持自定义翻译函数
  • 提供翻译缓存机制

技术实现

核心类结构

javascript
/**
 * 国际化管理器
 * 负责管理扩展的多语言支持,支持中英文动态切换和上下文感知翻译
 */
class I18nManager {
    /**
     * 构造函数
     * @param {Object} options - 配置选项
     */
    constructor(options = {}) {
        this.options = {
            defaultLanguage: 'zh-CN',
            supportedLanguages: ['zh-CN', 'en-US'],
            fallbackLanguage: 'zh-CN',
            enableLogging: true,
            cacheTimeout: 30000, // 30秒缓存超时
            ...options
        };

        // 初始化状态
        this.currentLang = this.options.defaultLanguage;
        this.translations = new Map();
        this.translationCache = new Map();
        this.cacheHits = 0;
        this.cacheMisses = 0;
        this.changeListeners = [];
        
        // 绑定方法上下文
        this.setLanguage = this.setLanguage.bind(this);
        this.getText = this.getText.bind(this);
        this.updatePageTexts = this.updatePageTexts.bind(this);
        this.loadLanguagePack = this.loadLanguagePack.bind(this);
        this.translateElement = this.translateElement.bind(this);
        
        this.log('🌐 国际化管理器已初始化', 'debug');
    }
}

语言切换实现

javascript
/**
 * 设置语言
 * @param {string} lang - 语言代码 ('zh-CN' | 'en-US')
 * @param {boolean} updateUI - 是否更新UI
 * @returns {Promise<boolean>} 是否设置成功
 */
async setLanguage(lang, updateUI = true) {
    try {
        // 验证语言代码
        if (!this.options.supportedLanguages.includes(lang)) {
            throw new Error(`不支持的语言: ${lang}`);
        }

        // 保存旧语言
        const oldLang = this.currentLang;

        // 更新当前语言
        this.currentLang = lang;

        // 保存到localStorage
        try {
            localStorage.setItem('language', lang);
            localStorage.setItem('lang', lang);
        } catch (storageError) {
            this.log(`⚠️ 无法保存语言设置到localStorage: ${storageError.message}`, 'warning');
        }

        this.log(`🌐 语言已切换: ${oldLang} -> ${lang}`, 'info');

        // 加载对应的语言包
        const loadResult = await this.loadLanguagePack(lang);
        if (!loadResult.success) {
            throw new Error(`加载语言包失败: ${loadResult.error}`);
        }

        // 更新UI文本
        if (updateUI) {
            await this.updatePageTexts();
        }

        // 触发语言变更事件
        this.emitLanguageChange(oldLang, lang);

        return true;

    } catch (error) {
        this.log(`❌ 设置语言失败: ${error.message}`, 'error');
        return false;
    }
}

/**
 * 切换语言
 * @returns {Promise<boolean>} 是否切换成功
 */
async toggleLanguage() {
    try {
        const currentLang = this.getCurrentLanguage();
        const nextLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN';

        this.log(`🔄 切换语言: ${currentLang} -> ${nextLang}`, 'info');

        // 设置新语言
        const result = await this.setLanguage(nextLang, true);

        if (result) {
            // 更新按钮状态
            this.updateLanguageToggleButton(nextLang);

            // 保存设置
            try {
                localStorage.setItem('language', nextLang);
                localStorage.setItem('lang', nextLang);
            } catch (storageError) {
                this.log(`⚠️ 无法保存语言设置: ${storageError.message}`, 'warning');
            }

            this.log(`✅ 语言已切换为: ${nextLang}`, 'success');
        } else {
            this.log(`❌ 语言切换失败`, 'error');
        }

        return result;

    } catch (error) {
        this.log(`❌ 切换语言异常: ${error.message}`, 'error');
        return false;
    }
}

/**
 * 更新语言切换按钮状态
 * @param {string} lang - 当前语言
 */
updateLanguageToggleButton(lang) {
    const langBtn = document.getElementById('language-toggle-btn');
    if (!langBtn) return;

    const iconSpan = langBtn.querySelector('.icon');
    const isEn = lang === 'en-US';

    // 更新按钮属性
    langBtn.setAttribute('aria-pressed', String(isEn));
    langBtn.title = isEn ? 'Switch to Chinese' : '切换为中文';

    // 更新图标
    if (iconSpan) {
        iconSpan.textContent = isEn ? '🇨🇳' : '🌐';
    }

    this.log(`🔄 语言切换按钮状态已更新: ${lang}`, 'debug');
}

语言包加载实现

javascript
/**
 * 加载语言包
 * @param {string} lang - 语言代码
 * @returns {Promise<Object>} 加载结果
 */
async loadLanguagePack(lang) {
    try {
        this.log(`📥 开始加载语言包: ${lang}`, 'debug');

        // 检查缓存
        const cached = this.getCachedTranslation(lang);
        if (cached) {
            this.translations.set(lang, cached);
            this.log(`キャッシング 从缓存加载语言包: ${lang}`, 'debug');
            this.cacheHits++;
            return { success: true, translations: cached };
        }

        this.cacheMisses++;

        // 构造语言包URL
        const langPackUrl = `./i18n/${lang}.json`;

        // 加载语言包
        const response = await fetch(langPackUrl);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const translations = await response.json();

        // 验证语言包结构
        if (!this.validateLanguagePack(translations)) {
            throw new Error('语言包格式无效');
        }

        // 缓存语言包
        this.cacheTranslation(lang, translations);

        // 保存到内存
        this.translations.set(lang, translations);

        this.log(`✅ 语言包加载成功: ${lang} (${Object.keys(translations).length} 个翻译条目)`, 'success');

        return { success: true, translations: translations };

    } catch (error) {
        this.log(`❌ 加载语言包失败: ${error.message}`, 'error');

        // 尝试加载回退语言包
        if (lang !== this.options.fallbackLanguage) {
            this.log(`🔄 尝试加载回退语言包: ${this.options.fallbackLanguage}`, 'debug');
            return await this.loadLanguagePack(this.options.fallbackLanguage);
        }

        return { success: false, error: error.message };
    }
}

/**
 * 验证语言包结构
 * @param {Object} translations - 翻译对象
 * @returns {boolean} 是否有效
 */
validateLanguagePack(translations) {
    if (!translations || typeof translations !== 'object') {
        return false;
    }

    // 检查必需的翻译条目
    const requiredKeys = [
        'common.projectInfo',
        'common.connectionStatus',
        'common.notConnected',
        'common.version',
        'common.path',
        'common.project',
        'common.composition',
        'common.eagle',
        'common.library',
        'common.currentGroup',
        'common.importModeBehavior',
        'common.directImport',
        'common.projectAdjacentCopy',
        'common.customFolder',
        'common.doNotImportToComp',
        'common.createPrecomp',
        'common.currentTime',
        'common.timelineStart',
        'common.detectLayers',
        'common.exportLayers',
        'common.exportToEagle',
        'common.statusInfo',
        'common.waitingForImport',
        'common.log',
        'common.clearLog',
        'common.switchLog',
        'common.settings',
        'common.themeToggle',
        'common.showHideLog',
        'common.testConnection',
        'common.chooseTargetFolder',
        'common.dragFolderHere',
        'common.orClickSelect',
        'common.manualPathInput',
        'common.inputFolderPath',
        'common.customFolderName',
        'common.validate',
        'common.cancel',
        'common.confirm',
        'common.advancedSettings',
        'common.importMode',
        'common.importBehavior',
        'common.exportPathSettings',
        'common.desktopExport',
        'common.projectAdjacentExport',
        'common.automaticCopy',
        'common.burnAfterReading',
        'common.timestampPrefix',
        'common.compositionNamePrefix',
        'common.communicationSettings',
        'common.communicationPortSettings',
        'common.communicationPort',
        'common.portRange',
        'common.uiSettings',
        'common.theme',
        'common.language',
        'common.log',
        'common.logPanel',
        'common.presetManagement',
        'common.resetToDefault',
        'common.openPresetDirectory',
        'common.customPresetDirectory',
        'common.projectAdjacentSettings',
        'common.customFolderSettings',
        'common.selectFolder',
        'common.recentlyUsedFolders',
        'common.deleteRecord',
        'common.userCancelled',
        'common.invalidFolderPath',
        'common.folderNameInvalidChars',
        'common.pleaseEnterFolderName',
        'common.invalidPortRange',
        'common.settingsSaved',
        'common.connectionTest',
        'common.connectionTestSuccess',
        'common.connectionTestFailed',
        'common.language',
        'common.switchLanguage',
        'common.unknown',
        'common.none',
        'common.noProjectOpen',
        'common.connectedDemo',
        'common.disconnectedDemo',
        'common.connectingDemo',
        'common.connectionErrorDemo',
        'common.connecting',
        'common.disconnecting',
        'common.connected'
    ];

    for (const key of requiredKeys) {
        if (!translations.hasOwnProperty(key)) {
            this.log(`⚠️ 缺少必需的翻译条目: ${key}`, 'warning');
            return false;
        }
    }

    return true;
}

文本翻译实现

javascript
/**
 * 获取翻译文本
 * @param {string} key - 翻译键
 * @param {Object} variables - 变量替换对象
 * @returns {string} 翻译文本
 */
getText(key, variables = {}) {
    try {
        // 获取当前语言的翻译
        const currentTranslations = this.translations.get(this.currentLang) || {};
        let translation = currentTranslations[key];

        // 如果没有找到翻译,尝试回退语言
        if (!translation && this.currentLang !== this.options.fallbackLanguage) {
            const fallbackTranslations = this.translations.get(this.options.fallbackLanguage) || {};
            translation = fallbackTranslations[key];
        }

        // 如果仍然没有找到翻译,返回键名作为默认值
        if (!translation) {
            this.log(`⚠️ 未找到翻译: ${key}`, 'debug');
            return key;
        }

        // 处理变量替换
        if (variables && typeof variables === 'object') {
            translation = this.interpolateVariables(translation, variables);
        }

        return translation;

    } catch (error) {
        this.log(`获取翻译失败: ${error.message}`, 'error');
        return key; // 返回键名作为回退
    }
}

/**
 * 变量插值
 * @param {string} text - 原始文本
 * @param {Object} variables - 变量对象
 * @returns {string} 插值后的文本
 */
interpolateVariables(text, variables) {
    try {
        let interpolatedText = text;

        // 使用正则表达式替换变量
        for (const [key, value] of Object.entries(variables)) {
            const regex = new RegExp(`{{${key}}}`, 'g');
            interpolatedText = interpolatedText.replace(regex, value);
        }

        return interpolatedText;

    } catch (error) {
        this.log(`变量插值失败: ${error.message}`, 'error');
        return text;
    }
}

/**
 * 更新页面文本
 * @param {boolean} forceUpdate - 是否强制更新
 * @returns {Promise<void>}
 */
async updatePageTexts(forceUpdate = false) {
    try {
        this.log('🔄 开始更新页面文本...', 'debug');

        // 获取所有需要翻译的元素
        const translatableElements = document.querySelectorAll('[data-i18n], [data-i18n-title], [data-i18n-placeholder]');

        this.log(`🔍 找到 ${translatableElements.length} 个可翻译元素`, 'debug');

        // 批量更新文本
        const batchSize = 50;
        for (let i = 0; i < translatableElements.length; i += batchSize) {
            const batch = Array.from(translatableElements).slice(i, i + batchSize);
            
            batch.forEach(element => {
                this.translateElement(element, forceUpdate);
            });

            // 避免阻塞UI线程
            if (i + batchSize < translatableElements.length) {
                await new Promise(resolve => setTimeout(resolve, 10));
            }
        }

        this.log('✅ 页面文本更新完成', 'success');

        // 触发更新完成事件
        this.emit('textUpdated', {
            language: this.currentLang,
            elementCount: translatableElements.length
        });

    } catch (error) {
        this.log(`❌ 更新页面文本失败: ${error.message}`, 'error');
    }
}

/**
 * 翻译单个元素
 * @param {HTMLElement} element - 要翻译的元素
 * @param {boolean} forceUpdate - 是否强制更新
 */
translateElement(element, forceUpdate = false) {
    try {
        // 检查元素是否需要更新
        const lastLang = element.dataset.lastLang || '';
        if (!forceUpdate && lastLang === this.currentLang) {
            return; // 已经是当前语言,无需更新
        }

        // 翻译文本内容
        const i18nKey = element.dataset.i18n;
        if (i18nKey) {
            const translation = this.getText(i18nKey);
            if (translation !== i18nKey || forceUpdate) {
                element.textContent = translation;
            }
        }

        // 翻译标题属性
        const i18nTitleKey = element.dataset.i18nTitle;
        if (i18nTitleKey) {
            const titleTranslation = this.getText(i18nTitleKey);
            if (titleTranslation !== i18nTitleKey || forceUpdate) {
                element.title = titleTranslation;
            }
        }

        // 翻译占位符属性
        const i18nPlaceholderKey = element.dataset.i18nPlaceholder;
        if (i18nPlaceholderKey) {
            const placeholderTranslation = this.getText(i18nPlaceholderKey);
            if (placeholderTranslation !== i18nPlaceholderKey || forceUpdate) {
                element.placeholder = placeholderTranslation;
            }
        }

        // 记录最后更新的语言
        element.dataset.lastLang = this.currentLang;

    } catch (error) {
        this.log(`翻译元素失败: ${error.message}`, 'error');
    }
}

语言包缓存实现

javascript
/**
 * 缓存翻译结果
 * @param {string} lang - 语言代码
 * @param {Object} translations - 翻译对象
 */
cacheTranslation(lang, translations) {
    try {
        const cacheKey = `i18n_${lang}`;
        const cacheEntry = {
            data: translations,
            timestamp: Date.now(),
            expiry: Date.now() + this.options.cacheTimeout
        };

        this.translationCache.set(cacheKey, cacheEntry);
        this.log(`キャッシング 已缓存语言包: ${lang}`, 'debug');

    } catch (error) {
        this.log(`缓存语言包失败: ${error.message}`, 'error');
    }
}

/**
 * 获取缓存的翻译结果
 * @param {string} lang - 语言代码
 * @returns {Object|null} 缓存的翻译对象或null
 */
getCachedTranslation(lang) {
    try {
        const cacheKey = `i18n_${lang}`;

        if (this.translationCache.has(cacheKey)) {
            const cacheEntry = this.translationCache.get(cacheKey);

            // 检查是否过期
            if (Date.now() > cacheEntry.expiry) {
                this.translationCache.delete(cacheKey);
                this.log(`キャッシング 语言包缓存已过期: ${lang}`, 'debug');
                return null;
            }

            this.log(`キャッシング 从缓存获取语言包: ${lang}`, 'debug');
            return cacheEntry.data;

        } else {
            this.log(`キャッシング 未找到语言包缓存: ${lang}`, 'debug');
            return null;
        }

    } catch (error) {
        this.log(`获取缓存翻译失败: ${error.message}`, 'error');
        return null;
    }
}

/**
 * 清除语言包缓存
 * @param {string} lang - 可选的语言代码,如果不提供则清除所有缓存
 */
clearTranslationCache(lang = null) {
    try {
        if (lang) {
            const cacheKey = `i18n_${lang}`;
            this.translationCache.delete(cacheKey);
            this.log(`キャッシング 已清除语言包缓存: ${lang}`, 'debug');
        } else {
            const cacheSize = this.translationCache.size;
            this.translationCache.clear();
            this.cacheHits = 0;
            this.cacheMisses = 0;
            this.log(`キャッシング 已清除所有语言包缓存 (${cacheSize} 个项目)`, 'debug');
        }

    } catch (error) {
        this.log(`清除语言包缓存失败: ${error.message}`, 'error');
    }
}

/**
 * 获取缓存信息
 * @returns {Object} 缓存统计信息
 */
getCacheInfo() {
    try {
        const totalRequests = this.cacheHits + this.cacheMisses;
        const hitRate = totalRequests > 0 ? Math.round((this.cacheHits / totalRequests) * 100) : 0;

        return {
            size: this.translationCache.size,
            hits: this.cacheHits,
            misses: this.cacheMisses,
            hitRate: hitRate,
            hitRatio: totalRequests > 0 ? (this.cacheHits / totalRequests).toFixed(2) : '0.00'
        };

    } catch (error) {
        this.log(`获取缓存信息失败: ${error.message}`, 'error');
        return {
            size: 0,
            hits: 0,
            misses: 0,
            hitRate: 0,
            hitRatio: '0.00'
        };
    }
}

事件系统实现

javascript
/**
 * 触发语言变更事件
 * @param {string} oldLang - 旧语言
 * @param {string} newLang - 新语言
 */
emitLanguageChange(oldLang, newLang) {
    try {
        // 触发自定义事件
        const event = new CustomEvent('languagechange', {
            detail: {
                oldLanguage: oldLang,
                newLanguage: newLang,
                timestamp: Date.now()
            }
        });

        document.dispatchEvent(event);

        // 触发通用变更事件
        this.emit('languageChange', {
            oldLanguage: oldLang,
            newLanguage: newLang
        });

        this.log(`🔊 语言变更事件已触发: ${oldLang} -> ${newLang}`, 'debug');

    } catch (error) {
        this.log(`触发语言变更事件失败: ${error.message}`, 'error');
    }
}

/**
 * 添加语言变更监听器
 * @param {Function} listener - 监听器函数
 * @returns {Function} 移除监听器的函数
 */
addLanguageChangeListener(listener) {
    try {
        if (typeof listener !== 'function') {
            throw new Error('监听器必须是函数');
        }

        // 添加到变更监听器列表
        this.changeListeners.push(listener);

        this.log('👂 已添加语言变更监听器', 'debug');

        // 返回移除监听器的函数
        return () => {
            const index = this.changeListeners.indexOf(listener);
            if (index !== -1) {
                this.changeListeners.splice(index, 1);
                this.log('👂 已移除语言变更监听器', 'debug');
            }
        };

    } catch (error) {
        this.log(`添加语言变更监听器失败: ${error.message}`, 'error');
        return () => {}; // 返回空函数以防调用错误
    }
}

/**
 * 移除语言变更监听器
 * @param {Function} listener - 监听器函数
 */
removeLanguageChangeListener(listener) {
    try {
        const index = this.changeListeners.indexOf(listener);
        if (index !== -1) {
            this.changeListeners.splice(index, 1);
            this.log('👂 已移除语言变更监听器', 'debug');
        }

    } catch (error) {
        this.log(`移除语言变更监听器失败: ${error.message}`, 'error');
    }
}

语言包格式

JSON结构

json
{
  "common": {
    "projectInfo": "项目信息",
    "connectionStatus": "连接状态",
    "notConnected": "未连接",
    "version": "版本",
    "path": "路径",
    "project": "项目",
    "composition": "合成",
    "eagle": "Eagle",
    "library": "资源库",
    "currentGroup": "当前组",
    "importModeBehavior": "导入模式&行为",
    "directImport": "直接导入",
    "projectAdjacentCopy": "项目旁复制",
    "customFolder": "指定文件夹",
    "doNotImportToComp": "不导入合成",
    "createPrecomp": "创建预合成",
    "currentTime": "当前时间",
    "timelineStart": "时间轴开始",
    "detectLayers": "检测图层",
    "exportLayers": "导出图层",
    "exportToEagle": "导出到Eagle",
    "statusInfo": "状态信息",
    "waitingForImport": "等待导入请求...",
    "log": "日志",
    "clearLog": "清理日志",
    "switchLog": "切换到Eagle日志",
    "settings": "高级设置",
    "themeToggle": "切换明暗模式",
    "showHideLog": "显示/隐藏日志",
    "testConnection": "点击测试连接",
    "chooseTargetFolder": "选择目标文件夹",
    "dragFolderHere": "将文件夹拖拽到这里",
    "orClickSelect": "或点击选择文件夹",
    "manualPathInput": "或手动输入路径:",
    "inputFolderPath": "输入文件夹路径...",
    "customFolderName": "或输入自定义文件夹名",
    "validate": "验证",
    "cancel": "取消",
    "confirm": "确认",
    "advancedSettings": "高级设置",
    "importMode": "导入模式",
    "importBehavior": "导入行为",
    "exportPathSettings": "导出路径设置",
    "desktopExport": "桌面导出",
    "projectAdjacentExport": "项目旁导出",
    "automaticCopy": "自动复制",
    "burnAfterReading": "阅后即焚",
    "timestampPrefix": "时间戳前缀",
    "compositionNamePrefix": "合成名前缀",
    "communicationSettings": "通信设置",
    "communicationPortSettings": "通信端口设置",
    "communicationPort": "通信端口",
    "portRange": "端口范围: 1024-65535,修改后需重新连接",
    "uiSettings": "UI 设置",
    "theme": "主题",
    "language": "语言",
    "log": "日志",
    "logPanel": "日志面板",
    "presetManagement": "预设管理",
    "resetToDefault": "重置默认",
    "openPresetDirectory": "打开预设目录",
    "customPresetDirectory": "自定义预设目录",
    "projectAdjacentSettings": "项目旁复制设置",
    "customFolderSettings": "指定文件夹设置",
    "selectFolder": "选择目标文件夹路径",
    "recentlyUsedFolders": "最近使用的文件夹",
    "deleteRecord": "删除此记录",
    "userCancelled": "用户取消了文件夹选择",
    "invalidFolderPath": "请选择文件夹路径",
    "folderNameInvalidChars": "文件夹名称只能包含字母、数字、下划线、中划线和中文字符",
    "pleaseEnterFolderName": "请输入自定义文件夹名称",
    "invalidPortRange": "端口必须在1024-65535范围内",
    "settingsSaved": "设置已保存",
    "connectionTest": "连接测试",
    "connectionTestSuccess": "连接测试成功!",
    "connectionTestFailed": "连接测试失败:",
    "language": "语言",
    "switchLanguage": "切换语言",
    "unknown": "未知",
    "none": "无",
    "noProjectOpen": "未打开项目",
    "connectedDemo": "已连接 (演示)",
    "disconnectedDemo": "未连接 (演示)",
    "connectingDemo": "连接中 (演示)",
    "connectionErrorDemo": "连接失败 (演示)",
    "connecting": "连接中...",
    "disconnecting": "断开连接中...",
    "connected": "已连接"
  },
  "tooltips": {
    "directImport": "从源目录直接导入到AE项目,不复制文件",
    "projectAdjacentCopy": "复制文件到AE项目文件旁边后导入",
    "customFolder": "复制文件到指定文件夹后导入",
    "currentTime": "将素材放置在当前时间指针位置",
    "timelineStart": "将素材移至时间轴开始处(0秒位置)",
    "detectLayers": "检测当前合成中的图层",
    "exportLayers": "导出选中的图层",
    "exportToEagle": "将AE图层数据导出到Eagle资源库",
    "desktopExport": "直接导出到桌面",
    "projectAdjacentExport": "导出到AE项目文件旁边的文件夹(与导入模式设置一致)",
    "customFolderExport": "导出到指定文件夹(与导入模式设置一致)",
    "automaticCopy": "导出完成后自动复制导出路径和文件到剪切板,可直接粘贴使用",
    "burnAfterReading": "启用后图片导出到临时文件夹,导出后复制到剪切板。\n文件累计超过100MB或100个文件后自动清空。\nAlt+点击清空,Ctrl+点击打开临时文件夹。",
    "timestampPrefix": "在导出文件夹名称前添加时间戳,如:20240101_120000_Export",
    "compositionNamePrefix": "在导出文件夹名称前添加当前合成名称,如:MyComp_Export",
    "resetToDefault": "重置为默认并写入预设JSON",
    "openPresetDirectory": "打开当前预设目录",
    "customPresetDirectory": "自定义预设目录",
    "uiToggleTheme": "点击切换主题按钮的显示/隐藏",
    "uiToggleLanguage": "点击切换语言按钮的显示/隐藏",
    "uiToggleLog": "点击切换日志按钮的显示/隐藏",
    "uiToggleProjectInfo": "点击切换项目信息面板的显示/隐藏",
    "uiToggleLogPanel": "点击切换日志面板的显示/隐藏"
  }
}

使用指南

基本使用

语言切换

javascript
// 创建国际化管理器实例
const i18nManager = new I18nManager({
    defaultLanguage: 'zh-CN',
    supportedLanguages: ['zh-CN', 'en-US'],
    fallbackLanguage: 'zh-CN'
});

// 切换语言
await i18nManager.setLanguage('en-US');

// 切换语言(自动更新UI)
await i18nManager.toggleLanguage();

// 获取翻译文本
const translatedText = i18nManager.getText('common.projectInfo');
console.log(translatedText); // 输出: "Project Information" (英文) 或 "项目信息" (中文)

动态文本更新

javascript
// 更新页面文本
await i18nManager.updatePageTexts();

// 强制更新页面文本
await i18nManager.updatePageTexts(true);

// 翻译特定元素
const element = document.querySelector('[data-i18n="common.projectInfo"]');
i18nManager.translateElement(element);

事件监听

javascript
// 添加语言变更监听器
const removeListener = i18nManager.addLanguageChangeListener((oldLang, newLang) => {
    console.log(`语言已从 ${oldLang} 变更为 ${newLang}`);
    
    // 根据新语言更新UI
    updateUIForLanguage(newLang);
});

// 使用完成后移除监听器
// removeListener();

// 监听语言变更事件
document.addEventListener('languagechange', (event) => {
    const { oldLanguage, newLanguage } = event.detail;
    console.log(`语言变更事件: ${oldLanguage} -> ${newLanguage}`);
    
    // 处理语言变更
    handleLanguageChange(oldLanguage, newLanguage);
});

高级使用

变量插值

javascript
// 使用变量插值
const message = i18nManager.getText('common.connectionTestFailed', {
    error: '连接超时'
});
console.log(message); // 输出: "Connection test failed: 连接超时"

自定义翻译函数

javascript
// 添加自定义翻译函数
i18nManager.addCustomTranslator('plurals', (key, count) => {
    // 处理复数形式
    const pluralForms = {
        'file': ['文件', '文件'], // 中文没有复数形式
        'files': ['file', 'files'] // 英文有复数形式
    };
    
    const forms = pluralForms[key] || [key, key];
    return count === 1 ? forms[0] : forms[1];
});

// 使用自定义翻译函数
const pluralText = i18nManager.translate('plurals', 'files', 5);
console.log(pluralText); // 输出: "files"

语言包热加载

javascript
// 热加载语言包
async function hotReloadLanguagePack(lang) {
    try {
        // 清除缓存
        i18nManager.clearTranslationCache(lang);
        
        // 重新加载语言包
        const loadResult = await i18nManager.loadLanguagePack(lang);
        
        if (loadResult.success) {
            // 更新UI
            await i18nManager.updatePageTexts(true);
            
            console.log(`✅ 语言包 ${lang} 热加载成功`);
        } else {
            console.error(`❌ 语言包 ${lang} 热加载失败: ${loadResult.error}`);
        }
        
    } catch (error) {
        console.error(`热加载语言包异常: ${error.message}`);
    }
}

最佳实践

语言包开发建议

结构化组织

json
{
  "common": {
    "navigation": {
      "home": "首页",
      "about": "关于",
      "contact": "联系"
    },
    "actions": {
      "save": "保存",
      "delete": "删除",
      "cancel": "取消",
      "confirm": "确认"
    },
    "status": {
      "loading": "加载中...",
      "success": "成功",
      "error": "错误",
      "warning": "警告"
    }
  },
  "pages": {
    "home": {
      "title": "主页",
      "welcome": "欢迎使用Eagle2Ae",
      "description": "连接Eagle与After Effects的强大工具"
    },
    "settings": {
      "title": "设置",
      "general": "通用设置",
      "advanced": "高级设置",
      "import": "导入设置",
      "export": "导出设置"
    }
  },
  "components": {
    "dialog": {
      "confirm": {
        "title": "确认操作",
        "message": "您确定要执行此操作吗?",
        "buttons": {
          "confirm": "确认",
          "cancel": "取消"
        }
      },
      "alert": {
        "title": "提示",
        "buttons": {
          "ok": "确定"
        }
      }
    }
  }
}

一致性原则

javascript
// 保持术语一致性
const consistentTerms = {
    'zh-CN': {
        'import': '导入',
        'export': '导出',
        'settings': '设置',
        'preferences': '偏好',
        'configuration': '配置',
        'options': '选项'
    },
    'en-US': {
        'import': 'Import',
        'export': 'Export',
        'settings': 'Settings',
        'preferences': 'Preferences',
        'configuration': 'Configuration',
        'options': 'Options'
    }
};

上下文感知

javascript
// 根据上下文提供不同的翻译
const contextAwareTranslations = {
    'save': {
        'file': '保存文件',
        'project': '保存项目',
        'preset': '保存预设',
        'default': '保存'
    },
    'delete': {
        'file': '删除文件',
        'project': '删除项目',
        'preset': '删除预设',
        'default': '删除'
    }
};

// 在代码中使用上下文感知翻译
function getTranslatedText(key, context = 'default') {
    const translations = contextAwareTranslations[key];
    return translations ? translations[context] || translations.default : key;
}

性能优化

缓存策略

javascript
// 实现LRU缓存
class LRUCache {
    constructor(maxSize = 100) {
        this.maxSize = maxSize;
        this.cache = new Map();
    }
    
    get(key) {
        if (this.cache.has(key)) {
            const value = this.cache.get(key);
            // 更新访问顺序
            this.cache.delete(key);
            this.cache.set(key, value);
            return value;
        }
        return undefined;
    }
    
    set(key, value) {
        if (this.cache.has(key)) {
            this.cache.delete(key);
        } else if (this.cache.size >= this.maxSize) {
            // 移除最少使用的项
            const firstKey = this.cache.keys().next().value;
            this.cache.delete(firstKey);
        }
        
        this.cache.set(key, value);
    }
}

// 使用LRU缓存优化翻译性能
const translationCache = new LRUCache(200);

function getCachedTranslation(key, lang) {
    const cacheKey = `${lang}_${key}`;
    return translationCache.get(cacheKey);
}

function setCachedTranslation(key, lang, value) {
    const cacheKey = `${lang}_${key}`;
    translationCache.set(cacheKey, value);
}

批量更新

javascript
// 批量更新文本以提高性能
async function batchUpdateTexts(elements, translations) {
    const batchSize = 50;
    
    for (let i = 0; i < elements.length; i += batchSize) {
        const batch = elements.slice(i, i + batchSize);
        
        batch.forEach(element => {
            const key = element.dataset.i18n;
            if (key && translations[key]) {
                element.textContent = translations[key];
            }
        });
        
        // 避免阻塞UI线程
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}

防抖处理

javascript
// 使用防抖避免频繁的语言切换
const debounce = (func, wait) => {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
};

// 防抖处理语言切换
const debouncedLanguageChange = debounce((lang) => {
    i18nManager.setLanguage(lang);
}, 300);

故障排除

常见问题

翻译文本未更新

  • 症状:切换语言后部分文本未更新
  • 解决
    1. 检查元素是否正确添加了data-i18n属性
    2. 验证语言包中是否包含对应键的翻译
    3. 手动调用i18n.updatePageTexts()方法

语言包加载失败

  • 症状:无法加载语言包,显示默认文本
  • 解决
    1. 检查语言包文件路径是否正确
    2. 验证JSON格式是否有效
    3. 确认网络连接是否正常

语言切换按钮无反应

  • 症状:点击语言切换按钮后无任何反应
  • 解决
    1. 检查按钮事件监听器是否正确绑定
    2. 验证localStorage权限
    3. 重启AE和扩展面板

调试技巧

启用详细日志

javascript
// 在控制台中启用详细日志
localStorage.setItem('debugLogLevel', '0');

// 监控语言变更事件
document.addEventListener('languagechange', (event) => {
    console.log('语言变更事件:', event.detail);
});

// 监控翻译过程
i18nManager.addListener((eventType, data) => {
    if (eventType === 'textUpdated') {
        console.log('文本更新完成:', data);
    } else if (eventType === 'languageChange') {
        console.log('语言变更:', data);
    }
});

检查缓存状态

javascript
// 检查翻译缓存状态
function inspectTranslationCache() {
    const cacheInfo = i18nManager.getCacheInfo();
    console.log('翻译缓存信息:', cacheInfo);
    
    // 检查缓存命中率
    if (cacheInfo.hitRate < 50) {
        console.warn('缓存命中率较低,请检查翻译调用模式');
    }
}

性能监控

javascript
// 监控翻译性能
const performanceMarkers = [];

function markTranslationStep(step) {
    performance.mark(`translation-${step}`);
    performanceMarkers.push(step);
}

function measureTranslationPerformance() {
    for (let i = 1; i < performanceMarkers.length; i++) {
        const start = `translation-${performanceMarkers[i-1]}`;
        const end = `translation-${performanceMarkers[i]}`;
        performance.measure(`translation-${performanceMarkers[i-1]}-to-${performanceMarkers[i]}`, start, end);
    }
    
    const measures = performance.getEntriesByType('measure');
    measures.forEach(measure => {
        console.log(`⏱️ ${measure.name}: ${measure.duration}ms`);
    });
}

扩展性

自定义扩展

扩展国际化管理器

javascript
// 创建自定义国际化管理器
class CustomI18nManager extends I18nManager {
    constructor(options = {}) {
        super(options);
        this.customTranslators = new Map();
        this.translationHooks = new Map();
    }
    
    /**
     * 添加自定义翻译器
     * @param {string} name - 翻译器名称
     * @param {Function} translator - 翻译器函数
     */
    addCustomTranslator(name, translator) {
        this.customTranslators.set(name, translator);
    }
    
    /**
     * 执行自定义翻译器
     * @param {string} name - 翻译器名称
     * @param {...any} args - 参数
     * @returns {any} 翻译结果
     */
    executeCustomTranslator(name, ...args) {
        const translator = this.customTranslators.get(name);
        if (translator && typeof translator === 'function') {
            return translator(...args);
        }
        throw new Error(`未知的自定义翻译器: ${name}`);
    }
    
    /**
     * 添加翻译钩子
     * @param {string} hookPoint - 钩子点
     * @param {Function} hook - 钩子函数
     */
    addTranslationHook(hookPoint, hook) {
        if (!this.translationHooks.has(hookPoint)) {
            this.translationHooks.set(hookPoint, []);
        }
        
        this.translationHooks.get(hookPoint).push(hook);
    }
    
    /**
     * 执行翻译钩子
     * @param {string} hookPoint - 钩子点
     * @param {...any} args - 参数
     */
    executeTranslationHooks(hookPoint, ...args) {
        if (this.translationHooks.has(hookPoint)) {
            const hooks = this.translationHooks.get(hookPoint);
            hooks.forEach(hook => {
                try {
                    hook(...args);
                } catch (error) {
                    this.log(`翻译钩子执行失败: ${error.message}`, 'error');
                }
            });
        }
    }
}

// 使用自定义国际化管理器
const customI18nManager = new CustomI18nManager({
    defaultLanguage: 'zh-CN',
    supportedLanguages: ['zh-CN', 'en-US', 'ja-JP', 'ko-KR'],
    fallbackLanguage: 'zh-CN'
});

// 添加自定义翻译器
customI18nManager.addCustomTranslator('plurals', (count, singular, plural) => {
    return count === 1 ? singular : plural;
});

// 添加翻译钩子
customI18nManager.addTranslationHook('beforeTranslate', (key, lang) => {
    console.log(`准备翻译: ${key} (${lang})`);
});

customI18nManager.addTranslationHook('afterTranslate', (key, lang, result) => {
    console.log(`翻译完成: ${key} (${lang}) -> ${result}`);
});

插件化架构

javascript
// 创建国际化插件
class I18nPlugin {
    constructor(i18nManager) {
        this.i18nManager = i18nManager;
        this.init();
    }
    
    init() {
        // 注册插件特定的语言包
        this.registerPluginLanguagePacks();
        
        // 添加插件特定的翻译器
        this.addPluginTranslators();
        
        // 绑定插件事件
        this.bindPluginEvents();
    }
    
    /**
     * 注册插件特定的语言包
     */
    registerPluginLanguagePacks() {
        const pluginLanguagePacks = {
            'zh-CN': {
                'plugin.name': '插件名称',
                'plugin.description': '插件描述',
                'plugin.version': '插件版本'
            },
            'en-US': {
                'plugin.name': 'Plugin Name',
                'plugin.description': 'Plugin Description',
                'plugin.version': 'Plugin Version'
            }
        };
        
        Object.entries(pluginLanguagePacks).forEach(([lang, translations]) => {
            // 合并到现有语言包
            const existingTranslations = this.i18nManager.translations.get(lang) || {};
            const mergedTranslations = { ...existingTranslations, ...translations };
            this.i18nManager.translations.set(lang, mergedTranslations);
        });
    }
    
    /**
     * 添加插件特定的翻译器
     */
    addPluginTranslators() {
        // 添加日期格式化翻译器
        this.i18nManager.addCustomTranslator('dateFormat', (date, format, lang) => {
            const options = {
                'zh-CN': {
                    year: 'numeric',
                    month: 'long',
                    day: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit',
                    second: '2-digit'
                },
                'en-US': {
                    year: 'numeric',
                    month: 'long',
                    day: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit',
                    second: '2-digit'
                }
            };
            
            return new Intl.DateTimeFormat(lang, options[lang]).format(date);
        });
        
        // 添加数字格式化翻译器
        this.i18nManager.addCustomTranslator('numberFormat', (number, lang) => {
            return new Intl.NumberFormat(lang).format(number);
        });
    }
    
    /**
     * 绑定插件事件
     */
    bindPluginEvents() {
        // 监听语言变更事件
        this.i18nManager.addLanguageChangeListener((oldLang, newLang) => {
            this.handleLanguageChange(oldLang, newLang);
        });
        
        // 监听文本更新事件
        this.i18nManager.addListener((eventType, data) => {
            if (eventType === 'textUpdated') {
                this.handleTextUpdated(data);
            }
        });
    }
    
    /**
     * 处理语言变更
     * @param {string} oldLang - 旧语言
     * @param {string} newLang - 新语言
     */
    handleLanguageChange(oldLang, newLang) {
        console.log(`插件: 语言已从 ${oldLang} 变更为 ${newLang}`);
        
        // 执行插件特定的逻辑
        this.updatePluginUI(newLang);
    }
    
    /**
     * 处理文本更新
     * @param {Object} data - 更新数据
     */
    handleTextUpdated(data) {
        console.log(`插件: 文本已更新,语言: ${data.language},元素数: ${data.elementCount}`);
        
        // 执行插件特定的逻辑
        this.refreshPluginTexts(data);
    }
    
    /**
     * 更新插件UI
     * @param {string} lang - 语言代码
     */
    updatePluginUI(lang) {
        // 更新插件UI元素
        const pluginElements = document.querySelectorAll('[data-plugin-i18n]');
        pluginElements.forEach(element => {
            const key = element.dataset.pluginI18n;
            const translation = this.i18nManager.getText(key);
            element.textContent = translation;
        });
    }
    
    /**
     * 刷新插件文本
     * @param {Object} data - 更新数据
     */
    refreshPluginTexts(data) {
        // 刷新插件特定的文本元素
        const pluginTextElements = document.querySelectorAll('.plugin-text');
        pluginTextElements.forEach(element => {
            // 执行插件特定的文本刷新逻辑
            this.refreshPluginTextElement(element, data);
        });
    }
    
    /**
     * 刷新插件文本元素
     * @param {HTMLElement} element - 文本元素
     * @param {Object} data - 更新数据
     */
    refreshPluginTextElement(element, data) {
        // 执行元素特定的刷新逻辑
        const elementType = element.dataset.type || 'default';
        
        switch (elementType) {
            case 'date':
                // 刷新日期显示
                const currentDate = new Date();
                element.textContent = this.i18nManager.executeCustomTranslator(
                    'dateFormat', 
                    currentDate, 
                    'full', 
                    data.language
                );
                break;
                
            case 'number':
                // 刷新数字显示
                const currentNumber = parseFloat(element.textContent) || 0;
                element.textContent = this.i18nManager.executeCustomTranslator(
                    'numberFormat', 
                    currentNumber, 
                    data.language
                );
                break;
                
            default:
                // 默认刷新逻辑
                break;
        }
    }
}

// 应用插件
const plugin = new I18nPlugin(i18nManager);

Released under the MIT License.