diff --git a/dashboard/vite-plugin-translate.mjs b/dashboard/vite-plugin-translate.mjs index a0db1e0..52b04b4 100644 --- a/dashboard/vite-plugin-translate.mjs +++ b/dashboard/vite-plugin-translate.mjs @@ -26,17 +26,51 @@ const CONSTANTS = { * @param {string} closeChar - 闭括号字符 * @returns {number} 闭括号的位置,未找到返回 -1 */ +/** + * 查找平衡括号的结束位置,正确处理字符串字面量中的括号 + * @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; + let inString = false; + let stringChar = null; while (pos < code.length && depth > 0) { const char = code[pos]; - if (char === openChar) depth++; - else if (char === closeChar) { - depth--; - if (depth === 0) return pos; + + // 处理字符串字面量 + if (!inString && (char === '"' || char === "'")) { + inString = true; + stringChar = char; + } else if (inString && char === stringChar) { + // 检查是否是转义的引号:计算连续的反斜杠数量 + let backslashCount = 0; + let checkPos = pos - 1; + while (checkPos >= 0 && code[checkPos] === '\\') { + backslashCount++; + checkPos--; + } + // 如果反斜杠数量是偶数,则引号未转义,字符串结束 + if (backslashCount % 2 === 0) { + inString = false; + stringChar = null; + } } + + // 只在非字符串中计算括号深度 + if (!inString) { + if (char === openChar) depth++; + else if (char === closeChar) { + depth--; + if (depth === 0) return pos; + } + } + pos++; } @@ -189,12 +223,14 @@ function processVueSFC(code, translations) { if (depth === 0 && templateContent) { let replacedContent = templateContent; - replacedContent = replaceInterpolations(replacedContent, translations); + // 先处理属性绑定(包括带参数的),避免 replaceInterpolations 误处理 replacedContent = replaceAttributeBindings(replacedContent, translations); // 处理属性绑定中的数组字面量,如 :items="[{ label: $t('key'), ... }]" replacedContent = replaceAttributeArrayLiterals(replacedContent, translations); // 处理属性绑定中的对象字面量,如 :options="{ title: $t('key'), ... }" replacedContent = replaceAttributeObjectLiterals(replacedContent, translations); + // 最后处理插值表达式 + replacedContent = replaceInterpolations(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); @@ -239,9 +275,18 @@ function replaceInterpolations(code, translations) { // 处理插值表达式内部的 $t() 调用(不在单独的 {{ $t(...) }} 中) // 例如:{{ condition ? $t('key1') : $t('key2') }} + // 注意:跳过属性绑定中的内容(它们会在 replaceAttributeBindings 中处理) code = code.replace( /\{\{[\s\S]*?\}\}/g, - (interpolation) => { + (interpolation, offset, string) => { + // 检查这个插值是否在属性绑定中(前后有 = 和引号) + const beforeMatch = string.substring(Math.max(0, offset - 20), offset); + const afterMatch = string.substring(offset + interpolation.length, Math.min(string.length, offset + interpolation.length + 20)); + if (/[=:]["']\s*$/.test(beforeMatch) || /^["']\s*[>\/\s]/.test(afterMatch)) { + // 在属性绑定中,跳过处理 + return interpolation; + } + // 如果这个插值已经是一个完整的 {{ $t(...) }},跳过 if (/^\{\{\s*[\$]t\s*\(/.test(interpolation.trim())) { return interpolation; @@ -422,6 +467,7 @@ function replaceAttributeBindings(code, translations) { } // 再处理不带参数的调用::label="$t('key')" + // 统一处理逻辑:保留 $t() 调用结构,只替换翻译键(与带参数的处理保持一致) return code.replace( /([:@]|v-bind:|v-on:)([a-zA-Z0-9_-]+)=(["'])[\$]?t\((['"])((?:\\.|(?!\4).)*)\4\)\3/g, (match, prefix, attrName, outerQuote, innerQuote, key) => { @@ -429,14 +475,10 @@ function replaceAttributeBindings(code, translations) { const translation = translations[unescapedKey]; if (!translation || !translation.trim()) return match; - let escaped = escapeString(translation, outerQuote); - if (outerQuote === '"') { - escaped = escaped.replace(/'/g, "\\'"); - return `${prefix}${attrName}="'${escaped}'"`; - } else { - escaped = escaped.replace(/"/g, '\\"'); - return `${prefix}${attrName}='"${escaped}"'`; - } + // 转义翻译文本以适配内层引号(与带参数的处理保持一致) + const escaped = escapeString(translation, innerQuote); + // 保留 $t() 调用结构,只替换翻译键 + return `${prefix}${attrName}=${outerQuote}$t(${innerQuote}${escaped}${innerQuote})${outerQuote}`; } ); }