feat: 搭建多语言框架

This commit is contained in:
digua
2026-01-04 00:06:20 +08:00
committed by digua
parent cc45c2e510
commit a3dba9ecd1
19 changed files with 5332 additions and 5060 deletions

View File

@@ -2,6 +2,7 @@ import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
export default defineConfig(() => {
return {
@@ -47,6 +48,14 @@ export default defineConfig(() => {
},
},
}),
VueI18nPlugin({
// 指定全局语言包目录
include: [resolve(__dirname, 'src/i18n/locales/**')],
// 默认语言
defaultSFCLang: 'json',
// 启用组件内 <i18n> 块
compositionOnly: true,
}),
],
root: 'src/',
build: {

View File

@@ -10,8 +10,8 @@
"main": "./out/main/index.js",
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"better-sqlite3"
"better-sqlite3",
"electron"
]
},
"scripts": {
@@ -34,13 +34,15 @@
"better-sqlite3": "^12.4.6",
"electron-updater": "^6.6.2",
"markdown-it": "^14.1.0",
"stream-json": "^1.9.1"
"stream-json": "^1.9.1",
"vue-i18n": "^11.2.8"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron/rebuild": "^4.0.1",
"@intlify/unplugin-vue-i18n": "^11.0.3",
"@nuxt/ui": "^4.2.1",
"@rushstack/eslint-patch": "^1.15.0",
"@tailwindcss/vite": "^4.0.0",

9916
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,12 @@ import { ChatRecordDrawer } from '@/components/common/ChatRecord'
import { useSessionStore } from '@/stores/session'
import { useLayoutStore } from '@/stores/layout'
import { usePromptStore } from '@/stores/prompt'
import { useSettingsStore } from '@/stores/settings'
const sessionStore = useSessionStore()
const layoutStore = useLayoutStore()
const promptStore = usePromptStore()
const settingsStore = useSettingsStore()
const { isInitialized } = storeToRefs(sessionStore)
const route = useRoute()
@@ -20,8 +22,11 @@ const tooltip = {
delayDuration: 100,
}
// 应用启动时从数据库加载会话列表
// 应用启动时初始化
onMounted(async () => {
// 初始化语言设置(同步 i18n 和 dayjs
settingsStore.initLocale()
// 从数据库加载会话列表
await sessionStore.loadSessions()
})
</script>

30
src/components.d.ts vendored
View File

@@ -13,25 +13,25 @@ declare module 'vue' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
UAvatar: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Avatar.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UChatPrompt: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/ChatPrompt.vue')['default']
UChatPromptSubmit: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/ChatPromptSubmit.vue')['default']
UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UInputTags: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/InputTags.vue')['default']
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
USelect: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handle@3.14.0_@tiptap+extensions_5zuht7xq3rocclrlc6s6a6pqpq/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
}
}

View File

@@ -2,16 +2,21 @@
import { storeToRefs } from 'pinia'
import { ref, onMounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import type { AnalysisSession } from '@/types/base'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
import 'dayjs/locale/en'
import SidebarFooter from './sidebar/SidebarFooter.vue'
import { useSessionStore } from '@/stores/session'
import { useLayoutStore } from '@/stores/layout'
import { useSettingsStore } from '@/stores/settings'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
const { t } = useI18n()
const settingsStore = useSettingsStore()
const sessionStore = useSessionStore()
const layoutStore = useLayoutStore()
@@ -103,19 +108,19 @@ function getContextMenuItems(session: AnalysisSession) {
return [
[
{
label: '重命名',
label: t('sidebar.contextMenu.rename'),
icon: 'i-lucide-pencil',
class: 'p-2',
onSelect: () => openRenameModal(session),
},
{
label: isPinned ? '取消置顶' : '置顶',
label: isPinned ? t('sidebar.contextMenu.unpin') : t('sidebar.contextMenu.pin'),
icon: isPinned ? 'i-lucide-pin-off' : 'i-lucide-pin',
class: 'p-2',
onSelect: () => sessionStore.togglePinSession(session.id),
},
{
label: '删除',
label: t('sidebar.contextMenu.delete'),
icon: 'i-lucide-trash',
color: 'error' as const,
class: 'p-2',
@@ -158,8 +163,8 @@ function getSessionAvatarText(session: AnalysisSession): string {
<div class="flex flex-col p-4">
<!-- Header / Toggle -->
<div class="mb-2 flex items-center" :class="[isCollapsed ? 'justify-center' : 'justify-between']">
<div v-if="!isCollapsed" class="text-2xl font-black tracking-tight text-pink-500 ml-2">ChatLab</div>
<UTooltip :text="isCollapsed ? '展开侧边栏' : '收起侧边栏'" :popper="{ placement: 'right' }">
<div v-if="!isCollapsed" class="text-2xl font-black tracking-tight text-pink-500 ml-2">{{ t('sidebar.brand') }}</div>
<UTooltip :text="isCollapsed ? t('sidebar.tooltip.expand') : t('sidebar.tooltip.collapse')" :popper="{ placement: 'right' }">
<UButton
icon="i-heroicons-bars-3"
color="gray"
@@ -172,7 +177,7 @@ function getSessionAvatarText(session: AnalysisSession): string {
</div>
<!-- New Analysis Button -->
<UTooltip :text="isCollapsed ? '分析新聊天' : ''" :popper="{ placement: 'right' }">
<UTooltip :text="isCollapsed ? t('sidebar.newAnalysis') : ''" :popper="{ placement: 'right' }">
<UButton
:block="!isCollapsed"
class="transition-all rounded-full hover:bg-gray-200/60 dark:hover:bg-gray-800 h-12 cursor-pointer"
@@ -182,12 +187,12 @@ function getSessionAvatarText(session: AnalysisSession): string {
@click="handleImport"
>
<UIcon name="i-heroicons-plus" class="h-5 w-5 shrink-0" :class="[isCollapsed ? '' : 'mr-2']" />
<span v-if="!isCollapsed" class="truncate">分析新聊天</span>
<span v-if="!isCollapsed" class="truncate">{{ t('sidebar.newAnalysis') }}</span>
</UButton>
</UTooltip>
<!-- Tools Button -->
<UTooltip :text="isCollapsed ? '实用工具' : ''" :popper="{ placement: 'right' }">
<UTooltip :text="isCollapsed ? t('sidebar.tools') : ''" :popper="{ placement: 'right' }">
<UButton
:block="!isCollapsed"
class="transition-all rounded-full hover:bg-gray-200/60 dark:hover:bg-gray-800 h-12 cursor-pointer mt-2"
@@ -202,7 +207,7 @@ function getSessionAvatarText(session: AnalysisSession): string {
@click="router.push({ name: 'tools' })"
>
<UIcon name="i-heroicons-wrench-screwdriver" class="h-4 w-4 shrink-0" :class="[isCollapsed ? '' : 'mr-2']" />
<span v-if="!isCollapsed" class="truncate">实用工具</span>
<span v-if="!isCollapsed" class="truncate">{{ t('sidebar.tools') }}</span>
</UButton>
</UTooltip>
</div>
@@ -212,18 +217,18 @@ function getSessionAvatarText(session: AnalysisSession): string {
<!-- 聊天记录标题 - 固定在顶部不随列表滚动 -->
<UTooltip
v-if="!isCollapsed && sessions.length > 0"
text="右键可删除或重命名聊天记录"
:text="t('sidebar.tooltip.hint')"
:popper="{ placement: 'right' }"
>
<div class="px-3 mb-2 flex items-center gap-1">
<div class="text-sm font-medium text-gray-500">聊天记录</div>
<div class="text-sm font-medium text-gray-500">{{ t('sidebar.chatHistory') }}</div>
<UIcon name="i-heroicons-question-mark-circle" class="size-3.5 text-gray-400" />
</div>
</UTooltip>
<!-- 聊天记录列表 - 可滚动区域 -->
<div class="flex-1 overflow-y-auto">
<div v-if="sessions.length === 0 && !isCollapsed" class="py-8 text-center text-sm text-gray-500">暂无记录</div>
<div v-if="sessions.length === 0 && !isCollapsed" class="py-8 text-center text-sm text-gray-500">{{ t('sidebar.noRecords') }}</div>
<div class="space-y-1 pb-8">
<UTooltip
@@ -298,17 +303,17 @@ function getSessionAvatarText(session: AnalysisSession): string {
<UModal v-model:open="showRenameModal">
<template #content>
<div class="p-4">
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">重命名</h3>
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">{{ t('sidebar.renameModal.title') }}</h3>
<UInput
ref="renameInputRef"
v-model="newName"
placeholder="请输入新名称"
:placeholder="t('sidebar.renameModal.placeholder')"
class="mb-4 w-100"
@keydown.enter="handleRename"
/>
<div class="flex justify-end gap-2">
<UButton variant="soft" @click="closeRenameModal">取消</UButton>
<UButton color="primary" :disabled="!newName.trim()" @click="handleRename">确定</UButton>
<UButton variant="soft" @click="closeRenameModal">{{ t('common.cancel') }}</UButton>
<UButton color="primary" :disabled="!newName.trim()" @click="handleRename">{{ t('common.confirm') }}</UButton>
</div>
</div>
</template>
@@ -318,15 +323,13 @@ function getSessionAvatarText(session: AnalysisSession): string {
<UModal v-model:open="showDeleteModal">
<template #content>
<div class="p-4">
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">确认删除</h3>
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">{{ t('sidebar.deleteModal.title') }}</h3>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
确定要删除聊天记录
<span class="font-medium text-gray-900 dark:text-white">"{{ deleteTarget?.name }}"</span>
此操作无法撤销
{{ t('sidebar.deleteModal.message', { name: deleteTarget?.name }) }}
</p>
<div class="flex justify-end gap-2">
<UButton variant="soft" @click="closeDeleteModal">取消</UButton>
<UButton color="error" @click="confirmDelete">删除</UButton>
<UButton variant="soft" @click="closeDeleteModal">{{ t('common.cancel') }}</UButton>
<UButton color="error" @click="confirmDelete">{{ t('common.delete') }}</UButton>
</div>
</div>
</template>

View File

@@ -1,12 +1,20 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLayoutStore } from '@/stores/layout'
import { useSettingsStore } from '@/stores/settings'
import { useColorMode } from '@vueuse/core'
import { availableLocales, type LocaleType } from '@/i18n'
import NetworkSettingsSection from './NetworkSettingsSection.vue'
const { t } = useI18n()
// Store
const layoutStore = useLayoutStore()
const settingsStore = useSettingsStore()
const { screenshotMobileAdapt } = storeToRefs(layoutStore)
const { locale } = storeToRefs(settingsStore)
// Color Mode
const colorMode = useColorMode({
@@ -19,10 +27,43 @@ const colorModeOptions = [
{ label: '浅色模式', value: 'light' },
{ label: '深色模式', value: 'dark' },
]
// Language options
const languageOptions = computed(() =>
availableLocales.map((l) => ({
label: l.nativeName,
value: l.code,
}))
)
// Handle language change
function handleLocaleChange(newLocale: LocaleType) {
settingsStore.setLocale(newLocale)
}
</script>
<template>
<div class="space-y-6">
<!-- 语言设置 -->
<div>
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
<UIcon name="i-heroicons-language" class="h-4 w-4 text-green-500" />
{{ t('settings.basic.language.title') }}
</h3>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
<div class="flex items-center justify-between">
<div class="flex-1 pr-4">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('settings.basic.language.description') }}
</p>
</div>
<div class="w-48">
<UTabs :model-value="locale" :items="languageOptions" @update:model-value="handleLocaleChange"></UTabs>
</div>
</div>
</div>
</div>
<!-- 外观设置 -->
<div>
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">

44
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,44 @@
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN'
import enUS from './locales/en-US'
import { defaultLocale, type LocaleType } from './types'
// 导出类型
export type { LocaleType } from './types'
export {
availableLocales,
defaultLocale,
detectSystemLocale,
isFeatureSupported,
featureLocaleRestrictions,
} from './types'
/**
* 创建 i18n 实例
*/
export const i18n = createI18n({
legacy: false, // 使用 Composition API 模式
locale: defaultLocale, // 默认语言
fallbackLocale: 'en-US', // 回退语言
messages: {
'zh-CN': zhCN,
'en-US': enUS,
},
})
/**
* 动态切换语言
*/
export function setLocale(locale: LocaleType) {
i18n.global.locale.value = locale
}
/**
* 获取当前语言
*/
export function getLocale(): LocaleType {
return i18n.global.locale.value as LocaleType
}
export default i18n

View File

@@ -0,0 +1,21 @@
{
"confirm": "OK",
"cancel": "Cancel",
"delete": "Delete",
"save": "Save",
"edit": "Edit",
"rename": "Rename",
"close": "Close",
"loading": "Loading...",
"noData": "No data",
"error": {
"general": "Operation failed, please try again",
"network": "Network error"
},
"unit": {
"messages": "messages",
"members": "members",
"days": "days"
}
}

View File

@@ -0,0 +1,10 @@
import common from './common.json'
import sidebar from './sidebar.json'
import settings from './settings.json'
export default {
common,
sidebar,
settings,
}

View File

@@ -0,0 +1,21 @@
{
"title": "Settings",
"tabs": {
"basic": "General",
"aiConfig": "AI Models",
"aiPrompt": "AI Chat Config",
"storage": "Storage",
"about": "About"
},
"basic": {
"language": {
"title": "Language",
"description": "Choose display language"
},
"appearance": {
"title": "Appearance",
"description": "Choose theme mode"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"brand": "ChatLab",
"newAnalysis": "New Analysis",
"tools": "Tools",
"chatHistory": "Chat History",
"noRecords": "No records",
"contextMenu": {
"rename": "Rename",
"pin": "Pin",
"unpin": "Unpin",
"delete": "Delete"
},
"renameModal": {
"title": "Rename",
"placeholder": "Enter new name"
},
"deleteModal": {
"title": "Confirm Delete",
"message": "Are you sure you want to delete \"{name}\"? This action cannot be undone."
},
"tooltip": {
"expand": "Expand sidebar",
"collapse": "Collapse sidebar",
"hint": "Right-click to rename or delete"
},
"sessionInfo": "{count} messages · {time}"
}

View File

@@ -0,0 +1,21 @@
{
"confirm": "确定",
"cancel": "取消",
"delete": "删除",
"save": "保存",
"edit": "编辑",
"rename": "重命名",
"close": "关闭",
"loading": "加载中...",
"noData": "暂无数据",
"error": {
"general": "操作失败,请重试",
"network": "网络错误"
},
"unit": {
"messages": "条消息",
"members": "个成员",
"days": "天"
}
}

View File

@@ -0,0 +1,10 @@
import common from './common.json'
import sidebar from './sidebar.json'
import settings from './settings.json'
export default {
common,
sidebar,
settings,
}

View File

@@ -0,0 +1,21 @@
{
"title": "全局设置",
"tabs": {
"basic": "基础设置",
"aiConfig": "模型配置",
"aiPrompt": "AI 对话配置",
"storage": "存储管理",
"about": "关于"
},
"basic": {
"language": {
"title": "语言",
"description": "选择界面显示语言"
},
"appearance": {
"title": "外观",
"description": "选择主题模式"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"brand": "ChatLab",
"newAnalysis": "分析新聊天",
"tools": "实用工具",
"chatHistory": "聊天记录",
"noRecords": "暂无记录",
"contextMenu": {
"rename": "重命名",
"pin": "置顶",
"unpin": "取消置顶",
"delete": "删除"
},
"renameModal": {
"title": "重命名",
"placeholder": "请输入新名称"
},
"deleteModal": {
"title": "确认删除",
"message": "确定要删除聊天记录 \"{name}\" 吗?此操作无法撤销。"
},
"tooltip": {
"expand": "展开侧边栏",
"collapse": "收起侧边栏",
"hint": "右键可删除或重命名聊天记录"
},
"sessionInfo": "{count} 条消息 · {time}"
}

71
src/i18n/types.ts Normal file
View File

@@ -0,0 +1,71 @@
/**
* 支持的语言类型
*/
export type LocaleType = 'zh-CN' | 'en-US'
/**
* 语言配置项
*/
export interface LocaleOption {
code: LocaleType
name: string
nativeName: string
}
/**
* 可用的语言列表
*/
export const availableLocales: LocaleOption[] = [
{ code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文' },
{ code: 'en-US', name: 'English (US)', nativeName: 'English' },
]
/**
* 默认语言
*/
export const defaultLocale: LocaleType = 'zh-CN'
/**
* 检测系统语言
*/
export function detectSystemLocale(): LocaleType {
const systemLocale = navigator.language
if (systemLocale.startsWith('zh')) {
return 'zh-CN'
}
return 'en-US'
}
/**
* 功能模块的语言支持配置
* 某些功能可能只支持特定语言
*/
export interface FeatureLocaleSupport {
/** 功能标识 */
feature: string
/** 支持的语言列表,如果为空则支持所有语言 */
supportedLocales: LocaleType[]
}
/**
* 功能语言限制配置
* 用于控制某些功能只在特定语言下显示
*/
export const featureLocaleRestrictions: Record<string, LocaleType[]> = {
// 群榜单(龙王、夜猫等)只在中文下显示
groupRanking: ['zh-CN'],
// 以后可以在这里添加更多限制
}
/**
* 检查功能是否支持当前语言
*/
export function isFeatureSupported(feature: string, currentLocale: LocaleType): boolean {
const supportedLocales = featureLocaleRestrictions[feature]
// 如果没有配置限制,则支持所有语言
if (!supportedLocales || supportedLocales.length === 0) {
return true
}
return supportedLocales.includes(currentLocale)
}

View File

@@ -4,6 +4,7 @@ import { router } from './routes/'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import ui from '@nuxt/ui/vue-plugin'
import i18n from './i18n'
import './assets/styles/main.css'
const app = createApp(App)
@@ -14,5 +15,6 @@ pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(ui)
app.use(i18n)
app.mount('#app')

55
src/stores/settings.ts Normal file
View File

@@ -0,0 +1,55 @@
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import 'dayjs/locale/en'
import { type LocaleType, detectSystemLocale, setLocale as setI18nLocale } from '@/i18n'
/**
* 全局设置 Store
* 管理语言偏好、外观设置等
*/
export const useSettingsStore = defineStore(
'settings',
() => {
// 语言设置(默认检测系统语言)
const locale = ref<LocaleType>(detectSystemLocale())
/**
* 切换语言
*/
function setLocale(newLocale: LocaleType) {
locale.value = newLocale
// 同步更新 vue-i18n
setI18nLocale(newLocale)
// 同步更新 dayjs
dayjs.locale(newLocale === 'zh-CN' ? 'zh-cn' : 'en')
// 通知主进程(用于对话框等)
window.electron?.ipcRenderer.send('locale:change', newLocale)
}
/**
* 初始化语言设置
* 应在应用启动时调用
*/
function initLocale() {
// 同步 i18n 和 dayjs 到当前保存的语言
setI18nLocale(locale.value)
dayjs.locale(locale.value === 'zh-CN' ? 'zh-cn' : 'en')
}
return {
locale,
setLocale,
initLocale,
}
},
{
persist: true, // 持久化到 localStorage
}
)