From 4ad23c57abb3246ceec56e153f316d79dce7477f Mon Sep 17 00:00:00 2001 From: jingrow Date: Mon, 29 Dec 2025 21:19:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96vite=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/pages/LoginSignup.vue | 2 +- dashboard/vite-plugin-translate.mjs | 74 +++++++++++++++++++++-------- dashboard/vite.config.ts | 2 +- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/dashboard/src/pages/LoginSignup.vue b/dashboard/src/pages/LoginSignup.vue index 28612c3..123b36c 100644 --- a/dashboard/src/pages/LoginSignup.vue +++ b/dashboard/src/pages/LoginSignup.vue @@ -304,7 +304,7 @@ > {{ $route.name == 'Login' - ? $t('Don\'t have an account? Create one.') + ? $t("Don't have an account? Create one.") : $t('Already have an account? Log in.') }} diff --git a/dashboard/vite-plugin-translate.mjs b/dashboard/vite-plugin-translate.mjs index 78f0341..bd89a18 100644 --- a/dashboard/vite-plugin-translate.mjs +++ b/dashboard/vite-plugin-translate.mjs @@ -68,11 +68,11 @@ function processNestedTranslations(paramsCode, translations, pattern) { // 格式1:带前缀的 (ctxPrefix, quote, key) ctxPrefix = captureGroups[0] || ''; quote = captureGroups[1]; - key = captureGroups[2]; + key = unescapeString(captureGroups[2]); } else if (captureGroups.length === 2) { // 格式2或3:不带前缀的 (quote, key) quote = captureGroups[0]; - key = captureGroups[1]; + key = unescapeString(captureGroups[1]); } else { // 无法解析,返回原匹配 return match; @@ -137,9 +137,10 @@ function vitePluginTranslate(options = {}) { } else { // 非 Vue 文件 code = code.replace( - /\bt\((['"])([^'"]+)\1\)/g, + /\bt\((['"])((?:\\.|(?!\1).)*)\1\)/g, (match, quote, key) => { - const translation = translations[key]; + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; return translation ? quote + escapeString(translation, quote) + quote : match; } ); @@ -202,9 +203,10 @@ function processVueSFC(code, translations) { /(]*>)([\s\S]*?)(<\/script>)/gi, (match, openTag, scriptContent, closeTag) => { const replacedContent = scriptContent.replace( - /\$t\((['"])([^'"]+)\1\)/g, + /\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g, (match, quote, key) => { - const translation = translations[key]; + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; return translation ? `$t(${quote}${escapeString(translation, quote)}${quote})` : match; } ); @@ -221,19 +223,43 @@ function processVueSFC(code, translations) { function replaceInterpolations(code, translations) { // 先处理不带参数的调用:{{ $t('xxx') }} code = code.replace( - /\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*\)\s*\}\}/g, - (match, quote, key) => translations[key] || match + /\{\{\s*[\$]t\s*\(\s*(['"])((?:\\.|(?!\1).)*)\1\s*\)\s*\}\}/g, + (match, quote, key) => { + const unescapedKey = unescapeString(key); + return translations[unescapedKey] || match; + } + ); + + // 处理插值表达式内部的 $t() 调用(不在单独的 {{ $t(...) }} 中) + // 例如:{{ condition ? $t('key1') : $t('key2') }} + code = code.replace( + /\{\{[\s\S]*?\}\}/g, + (interpolation) => { + // 如果这个插值已经是一个完整的 {{ $t(...) }},跳过 + if (/^\{\{\s*[\$]t\s*\(/.test(interpolation.trim())) { + return interpolation; + } + // 否则,处理其中的 $t() 调用 + return interpolation.replace( + /\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g, + (match, quote, key) => { + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; + return translation ? `$t(${quote}${escapeString(translation, quote)}${quote})` : match; + } + ); + } ); // 处理带参数的调用:{{ $t('key', { params }) }} const matches = []; - const pattern = /\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*,\s*/g; + const pattern = /\{\{\s*[\$]t\s*\(\s*(['"])((?:\\.|(?!\1).)*)\1\s*,\s*/g; let match; while ((match = pattern.exec(code)) !== null) { const startIndex = match.index; const quote = match[1]; - const key = match[2]; + const key = unescapeString(match[2]); const afterKeyIndex = match.index + match[0].length; // 找到参数部分的结束位置 @@ -261,13 +287,13 @@ function replaceInterpolations(code, translations) { paramsPart = processNestedTranslations( paramsPart, translations, - /\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*\)\s*\}\}/g + /\{\{\s*[\$]t\s*\(\s*(['"])((?:\\.|(?!\1).)*)\1\s*\)\s*\}\}/g ); // 再处理 $t(...) 格式(参数对象中的直接调用) paramsPart = processNestedTranslations( paramsPart, translations, - /\$t\((['"])([^'"]+)\1\)/g + /\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g ); const translation = translations[m.key]; @@ -286,14 +312,14 @@ function replaceInterpolations(code, translations) { function replaceCompiledFormat(code, translations) { // 处理带参数的调用 const matches = []; - const pattern = /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\s*,\s*/g; + const pattern = /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])((?:\\.|(?!\2).)*)\2\s*,\s*/g; let match; while ((match = pattern.exec(code)) !== null) { const startIndex = match.index; const ctxPrefix = match[1] || ''; const quote = match[2]; - const key = match[3]; + const key = unescapeString(match[3]); const afterKeyIndex = match.index + match[0].length; const paramsEnd = findBalancedBracket(code, afterKeyIndex); @@ -311,7 +337,7 @@ function replaceCompiledFormat(code, translations) { paramsPart = processNestedTranslations( paramsPart, translations, - /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\)/g + /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])((?:\\.|(?!\2).)*)\2\)/g ); const translation = translations[m.key]; @@ -323,9 +349,10 @@ function replaceCompiledFormat(code, translations) { // 处理不带参数的调用 code = code.replace( - /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\)/g, + /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])((?:\\.|(?!\2).)*)\2\)/g, (match, ctxPrefix, quote, key) => { - const translation = translations[key]; + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; return translation ? `${ctxPrefix || ''}$t(${quote}${escapeString(translation, quote)}${quote})` : match; } ); @@ -338,9 +365,10 @@ function replaceCompiledFormat(code, translations) { */ function replaceAttributeBindings(code, translations) { return code.replace( - /([:@]|v-bind:|v-on:)([a-zA-Z0-9_-]+)=(["'])[\$]?t\((['"])([^'"]+)\4\)\3/g, + /([:@]|v-bind:|v-on:)([a-zA-Z0-9_-]+)=(["'])[\$]?t\((['"])((?:\\.|(?!\4).)*)\4\)\3/g, (match, prefix, attrName, outerQuote, innerQuote, key) => { - const translation = translations[key]; + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; if (!translation || !translation.trim()) return match; let escaped = escapeString(translation, outerQuote); @@ -355,6 +383,14 @@ function replaceAttributeBindings(code, translations) { ); } +/** + * 去除字符串中的转义字符(将 \' 转换为 ',\" 转换为 " 等) + */ +function unescapeString(text) { + return text + .replace(/\\(.)/g, '$1'); +} + /** * 转义字符串 */ diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index ebc2dad..9b8e6f9 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -11,7 +11,7 @@ import { sentryVitePlugin } from '@sentry/vite-plugin'; import vitePluginTranslate from './vite-plugin-translate.mjs'; // 语言配置:设置目标语言,默认为 'en'(英文),可设置为 'zh'(中文)等 -const locale = process.env.DASHBOARD_LOCALE || 'en'; +const locale = process.env.DASHBOARD_LOCALE || 'zh'; export default defineConfig({ plugins: [