feat: user frontend i18n (zh-CN/en)

- vue-i18n@9 with locale files for zh-CN and en
- Language switcher in topbar
- Navigation, breadcrumb, credits page translated
- Discovery page i18n keys prepared
- Language persisted in localStorage
- Build verified
This commit is contained in:
TradeMate Dev
2026-06-12 11:21:19 +08:00
parent 79474d8480
commit d8780a716b
7 changed files with 274 additions and 16 deletions
+65
View File
@@ -14,6 +14,7 @@
"element-plus": "^2.9.1",
"pinia": "^2.3.0",
"vue": "^3.5.13",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0"
},
"devDependencies": {
@@ -553,6 +554,50 @@
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@intlify/core-base": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.4.tgz",
"integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "9.14.4",
"@intlify/shared": "9.14.4"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "9.14.4",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.4.tgz",
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -1990,6 +2035,26 @@
}
}
},
"node_modules/vue-i18n": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.4.tgz",
"integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "9.14.4",
"@intlify/shared": "9.14.4",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+1
View File
@@ -15,6 +15,7 @@
"element-plus": "^2.9.1",
"pinia": "^2.3.0",
"vue": "^3.5.13",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0"
},
"devDependencies": {
+22
View File
@@ -0,0 +1,22 @@
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN.json'
import en from './locales/en.json'
const savedLang = localStorage.getItem('lang') || 'zh-CN'
const i18n = createI18n({
legacy: false,
locale: savedLang,
fallbackLocale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en': en,
},
})
export function switchLang(lang) {
i18n.global.locale.value = lang
localStorage.setItem('lang', lang)
}
export default i18n
+26 -16
View File
@@ -16,16 +16,16 @@
:collapse-transition="false"
@select="showMobileMenu = false"
>
<el-menu-item index="/discovery"><el-icon><Search /></el-icon><span>发现客户</span></el-menu-item>
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>工作台</span></el-menu-item>
<el-menu-item index="/customers"><el-icon><User /></el-icon><span>客户管理</span></el-menu-item>
<el-menu-item index="/products"><el-icon><Goods /></el-icon><span>产品库</span></el-menu-item>
<el-menu-item index="/quotations"><el-icon><DocumentCopy /></el-icon><span>报价单</span></el-menu-item>
<el-menu-item index="/translate"><el-icon><ChatLineSquare /></el-icon><span>智能翻译</span></el-menu-item>
<el-menu-item index="/marketing"><el-icon><Promotion /></el-icon><span>营销素材</span></el-menu-item>
<el-menu-item index="/followup"><el-icon><Message /></el-icon><span>智能跟进</span></el-menu-item>
<el-menu-item index="/analytics"><el-icon><DataAnalysis /></el-icon><span>数据分析</span></el-menu-item>
<el-menu-item index="/team"><el-icon><UserFilled /></el-icon><span>团队协作</span></el-menu-item>
<el-menu-item index="/discovery"><el-icon><Search /></el-icon><span>{{ $t('nav.discovery') }}</span></el-menu-item>
<el-menu-item index="/workspace"><el-icon><Odometer /></el-icon><span>{{ $t('nav.workspace') }}</span></el-menu-item>
<el-menu-item index="/customers"><el-icon><User /></el-icon><span>{{ $t('nav.customers') }}</span></el-menu-item>
<el-menu-item index="/products"><el-icon><Goods /></el-icon><span>{{ $t('nav.products') }}</span></el-menu-item>
<el-menu-item index="/quotations"><el-icon><DocumentCopy /></el-icon><span>{{ $t('nav.quotations') }}</span></el-menu-item>
<el-menu-item index="/translate"><el-icon><ChatLineSquare /></el-icon><span>{{ $t('nav.translate') }}</span></el-menu-item>
<el-menu-item index="/marketing"><el-icon><Promotion /></el-icon><span>{{ $t('nav.marketing') }}</span></el-menu-item>
<el-menu-item index="/followup"><el-icon><Message /></el-icon><span>{{ $t('nav.followup') }}</span></el-menu-item>
<el-menu-item index="/analytics"><el-icon><DataAnalysis /></el-icon><span>{{ $t('nav.analytics') }}</span></el-menu-item>
<el-menu-item index="/team"><el-icon><UserFilled /></el-icon><span>{{ $t('nav.team') }}</span></el-menu-item>
</el-menu>
</aside>
@@ -35,13 +35,14 @@
<el-icon :size="20"><Expand /></el-icon>
</el-button>
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item :to="'/workspace'">工作台</el-breadcrumb-item>
<el-breadcrumb-item v-if="route.meta?.title" :to="route.path">{{ route.meta.title }}</el-breadcrumb-item>
<el-breadcrumb-item :to="'/workspace'">{{ $t('nav.workspace') }}</el-breadcrumb-item>
<el-breadcrumb-item v-if="route.meta?.title" :to="route.path">{{ $t('nav.' + route.name?.toLowerCase()) || route.meta.title }}</el-breadcrumb-item>
</el-breadcrumb>
<div class="topbar-right">
<el-button text style="font-size:13px;color:#999" @click="toggleLang">{{ currentLang }}</el-button>
<el-button v-if="creditBalance !== null" text class="credit-btn" @click="$router.push('/credits')">
<el-icon><Coin /></el-icon>
<span class="credit-text">{{ creditBalance }} </span>
<span class="credit-text">{{ creditBalance }} {{ $t('topbar.credits') }}</span>
</el-button>
<el-badge :value="unread" :hidden="!unread" class="notif-badge">
<el-button text style="font-size:18px" @click="$router.push('/notifications')">
@@ -56,9 +57,9 @@
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="$router.push('/profile')">个人中心</el-dropdown-item>
<el-dropdown-item @click="$router.push('/notifications')">通知中心</el-dropdown-item>
<el-dropdown-item divided @click="handleLogout">退出登录</el-dropdown-item>
<el-dropdown-item @click="$router.push('/profile')">{{ $t('nav.profile') }}</el-dropdown-item>
<el-dropdown-item @click="$router.push('/notifications')">{{ $t('nav.notifications') }}</el-dropdown-item>
<el-dropdown-item divided @click="handleLogout">{{ $t('common.confirm') }}退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -89,18 +90,27 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getUnreadCount, getCreditBalance } from '@/api'
import AiAssistant from '@/components/AiAssistant.vue'
import { switchLang } from '@/i18n'
const route = useRoute()
const router = useRouter()
const { locale } = useI18n()
const auth = useAuthStore()
const collapsed = ref(false)
const showMobileMenu = ref(false)
const unread = ref(0)
const creditBalance = ref(null)
const currentLang = computed(() => locale.value === 'en' ? 'English' : '中文')
function toggleLang() {
const next = locale.value === 'en' ? 'zh-CN' : 'en'
switchLang(next)
}
async function loadCreditBalance() {
try {
const res = await getCreditBalance()
+79
View File
@@ -0,0 +1,79 @@
{
"nav": {
"discovery": "Find Buyers",
"workspace": "Dashboard",
"customers": "Customers",
"products": "Products",
"quotations": "Quotations",
"translate": "Translate",
"marketing": "Marketing",
"followup": "Follow-ups",
"analytics": "Analytics",
"team": "Team",
"notifications": "Notifications",
"profile": "Profile",
"credits": "Buy Credits",
"settings": "Settings"
},
"topbar": {
"credits": "credits",
"purchase": "Buy Credits",
"notifications": "Notifications"
},
"discovery": {
"title": "Discover New Customers",
"subtitle": "Enter a product description to find potential buyers worldwide",
"product_placeholder": "e.g. LED lighting, solar panels, auto parts",
"market_placeholder": "Target market (e.g. US, Germany, leave empty for global)",
"search": "Search Buyers",
"searching": "Searching...",
"results": "Search Results",
"no_results": "No results found. Try different keywords",
"credits_cost": "Each search costs 10 credits",
"analyze": "Deep Analysis",
"outreach": "Generate Outreach",
"add_customer": "Add as Customer",
"visit_website": "Visit Website",
"match_score": "Match Score",
"contact": "Contact Info"
},
"credits": {
"title": "Credits",
"balance": "Current Balance",
"packages": "Buy Credits",
"subscription": "Subscription",
"history": "Transaction History",
"purchase": "Buy Now",
"subscribe": "Subscribe",
"per_month": "/month",
"per_credit": "per credit",
"pay_alipay": "Alipay",
"pay_wechat": "WeChat Pay",
"confirm_pay": "Confirm Payment",
"scan_pay": "Scan to Pay",
"insufficient": "Insufficient credits",
"buy_more": "Buy More",
"total_purchased": "Total Purchased",
"total_used": "Total Used"
},
"common": {
"loading": "Loading...",
"error": "Something went wrong",
"retry": "Retry",
"cancel": "Cancel",
"confirm": "Confirm",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"back": "Back",
"no_data": "No data",
"search": "Search",
"all": "All",
"status": "Status",
"time": "Time"
},
"lang": {
"switch_to": "中文",
"current": "English"
}
}
+79
View File
@@ -0,0 +1,79 @@
{
"nav": {
"discovery": "发现客户",
"workspace": "工作台",
"customers": "客户管理",
"products": "产品库",
"quotations": "报价单",
"translate": "智能翻译",
"marketing": "营销素材",
"followup": "跟进提醒",
"analytics": "数据分析",
"team": "团队协作",
"notifications": "通知中心",
"profile": "个人中心",
"credits": "购买次数",
"settings": "设置"
},
"topbar": {
"credits": "次",
"purchase": "购买次数",
"notifications": "通知"
},
"discovery": {
"title": "发现新客户",
"subtitle": "输入产品关键词,找到全球潜在买家",
"product_placeholder": "例如:LED lighting, solar panels, auto parts",
"market_placeholder": "目标市场(如 US, Germany, 留空为全球)",
"search": "搜索买家",
"searching": "正在搜索...",
"results": "搜索结果",
"no_results": "暂无结果,请尝试其他关键词",
"credits_cost": "每次搜索消耗 10 次",
"analyze": "深度分析",
"outreach": "生成开发信",
"add_customer": "添加为客户",
"visit_website": "访问网站",
"match_score": "匹配度",
"contact": "联系方式"
},
"credits": {
"title": "信用次数",
"balance": "当前余额",
"packages": "购买次数",
"subscription": "订阅套餐",
"history": "消费记录",
"purchase": "立即购买",
"subscribe": "开通订阅",
"per_month": "次/月",
"per_credit": "次均",
"pay_alipay": "支付宝",
"pay_wechat": "微信支付",
"confirm_pay": "确认支付",
"scan_pay": "扫码支付",
"insufficient": "次数不足",
"buy_more": "去购买",
"total_purchased": "已购买",
"total_used": "已使用"
},
"common": {
"loading": "加载中...",
"error": "出错了",
"retry": "重试",
"cancel": "取消",
"confirm": "确认",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"back": "返回",
"no_data": "暂无数据",
"search": "搜索",
"all": "全部",
"status": "状态",
"time": "时间"
},
"lang": {
"switch_to": "English",
"current": "中文"
}
}
+2
View File
@@ -6,10 +6,12 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import i18n from './i18n'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(i18n)
app.use(ElementPlus, { locale: zhCn })
for (const [k, v] of Object.entries(ElementPlusIconsVue)) app.component(k, v)
app.mount('#app')