修复用户信息下拉菜单无法翻译的问题

This commit is contained in:
jingrow 2025-12-30 03:55:02 +08:00
parent c6067d0a9f
commit 114f6211c7
3 changed files with 89 additions and 17 deletions

View File

@ -10,7 +10,7 @@
<JLogo class="h-8 w-8 rounded" /> <JLogo class="h-8 w-8 rounded" />
<div class="ml-2 flex flex-col"> <div class="ml-2 flex flex-col">
<div class="text-base font-medium leading-none text-gray-900"> <div class="text-base font-medium leading-none text-gray-900">
今果 Jingrow Jingrow
</div> </div>
<div <div
v-if="$account.user" v-if="$account.user"
@ -37,7 +37,7 @@
<span class="mr-1.5"> <span class="mr-1.5">
<FeatherIcon name="search" class="h-5 w-5 text-gray-700" /> <FeatherIcon name="search" class="h-5 w-5 text-gray-700" />
</span> </span>
<span class="text-sm">Search</span> <span class="text-sm">{{ $t('Search') }}</span>
<span class="ml-auto text-sm text-gray-500"> <span class="ml-auto text-sm text-gray-500">
<template v-if="$platform === 'mac'">K</template> <template v-if="$platform === 'mac'">K</template>
<template v-else>Ctrl+K</template> <template v-else>Ctrl+K</template>
@ -124,26 +124,29 @@ export default {
data() { data() {
return { return {
showCommandPalette: false, showCommandPalette: false,
showTeamSwitcher: false, showTeamSwitcher: false
dropdownItems: [ };
},
computed: {
dropdownItems() {
return [
{ {
label: 'Switch Team', label: this.$t('Switch Team'),
icon: 'command', icon: 'command',
onClick: () => (this.showTeamSwitcher = true) onClick: () => (this.showTeamSwitcher = true)
}, },
{ {
label: 'Support & Docs', label: this.$t('Support & Docs'),
icon: 'help-circle', icon: 'help-circle',
onClick: () => (window.location.href = '/support') onClick: () => (window.location.href = '/support')
}, },
{ {
label: 'Logout', label: this.$t('Logout'),
icon: 'log-out', icon: 'log-out',
onClick: () => this.$auth.logout() onClick: () => this.$auth.logout()
} }
] ];
}; },
},
mounted() { mounted() {
window.addEventListener('keydown', e => { window.addEventListener('keydown', e => {
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {

View File

@ -13,6 +13,7 @@ const CONSTANTS = {
VUE_TEMPLATE_TAG_LEN: 9, VUE_TEMPLATE_TAG_LEN: 9,
VUE_TEMPLATE_CLOSE_TAG_LEN: 11, VUE_TEMPLATE_CLOSE_TAG_LEN: 11,
VUE_TEMPLATE_MODULE: '?vue&type=template', VUE_TEMPLATE_MODULE: '?vue&type=template',
VUE_SCRIPT_MODULE: '?vue&type=script',
VUE_STYLE_MODULE: '?vue&type=style', VUE_STYLE_MODULE: '?vue&type=style',
FILE_EXTENSIONS: /\.(vue|js|ts|jsx|tsx)(\?.*)?$/, FILE_EXTENSIONS: /\.(vue|js|ts|jsx|tsx)(\?.*)?$/,
NON_ASCII_REGEX: /[^\x00-\x7F]/, NON_ASCII_REGEX: /[^\x00-\x7F]/,
@ -127,16 +128,29 @@ function processNestedTranslations(paramsCode, translations, pattern) {
* @param {string} options.defaultLocale - 默认语言代码 * @param {string} options.defaultLocale - 默认语言代码
*/ */
function vitePluginTranslate(options = {}) { function vitePluginTranslate(options = {}) {
let translations = {};
const locale = options.locale || 'en'; const locale = options.locale || 'en';
const defaultLocale = options.defaultLocale || 'en'; const defaultLocale = options.defaultLocale || 'en';
const translationsPath = options.translationsPath || '../jcloud/translations'; 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 { return {
name: 'vite-plugin-translate', name: 'vite-plugin-translate',
enforce: 'pre', enforce: 'pre',
configResolved(config) { configResolved(config) {
if (locale && locale !== defaultLocale) { // 确保翻译数据已加载(双重检查)
if (locale && locale !== defaultLocale && Object.keys(translations).length === 0) {
const csvPath = path.resolve(__dirname, `${translationsPath}/${locale}.csv`); const csvPath = path.resolve(__dirname, `${translationsPath}/${locale}.csv`);
if (fs.existsSync(csvPath)) { if (fs.existsSync(csvPath)) {
try { try {
@ -158,12 +172,44 @@ function vitePluginTranslate(options = {}) {
if (isVueFile) { if (isVueFile) {
const isTemplateModule = id.includes(CONSTANTS.VUE_TEMPLATE_MODULE); const isTemplateModule = id.includes(CONSTANTS.VUE_TEMPLATE_MODULE);
const isScriptModule = id.includes(CONSTANTS.VUE_SCRIPT_MODULE);
if (isTemplateModule) { if (isTemplateModule) {
// Vue 编译后的模板模块 // Vue 编译后的模板模块
code = replaceInterpolations(code, translations); code = replaceInterpolations(code, translations);
code = replaceCompiledFormat(code, translations); code = replaceCompiledFormat(code, translations);
code = replaceAttributeBindings(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 { } else {
// 原始 Vue SFC 文件 // 原始 Vue SFC 文件
code = processVueSFC(code, translations); code = processVueSFC(code, translations);
@ -242,17 +288,39 @@ function processVueSFC(code, translations) {
code = code.replace( code = code.replace(
/(<script[^>]*>)([\s\S]*?)(<\/script>)/gi, /(<script[^>]*>)([\s\S]*?)(<\/script>)/gi,
(match, openTag, scriptContent, closeTag) => { (match, openTag, scriptContent, closeTag) => {
const replacedContent = scriptContent.replace( // 先处理 $t() 和 this.$t() 调用
let replacedContent = scriptContent.replace(
/(?:this\.)?\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g, /(?:this\.)?\$t\((['"])((?:\\.|(?!\1).)*)\1\)/g,
(match, quote, key) => { (match, quote, key) => {
const unescapedKey = unescapeString(key); const unescapedKey = unescapeString(key);
const translation = translations[unescapedKey]; const translation = translations[unescapedKey];
// 如果匹配包含 this.,保留它 if (translation) {
const hasThis = match.startsWith('this.'); return quote + escapeString(translation, quote) + quote;
const prefix = hasThis ? 'this.' : ''; }
return translation ? `${prefix}$t(${quote}${escapeString(translation, quote)}${quote})` : match; 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; return openTag + replacedContent + closeTag;
} }
); );

View File

@ -506,6 +506,7 @@ You are not logged in.,您尚未登录。,
to access dashboard.,以访问仪表板。, to access dashboard.,以访问仪表板。,
Switch Team,切换团队, Switch Team,切换团队,
Logout,退出登录, Logout,退出登录,
Support & Docs,支持与文档,
Suspended,已暂停, Suspended,已暂停,
Broken,损坏, Broken,损坏,
China,中国大陆, China,中国大陆,

Can't render this file because it has a wrong number of fields in line 401.