import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // 常量配置 const CONSTANTS = { VUE_TEMPLATE_TAG: '', VUE_TEMPLATE_TAG_LEN: 9, VUE_TEMPLATE_CLOSE_TAG_LEN: 11, VUE_TEMPLATE_MODULE: '?vue&type=template', VUE_STYLE_MODULE: '?vue&type=style', FILE_EXTENSIONS: /\.(vue|js|ts|jsx|tsx)(\?.*)?$/, NON_ASCII_REGEX: /[^\x00-\x7F]/, }; /** * 查找平衡括号的结束位置 * @param {string} code - 代码字符串 * @param {number} startPos - 起始位置(开括号之后) * @param {string} openChar - 开括号字符 * @param {string} closeChar - 闭括号字符 * @returns {number} 闭括号的位置,未找到返回 -1 */ function findBalancedBracket(code, startPos, openChar = '(', closeChar = ')') { let depth = 1; let pos = startPos; while (pos < code.length && depth > 0) { const char = code[pos]; if (char === openChar) depth++; else if (char === closeChar) { depth--; if (depth === 0) return pos; } pos++; } return -1; } /** * 处理参数对象中的嵌套 $t() 调用 * @param {string} paramsCode - 参数部分的代码 * @param {object} translations - 翻译字典 * @param {RegExp} pattern - 匹配 $t() 调用的正则表达式 * @returns {string} 处理后的参数代码 */ function processNestedTranslations(paramsCode, translations, pattern) { return paramsCode.replace(pattern, (match, ...args) => { // String.replace 回调的参数:match, ...captureGroups, index, input // 需要根据捕获组数量判断格式 // 格式1: /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\)/g -> 3个捕获组 // 格式2: /\$t\((['"])([^'"]+)\1\)/g -> 2个捕获组 // 格式3: /\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*\)\s*\}\}/g -> 2个捕获组 // 获取捕获组数量(排除最后两个参数:index 和 input) const captureGroups = args.slice(0, -2); let ctxPrefix = ''; let quote; let key; if (captureGroups.length === 3) { // 格式1:带前缀的 (ctxPrefix, quote, key) ctxPrefix = captureGroups[0] || ''; quote = captureGroups[1]; key = captureGroups[2]; } else if (captureGroups.length === 2) { // 格式2或3:不带前缀的 (quote, key) quote = captureGroups[0]; key = captureGroups[1]; } else { // 无法解析,返回原匹配 return match; } const translation = translations[key]; return translation ? `${ctxPrefix}$t(${quote}${escapeString(translation, quote)}${quote})` : match; }); } /** * Vite 插件:构建时直接读取 CSV,替换代码中的 $t('xxx') 为翻译文本 * @param {object} options - 插件选项 * @param {string} options.locale - 目标语言代码 * @param {string} options.translationsPath - 翻译文件路径(相对于插件目录) * @param {string} options.defaultLocale - 默认语言代码 */ function vitePluginTranslate(options = {}) { let translations = {}; const locale = options.locale || 'en'; const defaultLocale = options.defaultLocale || 'en'; const translationsPath = options.translationsPath || '../jcloud/translations'; return { name: 'vite-plugin-translate', enforce: 'pre', configResolved(config) { if (locale && locale !== defaultLocale) { const csvPath = path.resolve(__dirname, `${translationsPath}/${locale}.csv`); if (fs.existsSync(csvPath)) { try { translations = parseCSV(csvPath); } catch (error) { console.warn(`[vite-plugin-translate] Failed to load translations from ${csvPath}:`, error.message); } } } }, transform(code, id) { const fileMatches = CONSTANTS.FILE_EXTENSIONS.test(id); if (!fileMatches || locale === defaultLocale || Object.keys(translations).length === 0) { return null; } const originalCode = code; const isVueFile = id.includes('.vue') && !id.includes(CONSTANTS.VUE_STYLE_MODULE); if (isVueFile) { const isTemplateModule = id.includes(CONSTANTS.VUE_TEMPLATE_MODULE); if (isTemplateModule) { // Vue 编译后的模板模块 code = replaceInterpolations(code, translations); code = replaceCompiledFormat(code, translations); code = replaceAttributeBindings(code, translations); } else { // 原始 Vue SFC 文件 code = processVueSFC(code, translations); } } else { // 非 Vue 文件 code = code.replace( /\bt\((['"])([^'"]+)\1\)/g, (match, quote, key) => { const translation = translations[key]; return translation ? quote + escapeString(translation, quote) + quote : match; } ); } return code !== originalCode ? { code, map: null } : null; } }; } /** * 处理 Vue SFC 文件 */ function processVueSFC(code, translations) { // 处理 ,处理嵌套 let depth = 1; let pos = templateRegex.lastIndex; let templateContent = ''; while (depth > 0 && pos < code.length) { const nextOpen = code.indexOf(CONSTANTS.VUE_TEMPLATE_TAG, pos); const nextClose = code.indexOf(CONSTANTS.VUE_TEMPLATE_CLOSE_TAG, pos); if (nextClose === -1) break; if (nextOpen !== -1 && nextOpen < nextClose) { depth++; pos = nextOpen + CONSTANTS.VUE_TEMPLATE_TAG_LEN; } else { depth--; if (depth === 0) { templateContent = code.substring(templateRegex.lastIndex, nextClose); break; } pos = nextClose + CONSTANTS.VUE_TEMPLATE_CLOSE_TAG_LEN; } } if (depth === 0 && templateContent) { let replacedContent = templateContent; replacedContent = replaceInterpolations(replacedContent, translations); replacedContent = replaceAttributeBindings(replacedContent, translations); code = code.substring(0, startIndex) + openTag + replacedContent + CONSTANTS.VUE_TEMPLATE_CLOSE_TAG + code.substring(startIndex + openTag.length + templateContent.length + CONSTANTS.VUE_TEMPLATE_CLOSE_TAG_LEN); break; } } // 处理