From 114f6211c7392ba852a641ad917c831773a0dd3c Mon Sep 17 00:00:00 2001 From: jingrow Date: Tue, 30 Dec 2025 03:55:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E4=B8=8B=E6=8B=89=E8=8F=9C=E5=8D=95=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/components/Sidebar.vue | 23 ++++---- dashboard/vite-plugin-translate.mjs | 82 +++++++++++++++++++++++++--- jcloud/translations/zh.csv | 1 + 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/dashboard/src/components/Sidebar.vue b/dashboard/src/components/Sidebar.vue index 4fe81f5..644c842 100644 --- a/dashboard/src/components/Sidebar.vue +++ b/dashboard/src/components/Sidebar.vue @@ -10,7 +10,7 @@
- 今果 Jingrow + Jingrow
- Search + {{ $t('Search') }} @@ -124,26 +124,29 @@ export default { data() { return { showCommandPalette: false, - showTeamSwitcher: false, - dropdownItems: [ + showTeamSwitcher: false + }; + }, + computed: { + dropdownItems() { + return [ { - label: 'Switch Team', + label: this.$t('Switch Team'), icon: 'command', onClick: () => (this.showTeamSwitcher = true) }, { - label: 'Support & Docs', + label: this.$t('Support & Docs'), icon: 'help-circle', onClick: () => (window.location.href = '/support') }, { - label: 'Logout', + label: this.$t('Logout'), icon: 'log-out', onClick: () => this.$auth.logout() } - ] - }; - }, + ]; + }, mounted() { window.addEventListener('keydown', e => { if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { diff --git a/dashboard/vite-plugin-translate.mjs b/dashboard/vite-plugin-translate.mjs index 52b04b4..8b68328 100644 --- a/dashboard/vite-plugin-translate.mjs +++ b/dashboard/vite-plugin-translate.mjs @@ -13,6 +13,7 @@ const CONSTANTS = { VUE_TEMPLATE_TAG_LEN: 9, VUE_TEMPLATE_CLOSE_TAG_LEN: 11, VUE_TEMPLATE_MODULE: '?vue&type=template', + VUE_SCRIPT_MODULE: '?vue&type=script', VUE_STYLE_MODULE: '?vue&type=style', FILE_EXTENSIONS: /\.(vue|js|ts|jsx|tsx)(\?.*)?$/, NON_ASCII_REGEX: /[^\x00-\x7F]/, @@ -127,16 +128,29 @@ function processNestedTranslations(paramsCode, translations, pattern) { * @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'; + // 在插件初始化时立即加载翻译数据,而不是等到 configResolved + let translations = {}; + 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); + } + } + } + return { name: 'vite-plugin-translate', enforce: 'pre', configResolved(config) { - if (locale && locale !== defaultLocale) { + // 确保翻译数据已加载(双重检查) + if (locale && locale !== defaultLocale && Object.keys(translations).length === 0) { const csvPath = path.resolve(__dirname, `${translationsPath}/${locale}.csv`); if (fs.existsSync(csvPath)) { try { @@ -158,12 +172,44 @@ function vitePluginTranslate(options = {}) { if (isVueFile) { const isTemplateModule = id.includes(CONSTANTS.VUE_TEMPLATE_MODULE); + const isScriptModule = id.includes(CONSTANTS.VUE_SCRIPT_MODULE); if (isTemplateModule) { // Vue 编译后的模板模块 code = replaceInterpolations(code, translations); code = replaceCompiledFormat(code, translations); code = replaceAttributeBindings(code, translations); + } else if (isScriptModule) { + // Vue 编译后的 script 模块 - 需要处理 this.$t()、$t() 以及编译后的函数调用(如 le()、t()) + // 先处理 this.$t() 和 $t() + code = code.replace( + /(?:this\.)?\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g, + (match, quote, key) => { + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; + if (translation) { + return quote + escapeString(translation, quote) + quote; + } + return match; + } + ); + // 再处理编译后的函数调用(如 le("key")、t("key")) + // 匹配函数名后跟括号和字符串参数 + code = code.replace( + /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\((['"])((?:\\.|(?!\2).)*)\2\)/g, + (match, funcName, quote, key) => { + // 只处理可能是翻译函数的调用(通常是单字母或短名称,如 t, le, i 等) + // 但排除明显的非翻译函数(如 console.log, JSON.parse 等) + if (funcName.length <= 3 && !['log', 'warn', 'error', 'parse', 'stringify', 'keys', 'values', 'entries'].includes(funcName)) { + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; + if (translation) { + return funcName + '(' + quote + escapeString(translation, quote) + quote + ')'; + } + } + return match; + } + ); } else { // 原始 Vue SFC 文件 code = processVueSFC(code, translations); @@ -242,17 +288,39 @@ function processVueSFC(code, translations) { code = code.replace( /(]*>)([\s\S]*?)(<\/script>)/gi, (match, openTag, scriptContent, closeTag) => { - const replacedContent = scriptContent.replace( + // 先处理 $t() 和 this.$t() 调用 + let replacedContent = scriptContent.replace( /(?:this\.)?\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g, (match, quote, key) => { const unescapedKey = unescapeString(key); const translation = translations[unescapedKey]; - // 如果匹配包含 this.,保留它 - const hasThis = match.startsWith('this.'); - const prefix = hasThis ? 'this.' : ''; - return translation ? `${prefix}$t(${quote}${escapeString(translation, quote)}${quote})` : match; + if (translation) { + return quote + escapeString(translation, quote) + quote; + } + return match; } ); + + // 再处理 t() 函数调用(从 i18n.js 导入的翻译函数) + // 检查是否有从 i18n 导入 t 函数 + const hasTImport = /import\s*\{[^}]*\bt\b[^}]*\}\s*from\s*['"]\.\.?\/.*i18n['"]/i.test(scriptContent) || + /import\s*\{[^}]*\bt\b[^}]*\}\s*from\s*['"]@\/.*i18n['"]/i.test(scriptContent); + + if (hasTImport) { + // 匹配 t('key') 或 t("key"),但排除 t.something() 或 t['something']() + replacedContent = replacedContent.replace( + /\bt\((['"])((?:\\.|(?!\1).)*)\1\)/g, + (match, quote, key) => { + const unescapedKey = unescapeString(key); + const translation = translations[unescapedKey]; + if (translation) { + return quote + escapeString(translation, quote) + quote; + } + return match; + } + ); + } + return openTag + replacedContent + closeTag; } ); diff --git a/jcloud/translations/zh.csv b/jcloud/translations/zh.csv index dac52b5..8689a3f 100644 --- a/jcloud/translations/zh.csv +++ b/jcloud/translations/zh.csv @@ -506,6 +506,7 @@ You are not logged in.,您尚未登录。, to access dashboard.,以访问仪表板。, Switch Team,切换团队, Logout,退出登录, +Support & Docs,支持与文档, Suspended,已暂停, Broken,损坏, China,中国大陆,