feat(dashboard): add i18n support with zh/en/ja/ko locales

- Add react-i18next + i18next + i18next-browser-languagedetector
- Create i18n config (singleton import) with zh/en/ja/ko JSON locale files
- Add language switcher Globe dropdown in Header topbar
- Replace all hardcoded Chinese strings in:
  - Layout (Header, Sidebar, NavItem, Layout, constants)
  - Settings (index, AppearanceTab, SecurityTab, OtherTab, AboutTab)
  - Auth page (auth.tsx)
  - Search dialog (searchItems via useMemo + t())
  - Restart overlay (getStatusConfig accepts t param)
  - Error boundary (ErrorFallback, ErrorDetails function components)
  - HTTP warning banner
- localStorage key: maibot-locale
- Compatible with Electron
This commit is contained in:
DrSmoothl
2026-03-03 20:50:06 +08:00
parent 5cc34f24c0
commit a65a40f85f
23 changed files with 7271 additions and 473 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,10 @@
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
"arch": [
"x64",
"arm64"
]
}
],
"icon": "electron/resources/icon.icns",
@@ -46,7 +49,9 @@
"target": [
{
"target": "nsis",
"arch": ["x64"]
"arch": [
"x64"
]
}
],
"icon": "electron/resources/icon.ico"
@@ -55,7 +60,9 @@
"target": [
{
"target": "AppImage",
"arch": ["x64"]
"arch": [
"x64"
]
}
],
"icon": "electron/resources/icon.png",
@@ -130,12 +137,15 @@
"dagre": "^0.8.5",
"date-fns": "^4.1.0",
"html-to-image": "^1.11.13",
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
"idb": "^8.0.3",
"katex": "^0.16.27",
"lucide-react": "^0.556.0",
"react": "^19.2.1",
"react-day-picker": "^9.12.0",
"react-dom": "^19.2.1",
"react-i18next": "^16.5.4",
"react-joyride": "^2.9.3",
"react-markdown": "^10.1.0",
"reactflow": "^11.11.4",

View File

@@ -1,4 +1,5 @@
import { Component } from 'react'
import { useTranslation } from 'react-i18next'
import type { ErrorInfo, ReactNode } from 'react'
import { AlertTriangle, RefreshCw, Home, ChevronDown, ChevronUp, Copy, Check, Bug } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -65,6 +66,7 @@ function ErrorDetails({ error, errorInfo }: { error: Error; errorInfo: ErrorInfo
const [isStackOpen, setIsStackOpen] = useState(true)
const [isComponentStackOpen, setIsComponentStackOpen] = useState(false)
const [copied, setCopied] = useState(false)
const { t } = useTranslation()
const stackFrames = error.stack ? parseStackTrace(error.stack) : []
@@ -183,12 +185,12 @@ Time: ${new Date().toISOString()}
{copied ? (
<>
<Check className="mr-2 h-4 w-4 text-green-500" />
{t('errorBoundary.copiedToClipboard')}
</>
) : (
<>
<Copy className="mr-2 h-4 w-4" />
{t('errorBoundary.copyError')}
</>
)}
</Button>
@@ -204,6 +206,7 @@ function ErrorFallback({
error: Error
errorInfo: ErrorInfo | null
}) {
const { t } = useTranslation()
const handleGoHome = () => {
window.location.href = '/'
}
@@ -219,9 +222,9 @@ function ErrorFallback({
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
<AlertTriangle className="h-8 w-8 text-red-600 dark:text-red-400" />
</div>
<CardTitle className="text-2xl font-bold"></CardTitle>
<CardTitle className="text-2xl font-bold">{t('errorBoundary.title')}</CardTitle>
<CardDescription className="text-base mt-2">
{t('errorBoundary.description')}
</CardDescription>
</CardHeader>
@@ -232,17 +235,17 @@ function ErrorFallback({
<div className="flex flex-col sm:flex-row gap-2 pt-2">
<Button onClick={handleRefresh} className="flex-1">
<RefreshCw className="mr-2 h-4 w-4" />
{t('errorBoundary.refreshPage')}
</Button>
<Button onClick={handleGoHome} variant="outline" className="flex-1">
<Home className="mr-2 h-4 w-4" />
{t('errorBoundary.goHome')}
</Button>
</div>
{/* 提示信息 */}
<p className="text-xs text-center text-muted-foreground pt-2">
{t('errorBoundary.footer')}
</p>
</CardContent>
</Card>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AlertTriangle, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -7,6 +8,7 @@ import { Button } from '@/components/ui/button'
* 当用户通过 HTTP 访问时显示安全警告
*/
export function HttpWarningBanner() {
const { t } = useTranslation()
// 直接计算初始状态,避免 effect 中调用 setState
const isHttp = window.location.protocol === 'http:'
const hostname = window.location.hostname.toLowerCase()
@@ -35,11 +37,11 @@ export function HttpWarningBanner() {
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-500 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
<span className="font-semibold"></span>
使 <strong>HTTP</strong> 访 MaiBot WebUI
<span className="font-semibold">{t('httpWarning.title')}</span>
{t('httpWarning.message')}
</p>
<p className="text-xs text-amber-800 dark:text-amber-200 mt-1">
Token使 HTTPS 访使
{t('httpWarning.description')}
</p>
</div>
</div>
@@ -48,7 +50,7 @@ export function HttpWarningBanner() {
size="icon"
onClick={handleDismiss}
className="h-8 w-8 text-amber-700 hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-200 flex-shrink-0"
aria-label="关闭警告"
aria-label={t('httpWarning.dismiss')}
>
<X className="h-4 w-4" />
</Button>

View File

@@ -1,17 +1,26 @@
import { BookOpen, ChevronLeft, LogOut, Menu, Moon, PieChart, Search, Server, Sun } from 'lucide-react'
import { Link } from '@tanstack/react-router'
import { BookOpen, ChevronLeft, Globe, LogOut, Menu, Moon, PieChart, Search, Server, Sun } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BackgroundLayer } from '@/components/background-layer'
import { Button } from '@/components/ui/button'
import { Kbd } from '@/components/ui/kbd'
import { SearchDialog } from '@/components/search-dialog'
import { useEffect, useState } from 'react'
import { BackendManager } from '@/components/electron/BackendManager'
import { isElectron } from '@/lib/runtime'
import { cn } from '@/lib/utils'
import { SearchDialog } from '@/components/search-dialog'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Kbd } from '@/components/ui/kbd'
import { toggleThemeWithTransition } from '@/components/use-theme'
import { useBackground } from '@/hooks/use-background'
import { logout } from '@/lib/fetch-with-auth'
import { toggleThemeWithTransition } from '@/components/use-theme'
import { isElectron } from '@/lib/runtime'
import { cn } from '@/lib/utils'
const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const
interface HeaderProps {
sidebarOpen: boolean
@@ -26,7 +35,7 @@ interface HeaderProps {
export function Header({
sidebarOpen,
// mobileMenuOpen, // unused - kept in props for API compatibility
searchOpen,
actualTheme,
onSidebarToggle,
@@ -34,6 +43,8 @@ export function Header({
onSearchOpenChange,
onThemeChange,
}: HeaderProps) {
const { t, i18n: i18nInstance } = useTranslation()
const currentLang = i18nInstance.language || 'zh'
const headerBg = useBackground('header')
const [backendManagerOpen, setBackendManagerOpen] = useState(false)
const [activeBackendName, setActiveBackendName] = useState<string>('')
@@ -41,7 +52,7 @@ export function Header({
useEffect(() => {
if (!isElectron()) return
window.electronAPI!.getActiveBackend().then((b) => {
setActiveBackendName(b?.name ?? '未连接')
setActiveBackendName(b?.name ?? t('header.notConnected'))
})
}, [])
@@ -65,7 +76,7 @@ export function Header({
<button
onClick={onSidebarToggle}
className="hidden rounded-lg p-2 hover:bg-accent lg:block"
title={sidebarOpen ? '收起侧边栏' : '展开侧边栏'}
title={sidebarOpen ? t('header.collapseSidebar') : t('header.expandSidebar')}
>
<ChevronLeft
className={cn('h-5 w-5 transition-transform', !sidebarOpen && 'rotate-180')}
@@ -82,7 +93,7 @@ export function Header({
size="sm"
className="gap-2"
onClick={() => setBackendManagerOpen(true)}
title="切换后端连接"
title={t('header.toggleConnection')}
>
<Server className="h-4 w-4" />
<span className="hidden sm:inline text-xs text-muted-foreground truncate max-w-[100px]">
@@ -99,10 +110,10 @@ export function Header({
variant="ghost"
size="sm"
className="gap-2 bg-gradient-to-r from-pink-500/10 to-purple-500/10 hover:from-pink-500/20 hover:to-purple-500/20 border border-pink-500/20"
title="查看年度总结"
title={t('header.viewAnnualSummary')}
>
<PieChart className="h-4 w-4 text-pink-500" />
<span className="hidden sm:inline bg-gradient-to-r from-pink-500 to-purple-500 bg-clip-text text-transparent font-medium">2025 </span>
<span className="hidden sm:inline bg-gradient-to-r from-pink-500 to-purple-500 bg-clip-text text-transparent font-medium">{t('header.annualSummary')}</span>
</Button>
</Link>
@@ -112,7 +123,7 @@ export function Header({
className="relative hidden md:flex items-center w-64 h-9 pl-9 pr-16 bg-background/50 border rounded-md hover:bg-accent/50 transition-colors text-left"
>
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<span className="text-sm text-muted-foreground">...</span>
<span className="text-sm text-muted-foreground">{t('header.searchPlaceholder')}</span>
<Kbd size="sm" className="absolute right-2 top-1/2 -translate-y-1/2">
<span className="text-xs"></span>K
</Kbd>
@@ -127,12 +138,41 @@ export function Header({
size="sm"
onClick={() => window.open('https://docs.mai-mai.org', '_blank')}
className="gap-2"
title="查看麦麦文档"
title={t('header.viewDocs')}
>
<BookOpen className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="hidden sm:inline">{t('header.docs')}</span>
</Button>
{/* 语言切换 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2">
<Globe className="h-4 w-4" />
<span className="hidden sm:inline text-xs">
{t(`language.${currentLang.split('-')[0] as 'zh' | 'en' | 'ja' | 'ko'}`) ?? currentLang}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{LANGUAGE_CODES.map((code) => (
<DropdownMenuItem
key={code}
onClick={() => i18nInstance.changeLanguage(code)}
className={cn(
'cursor-pointer',
currentLang.split('-')[0] === code && 'font-semibold text-primary'
)}
>
{currentLang.split('-')[0] === code && (
<span className="mr-2"></span>
)}
{t(`language.${code}`)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* 主题切换按钮 */}
<button
onClick={(e) => {
@@ -140,7 +180,7 @@ export function Header({
toggleThemeWithTransition(newTheme, onThemeChange, e)
}}
className="rounded-lg p-2 hover:bg-accent"
title={actualTheme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
title={actualTheme === 'dark' ? t('header.switchToLight') : t('header.switchToDark')}
>
{actualTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button>
@@ -154,10 +194,10 @@ export function Header({
size="sm"
onClick={handleLogout}
className="gap-2"
title="登出系统"
title={t('header.logout')}
>
<LogOut className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="hidden sm:inline">{t('header.logoutLabel')}</span>
</Button>
</div>
</header>

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BackgroundLayer } from '@/components/background-layer'
import { BackToTop } from '@/components/back-to-top'
@@ -16,6 +17,7 @@ import { Sidebar } from './Sidebar'
import type { LayoutProps } from './types'
export function Layout({ children }: LayoutProps) {
const { t } = useTranslation()
const { checking } = useAuthGuard() // 检查认证状态
const [sidebarOpen, setSidebarOpen] = useState(true)
@@ -55,7 +57,7 @@ export function Layout({ children }: LayoutProps) {
if (checking) {
return (
<div className="flex h-screen items-center justify-center bg-background">
<div className="text-muted-foreground">...</div>
<div className="text-muted-foreground">{t('layout.verifyingLogin')}</div>
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { Link, useMatchRoute } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
@@ -13,6 +14,7 @@ interface NavItemProps {
}
export function NavItem({ item, sidebarOpen, tooltipsEnabled, onMobileMenuClose }: NavItemProps) {
const { t } = useTranslation()
const matchRoute = useMatchRoute()
const isActive = matchRoute({ to: item.path })
const Icon = item.icon
@@ -42,7 +44,7 @@ export function NavItem({ item, sidebarOpen, tooltipsEnabled, onMobileMenuClose
? 'opacity-100 max-w-[200px]'
: 'opacity-100 max-w-[200px] lg:opacity-0 lg:max-w-0 lg:overflow-hidden'
)}>
{item.label}
{t(item.label)}
</span>
</div>
</>
@@ -70,7 +72,7 @@ export function NavItem({ item, sidebarOpen, tooltipsEnabled, onMobileMenuClose
</TooltipTrigger>
{tooltipsEnabled && (
<TooltipContent side="right" className="hidden lg:block">
<p>{item.label}</p>
<p>{t(item.label)}</p>
</TooltipContent>
)}
</Tooltip>

View File

@@ -1,3 +1,5 @@
import { useTranslation } from 'react-i18next'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
import { useBackground } from '@/hooks/use-background'
@@ -20,6 +22,7 @@ export function Sidebar({
tooltipsEnabled,
onMobileMenuClose
}: SidebarProps) {
const { t } = useTranslation()
const sidebarBg = useBackground('sidebar')
return (
@@ -60,7 +63,7 @@ export function Sidebar({
!sidebarOpen && "lg:mb-1 lg:invisible"
)}>
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/60 whitespace-nowrap">
{section.title}
{t(section.title)}
</h3>
</div>

View File

@@ -4,46 +4,46 @@ import type { MenuSection } from './types'
export const menuSections: MenuSection[] = [
{
title: '概览',
title: 'sidebar.groups.overview',
items: [
{ icon: Home, label: '首页', path: '/' },
{ icon: Home, label: 'sidebar.menu.home', path: '/' },
],
},
{
title: '麦麦配置编辑',
title: 'sidebar.groups.botConfig',
items: [
{ icon: FileText, label: '麦麦主程序配置', path: '/config/bot' },
{ icon: Server, label: 'AI模型厂商配置', path: '/config/modelProvider', tourId: 'sidebar-model-provider' },
{ icon: Boxes, label: '模型管理与分配', path: '/config/model', tourId: 'sidebar-model-management' },
{ icon: Sliders, label: '麦麦适配器配置', path: '/config/adapter' },
{ icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot' },
{ icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', tourId: 'sidebar-model-provider' },
{ icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', tourId: 'sidebar-model-management' },
{ icon: Sliders, label: 'sidebar.menu.adapterConfig', path: '/config/adapter' },
],
},
{
title: '麦麦资源管理',
title: 'sidebar.groups.botResources',
items: [
{ icon: Smile, label: '表情包管理', path: '/resource/emoji' },
{ icon: MessageSquare, label: '表达方式管理', path: '/resource/expression' },
{ icon: Hash, label: '黑话管理', path: '/resource/jargon' },
{ icon: UserCircle, label: '人物信息管理', path: '/resource/person' },
{ icon: Network, label: '知识库图谱可视化', path: '/resource/knowledge-graph' },
{ icon: Database, label: '麦麦知识库管理', path: '/resource/knowledge-base' },
{ icon: Smile, label: 'sidebar.menu.emojiManagement', path: '/resource/emoji' },
{ icon: MessageSquare, label: 'sidebar.menu.expressionManagement', path: '/resource/expression' },
{ icon: Hash, label: 'sidebar.menu.slangManagement', path: '/resource/jargon' },
{ icon: UserCircle, label: 'sidebar.menu.personInfo', path: '/resource/person' },
{ icon: Network, label: 'sidebar.menu.knowledgeGraph', path: '/resource/knowledge-graph' },
{ icon: Database, label: 'sidebar.menu.knowledgeBase', path: '/resource/knowledge-base' },
],
},
{
title: '扩展与监控',
title: 'sidebar.groups.extensionsMonitor',
items: [
{ icon: Package, label: '插件市场', path: '/plugins' },
{ icon: LayoutGrid, label: '配置模板市场', path: '/config/pack-market' },
{ icon: Sliders, label: '插件配置', path: '/plugin-config' },
{ icon: FileSearch, label: '日志查看器', path: '/logs' },
{ icon: Activity, label: '计划器&回复器监控', path: '/planner-monitor' },
{ icon: MessageSquare, label: '本地聊天室', path: '/chat' },
{ icon: Package, label: 'sidebar.menu.pluginMarket', path: '/plugins' },
{ icon: LayoutGrid, label: 'sidebar.menu.configTemplate', path: '/config/pack-market' },
{ icon: Sliders, label: 'sidebar.menu.pluginConfig', path: '/plugin-config' },
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs' },
{ icon: Activity, label: 'sidebar.menu.plannerMonitor', path: '/planner-monitor' },
{ icon: MessageSquare, label: 'sidebar.menu.localChat', path: '/chat' },
],
},
{
title: '系统',
title: 'sidebar.groups.system',
items: [
{ icon: Settings, label: '系统设置', path: '/settings' },
{ icon: Settings, label: 'sidebar.menu.settings', path: '/settings' },
],
},
]

View File

@@ -17,6 +17,7 @@
*/
import { useEffect, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
Loader2,
CheckCircle2,
@@ -70,6 +71,7 @@ const getStatusConfig = (
status: RestartStatus,
checkAttempts: number,
maxAttempts: number,
t: (key: string, opts?: Record<string, unknown>) => string,
customTitle?: string,
customDescription?: string
): StatusConfig => {
@@ -82,33 +84,33 @@ const getStatusConfig = (
},
requesting: {
icon: <Loader2 className="h-16 w-16 text-primary animate-spin" />,
title: customTitle ?? '准备重启',
description: customDescription ?? '正在发送重启请求...',
tip: '🔄 正在准备重启麦麦...',
title: customTitle ?? t('restart.preparing'),
description: customDescription ?? t('restart.preparingDesc'),
tip: t('restart.preparingTip'),
},
restarting: {
icon: <Loader2 className="h-16 w-16 text-primary animate-spin" />,
title: customTitle ?? '正在重启麦麦',
description: customDescription ?? '请稍候,麦麦正在重启中...',
tip: '🔄 配置已保存,正在重启主程序...',
title: customTitle ?? t('restart.restarting'),
description: customDescription ?? t('restart.restartingDesc'),
tip: t('restart.restartingTip'),
},
checking: {
icon: <Loader2 className="h-16 w-16 text-primary animate-spin" />,
title: '检查服务状态',
description: `等待服务恢复... (${checkAttempts}/${maxAttempts})`,
tip: '⏳ 正在等待服务恢复,请勿关闭页面...',
title: t('restart.checking'),
description: t('restart.checkingDesc', { current: checkAttempts, max: maxAttempts }),
tip: t('restart.checkingTip'),
},
success: {
icon: <CheckCircle2 className="h-16 w-16 text-green-500" />,
title: '重启成功',
description: '正在跳转到登录页面...',
tip: '✅ 配置已生效,服务运行正常',
title: t('restart.success'),
description: t('restart.successDesc'),
tip: t('restart.successTip'),
},
failed: {
icon: <AlertCircle className="h-16 w-16 text-destructive" />,
title: '重启超时',
description: '服务未能在预期时间内恢复',
tip: '⚠️ 如果长时间无响应,请尝试手动重启',
title: t('restart.failed'),
description: t('restart.failedDesc'),
tip: t('restart.failedTip'),
},
}
return configs[status]
@@ -192,6 +194,7 @@ function RestartOverlayContent({
className,
}: RestartOverlayContentProps) {
const { status, progress, elapsedTime, checkAttempts, maxAttempts } = state
const { t } = useTranslation()
// 回调处理
useEffect(() => {
@@ -206,6 +209,7 @@ function RestartOverlayContent({
status,
checkAttempts,
maxAttempts,
t,
title,
description
)
@@ -246,7 +250,7 @@ function RestartOverlayContent({
<Progress value={progress} className="h-2" />
<div className="flex justify-between text-sm text-muted-foreground">
<span>{progress}%</span>
<span>: {formatTime(elapsedTime)}</span>
<span>{t('restart.elapsed')} {formatTime(elapsedTime)}</span>
</div>
</div>
)}
@@ -265,11 +269,11 @@ function RestartOverlayContent({
className="flex-1"
>
<RefreshCw className="mr-2 h-4 w-4" />
{t('restart.refreshPage')}
</Button>
<Button onClick={onRetry} variant="secondary" className="flex-1">
<RotateCcw className="mr-2 h-4 w-4" />
{t('restart.retryCheck')}
</Button>
</div>
)}

View File

@@ -1,6 +1,8 @@
import { useState, useCallback } from 'react'
import { useState, useCallback, useMemo } from 'react'
import { Search, FileText, Server, Boxes, Smile, MessageSquare, UserCircle, FileSearch, BarChart3, Package, Settings, Home, Hash } from 'lucide-react'
import { useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
@@ -24,97 +26,98 @@ interface SearchItem {
category: string
}
const searchItems: SearchItem[] = [
{
icon: Home,
title: '首页',
description: '查看仪表板概览',
path: '/',
category: '概览',
},
{
icon: FileText,
title: '麦麦主程序配置',
description: '配置麦麦的核心设置',
path: '/config/bot',
category: '配置',
},
{
icon: Server,
title: '麦麦模型提供商配置',
description: '配置模型提供商',
path: '/config/modelProvider',
category: '配置',
},
{
icon: Boxes,
title: '麦麦模型配置',
description: '配置模型参数',
path: '/config/model',
category: '配置',
},
{
icon: Smile,
title: '表情包管理',
description: '管理麦麦的表情包',
path: '/resource/emoji',
category: '资源',
},
{
icon: MessageSquare,
title: '表达方式管理',
description: '管理麦麦的表达方式',
path: '/resource/expression',
category: '资源',
},
{
icon: UserCircle,
title: '人物信息管理',
description: '管理人物信息',
path: '/resource/person',
category: '资源',
},
{
icon: Hash,
title: '黑话管理',
description: '管理麦麦学习到的黑话和俚语',
path: '/resource/jargon',
category: '资源',
},
{
icon: BarChart3,
title: '统计信息',
description: '查看使用统计',
path: '/statistics',
category: '监控',
},
{
icon: Package,
title: '插件市场',
description: '浏览和安装插件',
path: '/plugins',
category: '扩展',
},
{
icon: FileSearch,
title: '日志查看器',
description: '查看系统日志',
path: '/logs',
category: '监控',
},
{
icon: Settings,
title: '系统设置',
description: '配置系统参数',
path: '/settings',
category: '系统',
},
]
export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
const [searchQuery, setSearchQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0)
const navigate = useNavigate()
const { t } = useTranslation()
const searchItems: SearchItem[] = useMemo(() => [
{
icon: Home,
title: t('search.items.home'),
description: t('search.items.homeDesc'),
path: '/',
category: t('search.categories.overview'),
},
{
icon: FileText,
title: t('search.items.botConfig'),
description: t('search.items.botConfigDesc'),
path: '/config/bot',
category: t('search.categories.config'),
},
{
icon: Server,
title: t('search.items.modelProvider'),
description: t('search.items.modelProviderDesc'),
path: '/config/modelProvider',
category: t('search.categories.config'),
},
{
icon: Boxes,
title: t('search.items.model'),
description: t('search.items.modelDesc'),
path: '/config/model',
category: t('search.categories.config'),
},
{
icon: Smile,
title: t('search.items.emoji'),
description: t('search.items.emojiDesc'),
path: '/resource/emoji',
category: t('search.categories.resources'),
},
{
icon: MessageSquare,
title: t('search.items.expression'),
description: t('search.items.expressionDesc'),
path: '/resource/expression',
category: t('search.categories.resources'),
},
{
icon: UserCircle,
title: t('search.items.person'),
description: t('search.items.personDesc'),
path: '/resource/person',
category: t('search.categories.resources'),
},
{
icon: Hash,
title: t('search.items.jargon'),
description: t('search.items.jargonDesc'),
path: '/resource/jargon',
category: t('search.categories.resources'),
},
{
icon: BarChart3,
title: t('search.items.statistics'),
description: t('search.items.statisticsDesc'),
path: '/statistics',
category: t('search.categories.monitor'),
},
{
icon: Package,
title: t('search.items.plugins'),
description: t('search.items.pluginsDesc'),
path: '/plugins',
category: t('search.categories.extensions'),
},
{
icon: FileSearch,
title: t('search.items.logs'),
description: t('search.items.logsDesc'),
path: '/logs',
category: t('search.categories.monitor'),
},
{
icon: Settings,
title: t('search.items.settings'),
description: t('search.items.settingsDesc'),
path: '/settings',
category: t('search.categories.system'),
},
], [t])
// 过滤搜索结果
const filteredItems = searchItems.filter(
@@ -154,7 +157,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl p-0 gap-0">
<DialogHeader className="px-4 pt-4 pb-0">
<DialogTitle className="sr-only"></DialogTitle>
<DialogTitle className="sr-only">{t('search.title')}</DialogTitle>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -164,7 +167,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
setSelectedIndex(0)
}}
onKeyDown={handleKeyDown}
placeholder="搜索页面..."
placeholder={t('search.placeholder')}
className="h-12 pl-11 text-base border-0 focus-visible:ring-0 shadow-none"
autoFocus
/>
@@ -207,7 +210,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground/50 mb-4" />
<p className="text-sm text-muted-foreground">
{searchQuery ? '未找到匹配的页面' : '输入关键词开始搜索'}
{searchQuery ? t('search.noResults') : t('search.startSearch')}
</p>
</div>
)}
@@ -219,15 +222,15 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded border"></kbd>
<kbd className="px-1.5 py-0.5 bg-muted rounded border"></kbd>
{t('search.navigate')}
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded border">Enter</kbd>
{t('search.select')}
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded border">Esc</kbd>
{t('search.close')}
</span>
</div>
</div>

View File

@@ -0,0 +1,37 @@
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import i18next from 'i18next'
import en from './locales/en.json'
import ja from './locales/ja.json'
import ko from './locales/ko.json'
import zh from './locales/zh.json'
i18next
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
zh: { translation: zh },
en: { translation: en },
ja: { translation: ja },
ko: { translation: ko },
},
fallbackLng: 'en',
supportedLngs: ['zh', 'en', 'ja', 'ko'],
interpolation: {
escapeValue: false,
},
detection: {
order: ['localStorage', 'navigator'],
lookupLocalStorage: 'maibot-locale',
caches: ['localStorage'],
},
keySeparator: '.',
})
i18next.on('languageChanged', (lng) => {
document.documentElement.lang = lng
})
export default i18next

View File

@@ -0,0 +1,477 @@
{
"language": { "zh": "中文", "en": "English", "ja": "日本語", "ko": "한국어" },
"header": {
"collapseSidebar": "Collapse sidebar",
"expandSidebar": "Expand sidebar",
"toggleConnection": "Toggle backend connection",
"viewAnnualSummary": "View annual summary",
"annualSummary": "2025 Annual Summary",
"searchPlaceholder": "Search...",
"viewDocs": "View MaiBot docs",
"docs": "MaiBot Docs",
"switchToLight": "Switch to light mode",
"switchToDark": "Switch to dark mode",
"logout": "Log out",
"logoutLabel": "Logout",
"notConnected": "Not connected"
},
"sidebar": {
"groups": {
"overview": "Overview",
"botConfig": "Bot Configuration",
"botResources": "Bot Resources",
"extensionsMonitor": "Extensions & Monitor",
"system": "System"
},
"menu": {
"home": "Home",
"botMainConfig": "Bot Main Config",
"aiModelProvider": "AI Model Providers",
"modelManagement": "Model Management",
"adapterConfig": "Adapter Config",
"emojiManagement": "Emoji Management",
"expressionManagement": "Expression Management",
"slangManagement": "Slang Management",
"personInfo": "Person Info",
"knowledgeGraph": "Knowledge Graph",
"knowledgeBase": "Knowledge Base",
"pluginMarket": "Plugin Market",
"configTemplate": "Config Templates",
"pluginConfig": "Plugin Config",
"logViewer": "Log Viewer",
"plannerMonitor": "Planner & Replier Monitor",
"localChat": "Local Chat",
"settings": "Settings"
}
},
"layout": {
"verifyingLogin": "Verifying login status...",
"logoTitle": "MaiBot WebUI",
"logoTitleShort": "M"
},
"settings": {
"title": "Settings",
"description": "Manage your application preferences",
"tabs": {
"appearance": "Appearance",
"security": "Security",
"other": "Other",
"about": "About"
},
"appearance": {
"themeMode": "Theme Mode",
"themeModeDesc": "Light / Dark / Follow system",
"light": "Light",
"dark": "Dark",
"system": "System",
"accentColor": "Accent Color",
"resetDefault": "Reset Default",
"colorPreview": "Live Color Preview",
"styleTweaks": "Style Tweaks",
"typography": "Typography",
"visualEffects": "Visual Effects",
"layout": "Layout",
"animation": "Animation",
"background": "Background",
"customCss": "Custom CSS",
"animationEffect": "Animation Effect",
"importExportTheme": "Import / Export Theme",
"importTheme": "Import Theme",
"exportTheme": "Export Theme",
"importSuccess": "Import successful",
"importFailed": "Import failed",
"resetSuccess": "Reset successful",
"fontFamily": "Font Family",
"fontSize": "Font Size",
"borderRadius": "Border Radius",
"contentWidth": "Content Width",
"sidebarWidth": "Sidebar Width",
"animationSpeed": "Animation Speed",
"backgroundImage": "Background Image",
"backgroundBlur": "Background Blur",
"backgroundOpacity": "Background Opacity",
"lightDesc": "Always use light theme",
"darkDesc": "Always use dark theme",
"systemDesc": "Auto-switch based on system settings",
"accentPrimary": "Primary Color",
"accentHint": "Click the color ring or enter a HEX value",
"resetTheme": "Reset to Default",
"confirmResetTheme": "Confirm Reset Theme",
"confirmResetThemeDesc": "This will reset all theme settings to default, including colors, fonts, layout and custom CSS. This cannot be undone, are you sure?",
"confirmResetAction": "Confirm Reset",
"cssWarningTitle": "The following have been filtered for safety:",
"cssPlaceholder": "/* Enter custom CSS here */\n\n/* Example: */\n/* .sidebar { background: #1a1a2e; } */",
"cssDescription": "Write custom CSS to further personalize the interface. Dangerous CSS (like @import, url()) will be automatically filtered.",
"clearCss": "Clear",
"exportDesc": "Export theme as a JSON file for sharing or backup; all settings will be applied automatically on import.",
"importSuccessDesc": "Theme config imported, page will reload automatically",
"resetSuccessDesc": "Theme has been reset to default",
"enableAnimations": "Enable Animations",
"enableAnimationsDesc": "Disabling this will turn off all transition animations and effects, improving performance",
"loginWavesBackground": "Login Page Wave Background",
"loginWavesBackgroundDesc": "Disabling this will use a solid color background on the login page, suitable for low-performance devices",
"inheritParentBg": "Inherit Parent Background",
"inheritParentBgDesc": "When enabled, uses the background config from the parent layer",
"fontFamilyLabel": "Font Family",
"fontFamilyPlaceholder": "Select font family",
"fontFamilySystem": "System Default (System)",
"fontFamilySans": "Sans-serif",
"fontFamilySerif": "Serif",
"fontFamilyMono": "Monospace",
"baseFontSize": "Base Font Size",
"lineHeight": "Line Height",
"lineHeightPlaceholder": "Select line height",
"lineHeightCompact": "Compact (1.2)",
"lineHeightNormal": "Normal (1.5)",
"lineHeightLoose": "Loose (1.75)",
"borderRadiusLabel": "Border Radius",
"shadowLabel": "Shadow Intensity",
"shadowPlaceholder": "Select shadow intensity",
"shadowNone": "None",
"shadowSm": "Small",
"shadowMd": "Medium",
"shadowLg": "Large",
"shadowXl": "Extra Large",
"blurLabel": "Blur Effect",
"sidebarWidthLabel": "Sidebar Width",
"maxContentWidth": "Max Content Width",
"spacingUnit": "Spacing Unit",
"animationSpeedLabel": "Animation Speed",
"animationSpeedPlaceholder": "Select animation speed",
"animationFast": "Fast (100ms)",
"animationNormal": "Normal (300ms)",
"animationSlow": "Slow (500ms)",
"animationOff": "Off (0ms)",
"bgPage": "Page",
"bgSidebar": "Sidebar",
"typographyGroup": "Typography",
"visualGroup": "Visual Effects",
"layoutGroup": "Layout",
"animationGroup": "Animation",
"backgroundGroup": "Background Settings"
},
"security": {
"currentToken": "Current Access Token",
"yourToken": "Your Access Token",
"regenerate": "Regenerate",
"customToken": "Custom Access Token",
"securityTip": "Security Tips",
"cannotCopy": "Cannot copy",
"copySuccess": "Copied",
"copyFailed": "Copy failed",
"updateSuccess": "Updated",
"updateFailed": "Update failed",
"generateSuccess": "Generated",
"generateFailed": "Generation failed",
"newToken": "New Access Token",
"confirmRegenerate": "Confirm Regenerate Token",
"confirmRegenerateDesc": "The old token will be invalidated after regeneration. You will need to log in again.",
"cancel": "Cancel",
"confirm": "Confirm",
"cannotCopyDesc": "Token is stored in a secure Cookie. Please regenerate to get a new Token.",
"copySuccessDesc": "Token copied to clipboard",
"copyFailedDesc": "Please copy the Token manually",
"inputError": "Input error",
"inputErrorDesc": "Please enter a new Token",
"formatError": "Format error",
"formatErrorDesc": "Token does not meet requirements: {{failedRules}}",
"updateSuccessDesc": "Access Token updated. Redirecting to login page.",
"updateFailedDesc": "Unable to update Token",
"updateFailedConn": "Failed to connect to server",
"generateSuccessDesc": "New Access Token generated. Please save it immediately.",
"generateFailedDesc": "Unable to generate new Token",
"generateFailedConn": "Failed to connect to server",
"cannotView": "Cannot view",
"cannotViewDesc": "Token is stored in a secure Cookie. Click \"Regenerate\" to get a new Token.",
"hide": "Hide",
"show": "Show",
"copyTip": "Copy to clipboard",
"regenerateShort": "Generate",
"confirmRegenerateFullDesc": "This will generate a new 64-character secure token, immediately invalidating the current Token. You will need to log in again with the new Token. This action cannot be undone. Are you sure?",
"confirmGenerate": "Confirm Generate",
"tokenStorePlaceholder": "Token is stored in a secure Cookie",
"safekeepTip": "Keep your Access Token safe and do not share it with others.",
"newTokenLabel": "New Access Token",
"customTokenPlaceholder": "Enter custom Token",
"tokenReqTitle": "Token security requirements:",
"tokenValid": "Token format is valid and ready to use",
"updateBtn": "Update Custom Token",
"updating": "Updating...",
"dialogTitle": "New Access Token",
"dialogDesc": "This is your new Token. Please save it immediately. You will be redirected to the login page after closing this window.",
"dialogTokenLabel": "Your new Token (64-char secure token)",
"important": "Important Notice",
"tip1": "This Token is only shown once and cannot be viewed after closing",
"tip2": "Copy and save it to a secure location immediately",
"tip3": "You will be automatically redirected to the login page after closing",
"tip4": "Use the new Token to log back in",
"copied": "Copied",
"copyToken": "Copy Token",
"savedClose": "Saved, close",
"securityTip1": "Regenerating creates a new system-generated 64-character secure token",
"securityTip2": "Custom Tokens must meet all security requirements",
"securityTip3": "After updating, the old Token will be immediately invalidated",
"securityTip4": "View and copy your Token only in a secure environment",
"securityTip5": "If you suspect a Token leak, regenerate or update it immediately",
"securityTip6": "System-generated Tokens are recommended for maximum security"
},
"other": {
"performance": "Performance & Storage",
"localStorage": "Local Storage Usage",
"logCache": "Log Cache Size",
"importExport": "Import / Export Settings",
"configWizard": "Config Wizard",
"devTools": "Developer Tools",
"clearStorage": "Clear Local Storage",
"clearStorageDesc": "Clear all local storage data",
"clearStorageConfirm": "Confirm Clear",
"clearLogCache": "Clear Log Cache",
"clearLogCacheDesc": "Clear all cached log data",
"clearLogCacheConfirm": "Confirm Clear",
"importSettings": "Import Settings",
"exportSettings": "Export Settings",
"resetAllSettings": "Reset All Settings",
"resetAllSettingsDesc": "Restore all settings to defaults",
"resetAllSettingsConfirm": "Confirm Reset",
"clearStorageSuccess": "Local storage cleared",
"clearStorageFailed": "Failed to clear",
"clearLogSuccess": "Log cache cleared",
"clearLogFailed": "Failed to clear",
"importSuccess": "Import successful",
"importFailed": "Import failed",
"exportSuccess": "Export successful",
"exportFailed": "Export failed",
"resetSuccess": "Reset successful",
"resetFailed": "Reset failed",
"storageItems": "{{count}} storage items",
"logCacheSizeDesc": "Controls the maximum number of log entries cached. Larger values use more memory.",
"logCacheSizeUnit": "entries",
"dataSyncIntervalLabel": "Home Data Refresh Interval",
"dataSyncIntervalUnit": "s",
"dataSyncIntervalDesc": "Controls the auto-refresh interval for home page statistics",
"wsReconnectLabel": "WebSocket Reconnect Interval",
"wsReconnectUnit": "s",
"wsReconnectDesc": "Base reconnect interval after log WebSocket disconnects",
"wsMaxReconnectLabel": "WebSocket Max Reconnect Attempts",
"wsMaxReconnectUnit": "times",
"wsMaxReconnectDesc": "Maximum reconnect attempts after connection failure",
"clearLogCacheFn": "Clear Log Cache",
"clearLocalCache": "Clear Local Cache",
"confirmClearCache": "Confirm Clear Local Cache",
"confirmClearCacheDesc": "This will clear all locally cached settings and data (excluding login credentials). You may need to reconfigure some preferences. Are you sure?",
"confirmClear": "Confirm Clear",
"importExportDesc": "Export current interface settings for backup, or restore from a previously exported file.",
"exporting": "Exporting...",
"importing": "Importing...",
"resetAllSettingsBtn": "Reset All Settings to Defaults",
"confirmResetAll": "Confirm Reset All Settings",
"confirmResetAllDesc": "This will restore all interface settings to their defaults, including theme, colors, animation, and other preferences. This will not affect your login status. Are you sure?",
"configWizardDesc": "Re-run the initial setup wizard to reconfigure system basics.",
"rerunSetup": "Re-run Initial Setup",
"confirmRerunSetup": "Confirm Re-run Setup",
"confirmRerunSetupDesc": "This will take you back to the initial setup wizard. You can reconfigure the system basics. Are you sure?",
"devToolsDesc": "The following features are for development and debugging only. They may cause crashes or abnormal behavior.",
"triggerError": "Trigger Test Error",
"confirmTriggerError": "Confirm Trigger Error",
"confirmTriggerErrorDesc": "This will manually trigger a React error to test the error boundary component. The page will display an error view; you can recover by refreshing the page or clicking Go Home.",
"confirmTrigger": "Confirm Trigger",
"logCleared": "Log cleared",
"logClearedDesc": "Log cache has been cleared",
"cacheCleared": "Cache cleared",
"cacheClearedDesc": "Cleared {{count}} cached items",
"exportSuccessDesc": "Settings exported as a JSON file",
"exportFailedDesc": "Unable to export settings",
"importSuccessDesc": "Successfully imported {{imported}} settings",
"importSkippedSuffix": ", skipped {{skipped}}",
"importRefreshHint": "Note",
"importRefreshHintDesc": "Some settings require a page refresh to take full effect",
"importNoDataDesc": "No valid settings to import",
"importInvalidDesc": "Invalid file format",
"resetDone": "Reset done",
"resetDoneDesc": "All settings restored to defaults. Refresh the page to apply changes."
},
"about": {
"openSource": "Open Source",
"aboutApp": "About MaiBot Dashboard",
"version": "Version:",
"author": "Author",
"techStack": "Tech Stack",
"frontendFramework": "Frontend Framework",
"uiComponents": "UI Components",
"backend": "Backend",
"buildTool": "Build Tool",
"openSourceThanks": "Open Source Libraries",
"openSourceLicense": "Open Source License",
"openSourceDesc": "This project is open source on GitHub. Give us a Star ⭐!",
"visitGitHub": "Visit GitHub",
"appDesc": "A modern Web management interface for MaiBot",
"maimaiCore": "MaiBot Core",
"uiFrameworkGroup": "UI Framework & Components",
"routingStateGroup": "Routing & State Management",
"formGroup": "Form Handling",
"utilsGroup": "Utility Libraries",
"animationGroup": "Animation",
"backendGroup": "Backend Framework",
"devToolsGroup": "Developer Tools",
"openSourceThanksDesc": "This project uses the following excellent open source libraries. Thank you for your contributions:",
"licenseDesc": "This project is licensed under the GNU General Public License v3.0. You are free to use, modify, and distribute the software, provided you keep the same open source license.",
"licenseDeps": "All open source libraries used by this project comply with their respective licenses (MIT, Apache-2.0, BSD, etc.). Thank you to all open source contributors for your selfless work.",
"lib": {
"react": "UI library for building user interfaces",
"shadcn": "Elegant React component library",
"radix": "Unstyled accessible component primitives",
"tailwind": "Utility-first CSS framework",
"lucide": "Beautiful icon library",
"tanstackRouter": "Type-safe routing library",
"zustand": "Lightweight state management",
"reactHookForm": "High-performance form library",
"zod": "TypeScript-first schema validation",
"clsx": "Conditional className builder",
"tailwindMerge": "Tailwind class name merger",
"cva": "Component variant management",
"dateFns": "Modern date utility library",
"framerMotion": "React animation library",
"vaul": "Drawer component animation",
"fastapi": "Modern Python web framework",
"uvicorn": "ASGI server",
"pydantic": "Data validation library",
"pythonMultipart": "File upload support",
"typescript": "Superset of JavaScript",
"vite": "Next-generation frontend build tool",
"eslint": "JavaScript code linter",
"postcss": "CSS transformation tool"
}
}
},
"auth": {
"title": "Login",
"description": "Enter your access token to continue",
"tokenLabel": "Access Token",
"tokenPlaceholder": "Enter Access Token",
"loginButton": "Login",
"loggingIn": "Logging in...",
"loginFailed": "Login failed",
"loginSuccess": "Login successful",
"checkingAuth": "Checking login status...",
"welcome": "Welcome to MaiBot",
"accessDesc": "Enter your Access Token to continue accessing the system",
"tokenRequired": "Please enter your Access Token",
"verifyingLabel": "Verifying...",
"verifyEnter": "Verify & Enter",
"helpLink": "I don't have a Token. Where can I get one?",
"helpTitle": "How to Get an Access Token",
"helpDesc": "Access Token is the only credential to access MaiBot WebUI. Get yours in one of the following ways",
"method1Title": "Method 1: Check Startup Log",
"method1Desc": "When MaiBot starts, the console will display the WebUI Access Token.",
"method1Example1": "🔑 WebUI Access Token: abc123...",
"method1Example2": "💡 Use this Token to log in to WebUI",
"method2Title": "Method 2: Check Config File",
"method2Desc": "The Token is saved in the config file at the project root:",
"method2FileHint": "Open this file and copy the value of the access_token field",
"securityTipTitle": "Security Notice",
"securityTip1": "Keep your Token safe and never share it with others",
"securityTip2": "To reset the Token, go to System Settings after logging in",
"slowLink": "The interface feels laggy. What can I do?",
"disableAnimTitle": "Disable Background Animation",
"disableAnimDesc": "Background animation may cause lag on low-performance devices. Disabling it can significantly improve smoothness.",
"disableAnimDetail": "After disabling, the background will be a solid color, but all features remain fully functional. You can re-enable it anytime in System Settings.",
"disableAnimBtn": "Disable Animation",
"verifyFailed": "Token verification failed. Please check and try again.",
"connFailed": "Failed to connect to the server. Please check your network connection.",
"switchToLight": "Switch to light mode",
"switchToDark": "Switch to dark mode"
},
"common": {
"loading": "Loading...",
"error": "Error",
"retry": "Retry",
"save": "Save",
"cancel": "Cancel",
"confirm": "Confirm",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"search": "Search",
"noData": "No data",
"success": "Success",
"failed": "Failed"
},
"restart": {
"preparing": "Preparing to restart",
"preparingDesc": "Sending restart request...",
"preparingTip": "🔄 Preparing to restart MaiBot...",
"restarting": "Restarting MaiBot",
"restartingDesc": "Please wait, MaiBot is restarting...",
"restartingTip": "🔄 Config saved, restarting main process...",
"checking": "Checking service status",
"checkingDesc": "Waiting for service to recover... ({{current}}/{{max}})",
"checkingTip": "⏳ Waiting for service to recover, do not close this page...",
"success": "Restart successful",
"successDesc": "Redirecting to login page...",
"successTip": "✅ Config applied, service is running normally",
"failed": "Restart timed out",
"failedDesc": "Service failed to recover within the expected time",
"failedTip": "⚠️ If unresponsive for a long time, try restarting manually",
"refreshPage": "Refresh page",
"retryCheck": "Retry check",
"elapsed": "Elapsed:"
},
"errorBoundary": {
"title": "Something went wrong",
"description": "The application encountered an unexpected error. You can try refreshing the page or going back home.",
"refreshPage": "Refresh page",
"goHome": "Go home",
"footer": "If the problem persists, copy the error info and report it to the developer",
"copiedToClipboard": "Copied to clipboard",
"copyError": "Copy error info"
},
"search": {
"placeholder": "Search pages...",
"title": "Search",
"noResults": "No matching pages found",
"startSearch": "Type a keyword to start searching",
"navigate": "Navigate",
"select": "Select",
"close": "Close",
"categories": {
"overview": "Overview",
"config": "Config",
"resources": "Resources",
"monitor": "Monitor",
"extensions": "Extensions",
"system": "System"
},
"items": {
"home": "Home",
"homeDesc": "View dashboard overview",
"botConfig": "Bot Main Config",
"botConfigDesc": "Configure bot core settings",
"modelProvider": "Model Provider Config",
"modelProviderDesc": "Configure model providers",
"model": "Model Config",
"modelDesc": "Configure model parameters",
"emoji": "Emoji Management",
"emojiDesc": "Manage bot emoji",
"expression": "Expression Management",
"expressionDesc": "Manage bot expressions",
"person": "Person Info",
"personDesc": "Manage person info",
"jargon": "Slang Management",
"jargonDesc": "Manage bot learned slang and jargon",
"statistics": "Statistics",
"statisticsDesc": "View usage statistics",
"plugins": "Plugin Market",
"pluginsDesc": "Browse and install plugins",
"logs": "Log Viewer",
"logsDesc": "View system logs",
"settings": "Settings",
"settingsDesc": "Configure system settings"
}
},
"httpWarning": {
"title": "Security Warning:",
"message": "You are accessing MaiBot WebUI via HTTP",
"description": "If this is a public server, your data (including Token, chat history, etc.) may be intercepted in transit. It is strongly recommended to use HTTPS or access only from a local network.",
"dismiss": "Dismiss warning"
}
}

View File

@@ -0,0 +1,477 @@
{
"language": { "zh": "中文", "en": "English", "ja": "日本語", "ko": "한국어" },
"header": {
"collapseSidebar": "サイドバーを折りたたむ",
"expandSidebar": "サイドバーを展開する",
"toggleConnection": "バックエンド接続を切り替える",
"viewAnnualSummary": "年間サマリーを表示",
"annualSummary": "2025 年間サマリー",
"searchPlaceholder": "検索...",
"viewDocs": "MaiBot ドキュメントを表示",
"docs": "MaiBot ドキュメント",
"switchToLight": "ライトモードに切り替える",
"switchToDark": "ダークモードに切り替える",
"logout": "ログアウト",
"logoutLabel": "ログアウト",
"notConnected": "未接続"
},
"sidebar": {
"groups": {
"overview": "概要",
"botConfig": "ボット設定",
"botResources": "ボットリソース",
"extensionsMonitor": "拡張機能 & 監視",
"system": "システム"
},
"menu": {
"home": "ホーム",
"botMainConfig": "ボットメイン設定",
"aiModelProvider": "AIモデルプロバイダー",
"modelManagement": "モデル管理",
"adapterConfig": "アダプター設定",
"emojiManagement": "絵文字管理",
"expressionManagement": "表現管理",
"slangManagement": "スラング管理",
"personInfo": "人物情報",
"knowledgeGraph": "知識グラフ",
"knowledgeBase": "ナレッジベース",
"pluginMarket": "プラグインマーケット",
"configTemplate": "設定テンプレート",
"pluginConfig": "プラグイン設定",
"logViewer": "ログビューア",
"plannerMonitor": "プランナー & リプライヤー監視",
"localChat": "ローカルチャット",
"settings": "設定"
}
},
"layout": {
"verifyingLogin": "ログイン状態を確認中...",
"logoTitle": "MaiBot WebUI",
"logoTitleShort": "M"
},
"settings": {
"title": "設定",
"description": "アプリの設定を管理する",
"tabs": {
"appearance": "外観",
"security": "セキュリティ",
"other": "その他",
"about": "について"
},
"appearance": {
"themeMode": "テーマモード",
"themeModeDesc": "ライト / ダーク / システムに従う",
"light": "ライト",
"dark": "ダーク",
"system": "システム",
"accentColor": "アクセントカラー",
"resetDefault": "デフォルトにリセット",
"colorPreview": "カラープレビュー",
"styleTweaks": "スタイル調整",
"typography": "タイポグラフィ",
"visualEffects": "視覚効果",
"layout": "レイアウト",
"animation": "アニメーション",
"background": "背景",
"customCss": "カスタム CSS",
"animationEffect": "アニメーション効果",
"importExportTheme": "テーマのインポート / エクスポート",
"importTheme": "テーマをインポート",
"exportTheme": "テーマをエクスポート",
"importSuccess": "インポート成功",
"importFailed": "インポート失敗",
"resetSuccess": "リセット成功",
"fontFamily": "フォントファミリー",
"fontSize": "フォントサイズ",
"borderRadius": "ボーダー半径",
"contentWidth": "コンテンツ幅",
"sidebarWidth": "サイドバー幅",
"animationSpeed": "アニメーション速度",
"backgroundImage": "背景画像",
"backgroundBlur": "背景ぼかし",
"backgroundOpacity": "背景透明度",
"lightDesc": "常にライトテーマを使用",
"darkDesc": "常にダークテーマを使用",
"systemDesc": "システム設定に従って自動切り替え",
"accentPrimary": "メインカラー",
"accentHint": "カラーリングをクリックするか、HEX 値を入力してください",
"resetTheme": "デフォルトにリセット",
"confirmResetTheme": "テーマのリセットを確認",
"confirmResetThemeDesc": "これにより、色、フォント、レイアウト、カスタム CSS を含むすべてのテーマ設定がデフォルトにリセットされます。この操作は元に戻せません。よろしいですか?",
"confirmResetAction": "リセットを確認",
"cssWarningTitle": "以下の内容はセキュリティフィルターされました:",
"cssPlaceholder": "/* カスタム CSS をここに入力 */\n\n/* 例: */\n/* .sidebar { background: #1a1a2e; } */",
"cssDescription": "カスタム CSS を記述して、インターフェースをさらにカスタマイズしてください。危険な CSS@import、url() など)は自動的にフィルターされます。",
"clearCss": "クリア",
"exportDesc": "テーマを JSON ファイルとしてエクスポートして共有またはバックアップ。インポート時にすべての設定が自動適用されます。",
"importSuccessDesc": "テーマ設定がインポートされました。ページが自動的にリロードされます",
"resetSuccessDesc": "テーマがデフォルトにリセットされました",
"enableAnimations": "アニメーションを有効にする",
"enableAnimationsDesc": "無効にすると、すべてのトランジションアニメーションとエフェクトが無効化されパフォーマンスが向上します",
"loginWavesBackground": "ログインページのウェーブ背景",
"loginWavesBackgroundDesc": "無効にするとログインページが単色背景になります。低スペックのデバイスに適しています",
"inheritParentBg": "上位背景を継承",
"inheritParentBgDesc": "有効にすると上位レイヤーの背景設定を使用します",
"fontFamilyLabel": "フォントファミリー",
"fontFamilyPlaceholder": "フォントファミリーを選択",
"fontFamilySystem": "システムデフォルト (System)",
"fontFamilySans": "サンセリフ (Sans-serif)",
"fontFamilySerif": "セリフ (Serif)",
"fontFamilyMono": "等幅 (Monospace)",
"baseFontSize": "基準フォントサイズ (Base Size)",
"lineHeight": "行高 (Line Height)",
"lineHeightPlaceholder": "行高を選択",
"lineHeightCompact": "コンパクト (1.2)",
"lineHeightNormal": "標準 (1.5)",
"lineHeightLoose": "ルーズ (1.75)",
"borderRadiusLabel": "圆角の大きさ (Radius)",
"shadowLabel": "シャドウの強度 (Shadow)",
"shadowPlaceholder": "シャドウの強度を選択",
"shadowNone": "なし (None)",
"shadowSm": "軽微 (Small)",
"shadowMd": "中程度 (Medium)",
"shadowLg": "強い (Large)",
"shadowXl": "極強 (Extra Large)",
"blurLabel": "ボカシ効果 (Blur)",
"sidebarWidthLabel": "サイドバー幅 (Sidebar Width)",
"maxContentWidth": "コンテンツ最大幅 (Max Width)",
"spacingUnit": "基準間隔 (Spacing Unit)",
"animationSpeedLabel": "アニメーション速度 (Speed)",
"animationSpeedPlaceholder": "アニメーション速度を選択",
"animationFast": "高速 (100ms)",
"animationNormal": "標準 (300ms)",
"animationSlow": "低速 (500ms)",
"animationOff": "オフ (0ms)",
"bgPage": "ページ",
"bgSidebar": "サイドバー",
"typographyGroup": "タイポグラフィ (Typography)",
"visualGroup": "視覚効果 (Visual)",
"layoutGroup": "レイアウト (Layout)",
"animationGroup": "アニメーション (Animation)",
"backgroundGroup": "背景設定 (Backgrounds)"
},
"security": {
"currentToken": "現在のアクセストークン",
"yourToken": "あなたのアクセストークン",
"regenerate": "再生成",
"customToken": "カスタムアクセストークン",
"securityTip": "セキュリティのヒント",
"cannotCopy": "コピーできません",
"copySuccess": "コピーしました",
"copyFailed": "コピー失敗",
"updateSuccess": "更新しました",
"updateFailed": "更新失敗",
"generateSuccess": "生成しました",
"generateFailed": "生成失敗",
"newToken": "新しいアクセストークン",
"confirmRegenerate": "トークンの再生成を確認",
"confirmRegenerateDesc": "再生成後、古いトークンは無効になります。再ログインが必要です。",
"cancel": "キャンセル",
"confirm": "確認",
"cannotCopyDesc": "Token はセキュアな Cookie に保存されています。新しい Token を取得するには再生成してください。",
"copySuccessDesc": "Token をクリップボードにコピーしました",
"copyFailedDesc": "Token を手動でコピーしてください",
"inputError": "入力エラー",
"inputErrorDesc": "新しい Token を入力してください",
"formatError": "フォーマットエラー",
"formatErrorDesc": "Token が要件を満たしていません: {{failedRules}}",
"updateSuccessDesc": "Access Token を更新しました。ログインページにリダイレクトします。",
"updateFailedDesc": "Token を更新できません",
"updateFailedConn": "サーバーへの接続に失敗しました",
"generateSuccessDesc": "新しい Access Token を生成しました。すぐに保存してください。",
"generateFailedDesc": "新しい Token を生成できません",
"generateFailedConn": "サーバーへの接続に失敗しました",
"cannotView": "表示できません",
"cannotViewDesc": "Token はセキュアな Cookie に保存されています。新しい Token が必要な場合は\"再生成\"をクリックしてください。",
"hide": "非表示",
"show": "表示",
"copyTip": "クリップボードにコピー",
"regenerateShort": "生成",
"confirmRegenerateFullDesc": "新しい 64 桁のセキュアトークンを生成し、現在の Token を即座に無効にします。新しい Token で再ログインが必要です。この操作は元に戻せません。続けますか?",
"confirmGenerate": "生成を確認",
"tokenStorePlaceholder": "Token はセキュアな Cookie に保存されています",
"safekeepTip": "Access Token を安全に保管し、他人に漏洩しないでください。",
"newTokenLabel": "新しいアクセストークン",
"customTokenPlaceholder": "カスタム Token を入力",
"tokenReqTitle": "Token セキュリティ要件:",
"tokenValid": "Token のフォーマットは正しく使用できます",
"updateBtn": "カスタム Token を更新",
"updating": "更新中...",
"dialogTitle": "新しい Access Token",
"dialogDesc": "これが新しい Token です。すぐに保存してください。このウィンドウを閉じるとログインページにリダイレクトされます。",
"dialogTokenLabel": "新しい Token (64 桁セキュアトークン)",
"important": "重要なお知らせ",
"tip1": "この Token は一度しか表示されません。閉じた後は表示できません",
"tip2": "すぐにコピーして安全な場所に保存してください",
"tip3": "閉じると自動的にログインページにリダイレクトされます",
"tip4": "新しい Token で再ログインしてください",
"copied": "コピー済み",
"copyToken": "Token をコピー",
"savedClose": "保存しました。閉じる",
"securityTip1": "再生成するとシステムがランダムに生成した 64 桁のセキュアトークンが作成されます",
"securityTip2": "カスタム Token はすべてのセキュリティ要件を満たす必要があります",
"securityTip3": "Token を更新すると、古い Token は即座に無効になります",
"securityTip4": "安全な環境で Token を表示してコピーしてください",
"securityTip5": "Token の漏洩が疏われる場合は、すぐに再生成または更新してください",
"securityTip6": "最高のセキュリティのためにシステム生成の Token を推奨します"
},
"other": {
"performance": "パフォーマンス & ストレージ",
"localStorage": "ローカルストレージ使用量",
"logCache": "ログキャッシュサイズ",
"importExport": "設定のインポート / エクスポート",
"configWizard": "設定ウィザード",
"devTools": "開発者ツール",
"clearStorage": "ローカルストレージを削除",
"clearStorageDesc": "すべてのローカルストレージデータを削除します",
"clearStorageConfirm": "削除を確認",
"clearLogCache": "ログキャッシュを削除",
"clearLogCacheDesc": "すべてのキャッシュされたログデータを削除します",
"clearLogCacheConfirm": "削除を確認",
"importSettings": "設定をインポート",
"exportSettings": "設定をエクスポート",
"resetAllSettings": "すべての設定をリセット",
"resetAllSettingsDesc": "すべての設定をデフォルトに戻します",
"resetAllSettingsConfirm": "リセットを確認",
"clearStorageSuccess": "ローカルストレージを削除しました",
"clearStorageFailed": "削除失敗",
"clearLogSuccess": "ログキャッシュを削除しました",
"clearLogFailed": "削除失敗",
"importSuccess": "インポート成功",
"importFailed": "インポート失敗",
"exportSuccess": "エクスポート成功",
"exportFailed": "エクスポート失敗",
"resetSuccess": "リセット成功",
"resetFailed": "リセット失敗",
"storageItems": "{{count}} 件のアイテム",
"logCacheSizeDesc": "ログビューアがキャッシュする最大ログ数を制御します。大きい値はより多くのメモリを使用します。",
"logCacheSizeUnit": "件",
"dataSyncIntervalLabel": "ホームデータ更新間隔",
"dataSyncIntervalUnit": "秒",
"dataSyncIntervalDesc": "ホーム画面の統計データの自動更新間隔を制御します",
"wsReconnectLabel": "WebSocket 再接続間隔",
"wsReconnectUnit": "秒",
"wsReconnectDesc": "ログ WebSocket 切断後の再接続基本間隔",
"wsMaxReconnectLabel": "WebSocket 最大再接続回数",
"wsMaxReconnectUnit": "回",
"wsMaxReconnectDesc": "接続失敗後の最大再接続試行回数",
"clearLogCacheFn": "ログキャッシュをクリア",
"clearLocalCache": "ローカルキャッシュをクリア",
"confirmClearCache": "ローカルキャッシュのクリアを確認",
"confirmClearCacheDesc": "ローカルにキャッシュされたすべての設定とデータ(ログイン資格情報を除く)をクリアします。一部の設定を再構成する必要があるかもしれません。続けますか?",
"confirmClear": "クリアを確認",
"importExportDesc": "現在のインターフェース設定をエクスポートしてバックアップしたり、以前のファイルから復元したりできます。",
"exporting": "エクスポート中...",
"importing": "インポート中...",
"resetAllSettingsBtn": "すべての設定をデフォルトにリセット",
"confirmResetAll": "すべての設定のリセットを確認",
"confirmResetAllDesc": "テーマ、色、アニメーションなどのインターフェース設定をデフォルトに戻します。ログイン状態には影響しません。続けますか?",
"configWizardDesc": "初回設定ウィザードを再実行してシステムの基本設定を再構成できます。",
"rerunSetup": "初回設定を再実行",
"confirmRerunSetup": "再構成を確認",
"confirmRerunSetupDesc": "初回設定ウィザードに戻ります。システムの基本設定を再設定できます。続けますか?",
"devToolsDesc": "以下の機能は開発・デバッグ目的のみです。クラッシュや異常動作を引き起こす可能性があります。",
"triggerError": "テストエラーを発生させる",
"confirmTriggerError": "エラーの発生を確認",
"confirmTriggerErrorDesc": "React エラーを手動で発生させ、エラーボーダーコンポーネントをテストします。ページを更新するかホームに戻ることで復元できます。",
"confirmTrigger": "発生を確認",
"logCleared": "ログをクリアしました",
"logClearedDesc": "ログキャッシュをクリアしました",
"cacheCleared": "キャッシュをクリアしました",
"cacheClearedDesc": "{{count}} 件のキャッシュデータをクリアしました",
"exportSuccessDesc": "設定を JSON ファイルとしてエクスポートしました",
"exportFailedDesc": "設定のエクスポートに失敗しました",
"importSuccessDesc": "{{imported}} 件の設定をインポートしました",
"importSkippedSuffix": "、{{skipped}} 件をスキップ",
"importRefreshHint": "お知らせ",
"importRefreshHintDesc": "一部の設定はページを更新するまで完全に有効になりません",
"importNoDataDesc": "インポートする有効な設定がありません",
"importInvalidDesc": "ファイルフォーマットが無効です",
"resetDone": "リセット完了",
"resetDoneDesc": "すべての設定がデフォルトに戻りました。変更を適用するにはページを更新してください。"
},
"about": {
"openSource": "オープンソース",
"aboutApp": "MaiBot Dashboard について",
"version": "バージョン:",
"author": "作者",
"techStack": "技術スタック",
"frontendFramework": "フロントエンドフレームワーク",
"uiComponents": "UI コンポーネント",
"backend": "バックエンド",
"buildTool": "ビルドツール",
"openSourceThanks": "使用オープンソースライブラリ",
"openSourceLicense": "オープンソースライセンス",
"openSourceDesc": "このプロジェクトは GitHub で公開されています。Star ⭐ でサポートしてください!",
"visitGitHub": "GitHub へ進む",
"appDesc": "MaiBot のモダンな Web 管理インターフェース",
"maimaiCore": "MaiBot コア",
"uiFrameworkGroup": "UI フレームワーク & コンポーネント",
"routingStateGroup": "ルーティング & 状態管理",
"formGroup": "フォーム処理",
"utilsGroup": "ユーティリティライブラリ",
"animationGroup": "アニメーション",
"backendGroup": "バックエンドフレームワーク",
"devToolsGroup": "開発ツール",
"openSourceThanksDesc": "このプロジェクトは以下の優れたオープンソースライブラリを使用しています。貢献に感謝します:",
"licenseDesc": "このプロジェクトは GNU General Public License v3.0 でライセンスされています。同じオープンソースライセンスを保持する限り、自由に使用・修改・配布できます。",
"licenseDeps": "このプロジェクトのすべての依存オープンソースライブラリはそれぞれのライセンスMIT、Apache-2.0、BSD など)に従っています。すべてのオープンソース貢献者に感謝します。",
"lib": {
"react": "UIを構築するためのライブラリ",
"shadcn": "エレガントな React コンポーネントライブラリ",
"radix": "スタイルなしのアクセシブルなコンポーネント",
"tailwind": "ユーティリティファーストの CSS フレームワーク",
"lucide": "美しいアイコンライブラリ",
"tanstackRouter": "型安全なルーティングライブラリ",
"zustand": "軽量な状態管理ライブラリ",
"reactHookForm": "高パフォーマンスなフォームライブラリ",
"zod": "TypeScript ファーストのスキーマ検証",
"clsx": "条件付き className ビルダー",
"tailwindMerge": "Tailwind クラス名マージツール",
"cva": "コンポーネントバリアント管理",
"dateFns": "モダンな日付ユーティリティライブラリ",
"framerMotion": "React アニメーションライブラリ",
"vaul": "ドロワーコンポーネントアニメーション",
"fastapi": "モダンな Python Web フレームワーク",
"uvicorn": "ASGI サーバー",
"pydantic": "データ検証ライブラリ",
"pythonMultipart": "ファイルアップロードサポート",
"typescript": "JavaScript のスーパーセット",
"vite": "次世代フロントエンドビルドツール",
"eslint": "JavaScript コードリンター",
"postcss": "CSS 変換ツール"
}
}
},
"auth": {
"title": "ログイン",
"description": "続行するにはアクセストークンを入力してください",
"tokenLabel": "アクセストークン",
"tokenPlaceholder": "アクセストークンを入力",
"loginButton": "ログイン",
"loggingIn": "ログイン中...",
"loginFailed": "ログイン失敗",
"loginSuccess": "ログイン成功",
"checkingAuth": "ログイン状態を確認中...",
"welcome": "MaiBot へようこそ",
"accessDesc": "システムにアクセスするためにアクセストークンを入力してください",
"tokenRequired": "アクセストークンを入力してください",
"verifyingLabel": "验証中...",
"verifyEnter": "验証して入る",
"helpLink": "Token がありません。どこで取得できますか?",
"helpTitle": "Access Token の取得方法",
"helpDesc": "Access Token は MaiBot WebUI にアクセスする唯一の証明情報です。次の方法で取得してください",
"method1Title": "方法1:起動ログを確認",
"method1Desc": "MaiBot 起動時にコンソールに WebUI Access Token が表示されます。",
"method1Example1": "🔑 WebUI Access Token: abc123...",
"method1Example2": "💡 この Token で WebUI にログインしてください",
"method2Title": "方法2:設定ファイルを確認",
"method2Desc": "Token はプロジェクトルートの設定ファイルに保存されています:",
"method2FileHint": "このファイルを開いて access_token フィールドの値をコピーしてください",
"securityTipTitle": "セキュリティノート",
"securityTip1": "Token を安全に保管し、他人に漏洱しないでください",
"securityTip2": "Token をリセットするには、ログイン後にシステム設定へ進んでください",
"slowLink": "インターフェースが重いです。どうすればいいですか?",
"disableAnimTitle": "バックグラウンドアニメーションを無効にする",
"disableAnimDesc": "バックグラウンドアニメーションは低スペックのデバイスで遅延を引き起こす可能性があります。無効にすると活百度が大幅に向上します。",
"disableAnimDetail": "無効にすると背景が単色になりますが、機能には影響しません。システム設定からいつでも再有効化できます。",
"disableAnimBtn": "アニメーションを無効にする",
"verifyFailed": "Token の検証に失敗しました。确認して再試行してください。",
"connFailed": "サーバーへの接続に失敗しました。ネットワーク接続を確認してください。",
"switchToLight": "ライトモードに切り替える",
"switchToDark": "ダークモードに切り替える"
},
"common": {
"loading": "読み込み中...",
"error": "エラー",
"retry": "再試行",
"save": "保存",
"cancel": "キャンセル",
"confirm": "確認",
"delete": "削除",
"edit": "編集",
"close": "閉じる",
"search": "検索",
"noData": "データなし",
"success": "成功",
"failed": "失敗"
},
"restart": {
"preparing": "再起動を準備中",
"preparingDesc": "再起動リクエストを送信中...",
"preparingTip": "🔄 MaiBot の再起動を準備中...",
"restarting": "MaiBot を再起動中",
"restartingDesc": "しばらお待ちください、MaiBot が再起動中です...",
"restartingTip": "🔄 設定を保存しました、メインプロセスを再起動中...",
"checking": "サービス状態を確認中",
"checkingDesc": "サービスの回復を待機中... ({{current}}/{{max}})",
"checkingTip": "⏳ サービスの回復を待機中、ページを閉じないでください...",
"success": "再起動成功",
"successDesc": "ログインページにリダイレクト中...",
"successTip": "✅ 設定が適用されました、サービスは正常に動作しています",
"failed": "再起動タイムアウト",
"failedDesc": "サービスが予定時間内に回復しませんでした",
"failedTip": "⚠️ 長時間応答がない場合は、手動で再起動してください",
"refreshPage": "ページを更新",
"retryCheck": "再試行",
"elapsed": "経過時間:"
},
"errorBoundary": {
"title": "問題が発生しました",
"description": "アプリケーションが予期しないエラーを検出しました。ページを更新するかホームに戻ることができます。",
"refreshPage": "ページを更新",
"goHome": "ホームに戻る",
"footer": "問題が解決しない場合は、エラー情報をコピーして開発者に報告してください",
"copiedToClipboard": "クリップボードにコピーしました",
"copyError": "エラー情報をコピー"
},
"search": {
"placeholder": "ページを検索...",
"title": "検索",
"noResults": "一致するページが見つかりません",
"startSearch": "キーワードを入力して検索を開始",
"navigate": "ナビゲート",
"select": "選択",
"close": "閉じる",
"categories": {
"overview": "概要",
"config": "設定",
"resources": "リソース",
"monitor": "監視",
"extensions": "拡張機能",
"system": "システム"
},
"items": {
"home": "ホーム",
"homeDesc": "ダッシュボード概要を表示",
"botConfig": "ボットメイン設定",
"botConfigDesc": "ボットのコア設定を構成",
"modelProvider": "モデルプロバイダー設定",
"modelProviderDesc": "モデルプロバイダーを設定",
"model": "モデル設定",
"modelDesc": "モデルパラメーターを設定",
"emoji": "絵文字管理",
"emojiDesc": "ボットの絵文字を管理",
"expression": "表現管理",
"expressionDesc": "ボットの表現を管理",
"person": "人物情報",
"personDesc": "人物情報を管理",
"jargon": "スラング管理",
"jargonDesc": "ボットが学習したスラングを管理",
"statistics": "統計情報",
"statisticsDesc": "使用統計を表示",
"plugins": "プラグインマーケット",
"pluginsDesc": "プラグインを閉覧してインストール",
"logs": "ログビューア",
"logsDesc": "システムログを表示",
"settings": "設定",
"settingsDesc": "システム設定を構成"
}
},
"httpWarning": {
"title": "セキュリティ警告:",
"message": "HTTP で MaiBot WebUI にアクセスしています",
"description": "これが公開サーバーの場合、データToken、チャット履歴などが転送中に傍受される可能性があります。HTTPS を使用するか、ローカルネットワークからのみアクセスすることを強くお勧めします。",
"dismiss": "警告を閉じる"
}
}

View File

@@ -0,0 +1,477 @@
{
"language": { "zh": "中文", "en": "English", "ja": "日本語", "ko": "한국어" },
"header": {
"collapseSidebar": "사이드바 접기",
"expandSidebar": "사이드바 펼치기",
"toggleConnection": "백엔드 연결 전환",
"viewAnnualSummary": "연간 요약 보기",
"annualSummary": "2025 연간 요약",
"searchPlaceholder": "검색...",
"viewDocs": "MaiBot 문서 보기",
"docs": "MaiBot 문서",
"switchToLight": "라이트 모드로 전환",
"switchToDark": "다크 모드로 전환",
"logout": "로그아웃",
"logoutLabel": "로그아웃",
"notConnected": "연결 안됨"
},
"sidebar": {
"groups": {
"overview": "개요",
"botConfig": "봇 설정",
"botResources": "봇 리소스",
"extensionsMonitor": "확장 기능 & 모니터",
"system": "시스템"
},
"menu": {
"home": "홈",
"botMainConfig": "봇 메인 설정",
"aiModelProvider": "AI 모델 공급자",
"modelManagement": "모델 관리",
"adapterConfig": "어댑터 설정",
"emojiManagement": "이모티콘 관리",
"expressionManagement": "표현 관리",
"slangManagement": "슬랭 관리",
"personInfo": "인물 정보",
"knowledgeGraph": "지식 그래프",
"knowledgeBase": "지식 베이스",
"pluginMarket": "플러그인 마켓",
"configTemplate": "설정 템플릿",
"pluginConfig": "플러그인 설정",
"logViewer": "로그 뷰어",
"plannerMonitor": "플래너 & 리플라이어 모니터",
"localChat": "로컬 채팅",
"settings": "설정"
}
},
"layout": {
"verifyingLogin": "로그인 상태 확인 중...",
"logoTitle": "MaiBot WebUI",
"logoTitleShort": "M"
},
"settings": {
"title": "설정",
"description": "앱 환경 설정 관리",
"tabs": {
"appearance": "외관",
"security": "보안",
"other": "기타",
"about": "정보"
},
"appearance": {
"themeMode": "테마 모드",
"themeModeDesc": "라이트 / 다크 / 시스템 따라가기",
"light": "라이트",
"dark": "다크",
"system": "시스템",
"accentColor": "강조 색상",
"resetDefault": "기본값으로 재설정",
"colorPreview": "색상 미리보기",
"styleTweaks": "스타일 조정",
"typography": "타이포그래피",
"visualEffects": "시각 효과",
"layout": "레이아웃",
"animation": "애니메이션",
"background": "배경",
"customCss": "사용자 정의 CSS",
"animationEffect": "애니메이션 효과",
"importExportTheme": "테마 가져오기 / 내보내기",
"importTheme": "테마 가져오기",
"exportTheme": "테마 내보내기",
"importSuccess": "가져오기 성공",
"importFailed": "가져오기 실패",
"resetSuccess": "재설정 성공",
"fontFamily": "글꼴",
"fontSize": "글자 크기",
"borderRadius": "테두리 반경",
"contentWidth": "콘텐츠 너비",
"sidebarWidth": "사이드바 너비",
"animationSpeed": "애니메이션 속도",
"backgroundImage": "배경 이미지",
"backgroundBlur": "배경 흐림",
"backgroundOpacity": "배경 투명도",
"lightDesc": "항상 라이트 테마 사용",
"darkDesc": "항상 다크 테마 사용",
"systemDesc": "시스템 설정에 따라 자동 전환",
"accentPrimary": "링 컴러",
"accentHint": "색상 퐹을 클릭하거나 HEX 값을 입력하세요",
"resetTheme": "기본값으로 재설정",
"confirmResetTheme": "테마 재설정 확인",
"confirmResetThemeDesc": "이렇게 하면 색상, 글꼴, 레이아웃 및 사용자 정의 CSS를 포함한 모든 테마 설정이 기본값으로 재설정됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?",
"confirmResetAction": "재설정 확인",
"cssWarningTitle": "다음 내용이 안전 필터를 거쳣습니다:",
"cssPlaceholder": "/* 사용자 정의 CSS를 여기에 입력 */\n\n/* 예시: */\n/* .sidebar { background: #1a1a2e; } */",
"cssDescription": "인터페이스를 더욱 개인화하도록 사용자 정의 CSS를 작성하세요. 위험한 CSS(@import, url() 등)는 자동으로 필터됩니다.",
"clearCss": "지우기",
"exportDesc": "주제를 JSON 파일로 내보내 공유하거나 백업합니다. 가져올 때 모든 설정이 자동으로 적용됩니다.",
"importSuccessDesc": "테마 설정을 가져왔습니다. 페이지가 자동으로 새로고침됩니다",
"resetSuccessDesc": "테마가 기본값으로 재설정되었습니다",
"enableAnimations": "애니메이션 활성화",
"enableAnimationsDesc": "비활성화하면 모든 전환 애니메이션과 효과가 긺히고 성능이 향상됩니다",
"loginWavesBackground": "로그인 페이지 파도 배경",
"loginWavesBackgroundDesc": "비활성화하면 로그인 페이지가 단색 배경이 됩니다. 저사양 디바이스에 적합합니다",
"inheritParentBg": "상위 배경 상속",
"inheritParentBgDesc": "활성화하면 상위 레이어의 배경 설정을 사용합니다",
"fontFamilyLabel": "글꼴 패밀리",
"fontFamilyPlaceholder": "글꼴 패밀리 선택",
"fontFamilySystem": "시스템 기본 (System)",
"fontFamilySans": "돋움체 (Sans-serif)",
"fontFamilySerif": "세리프 (Serif)",
"fontFamilyMono": "등폭 (Monospace)",
"baseFontSize": "기본 글자 크기 (Base Size)",
"lineHeight": "줄 높이 (Line Height)",
"lineHeightPlaceholder": "줄 높이 선택",
"lineHeightCompact": "콤팬트 (1.2)",
"lineHeightNormal": "보통 (1.5)",
"lineHeightLoose": "느슨 (1.75)",
"borderRadiusLabel": "테두리 반경 (Radius)",
"shadowLabel": "그림자 강도 (Shadow)",
"shadowPlaceholder": "그림자 강도 선택",
"shadowNone": "없음 (None)",
"shadowSm": "약함 (Small)",
"shadowMd": "중간 (Medium)",
"shadowLg": "강함 (Large)",
"shadowXl": "매우 강함 (Extra Large)",
"blurLabel": "흘림 효과 (Blur)",
"sidebarWidthLabel": "사이드바드 너비 (Sidebar Width)",
"maxContentWidth": "콘텐츠 최대 너비 (Max Width)",
"spacingUnit": "기본 간격 (Spacing Unit)",
"animationSpeedLabel": "애니메이션 속도 (Speed)",
"animationSpeedPlaceholder": "애니메이션 속도 선택",
"animationFast": "빠름 (100ms)",
"animationNormal": "보통 (300ms)",
"animationSlow": "느림 (500ms)",
"animationOff": "끄기 (0ms)",
"bgPage": "페이지",
"bgSidebar": "사이드바드",
"typographyGroup": "타이포그래피 (Typography)",
"visualGroup": "시각 효과 (Visual)",
"layoutGroup": "레이아웃 (Layout)",
"animationGroup": "애니메이션 (Animation)",
"backgroundGroup": "배경 설정 (Backgrounds)"
},
"security": {
"currentToken": "현재 액세스 토큰",
"yourToken": "액세스 토큰",
"regenerate": "재생성",
"customToken": "사용자 정의 액세스 토큰",
"securityTip": "보안 팁",
"cannotCopy": "복사할 수 없습니다",
"copySuccess": "복사됨",
"copyFailed": "복사 실패",
"updateSuccess": "업데이트됨",
"updateFailed": "업데이트 실패",
"generateSuccess": "생성됨",
"generateFailed": "생성 실패",
"newToken": "새 액세스 토큰",
"confirmRegenerate": "토큰 재생성 확인",
"confirmRegenerateDesc": "재생성 후 이전 토큰은 무효화됩니다. 다시 로그인해야 합니다.",
"cancel": "취소",
"confirm": "확인",
"cannotCopyDesc": "토큰이 보안 쿠키에 저장되어 있습니다. 새 토큰을 얻으려면 재생성하세요.",
"copySuccessDesc": "토큰이 클립보드에 복사되었습니다",
"copyFailedDesc": "토큰을 수동으로 복사하세요",
"inputError": "입력 오류",
"inputErrorDesc": "새 토큰을 입력하세요",
"formatError": "형식 오류",
"formatErrorDesc": "토큰이 요구 사항을 충족하지 않습니다: {{failedRules}}",
"updateSuccessDesc": "액세스 토큰이 업데이트되었습니다. 로그인 페이지로 이동합니다.",
"updateFailedDesc": "토큰을 업데이트할 수 없습니다",
"updateFailedConn": "서버 연결에 실패했습니다",
"generateSuccessDesc": "새 액세스 토큰이 생성되었습니다. 즉시 저장하세요.",
"generateFailedDesc": "새 토큰을 생성할 수 없습니다",
"generateFailedConn": "서버 연결에 실패했습니다",
"cannotView": "볼 수 없습니다",
"cannotViewDesc": "토큰이 보안 쿠키에 저장되어 있습니다. 새 토큰이 필요하면 \"재생성\"을 클릭하세요.",
"hide": "숨기기",
"show": "표시",
"copyTip": "클립보드에 복사",
"regenerateShort": "생성",
"confirmRegenerateFullDesc": "새로운 64자 보안 토큰을 생성하고 현재 토큰을 즉시 무효화합니다. 새 토큰으로 다시 로그인해야 합니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?",
"confirmGenerate": "생성 확인",
"tokenStorePlaceholder": "토큰이 보안 쿠키에 저장되어 있습니다",
"safekeepTip": "액세스 토큰을 안전하게 보관하고 다른 사람과 공유하지 마세요.",
"newTokenLabel": "새 액세스 토큰",
"customTokenPlaceholder": "사용자 정의 토큰 입력",
"tokenReqTitle": "토큰 보안 요구 사항:",
"tokenValid": "토큰 형식이 올바르며 사용 가능합니다",
"updateBtn": "사용자 정의 토큰 업데이트",
"updating": "업데이트 중...",
"dialogTitle": "새 액세스 토큰",
"dialogDesc": "새 토큰입니다. 즉시 저장하세요. 창을 닫으면 로그인 페이지로 이동합니다.",
"dialogTokenLabel": "새 토큰 (64자 보안 토큰)",
"important": "중요 공지",
"tip1": "이 토큰은 한 번만 표시됩니다. 닫은 후에는 볼 수 없습니다",
"tip2": "즉시 복사하여 안전한 위치에 저장하세요",
"tip3": "닫으면 자동으로 로그인 페이지로 이동합니다",
"tip4": "새 토큰으로 다시 로그인하세요",
"copied": "복사됨",
"copyToken": "토큰 복사",
"savedClose": "저장했습니다, 닫기",
"securityTip1": "재생성하면 시스템이 랜덤 64자 보안 토큰을 생성합니다",
"securityTip2": "사용자 정의 토큰은 모든 보안 요구 사항을 충족해야 합니다",
"securityTip3": "토큰을 업데이트하면 이전 토큰이 즉시 무효화됩니다",
"securityTip4": "안전한 환경에서 토큰을 확인하고 복사하세요",
"securityTip5": "토큰 유출이 의심되면 즉시 재생성하거나 업데이트하세요",
"securityTip6": "최고의 보안을 위해 시스템 생성 토큰을 권장합니다"
},
"other": {
"performance": "성능 & 저장소",
"localStorage": "로컬 저장소 사용량",
"logCache": "로그 캐시 크기",
"importExport": "설정 가져오기 / 내보내기",
"configWizard": "설정 마법사",
"devTools": "개발자 도구",
"clearStorage": "로컬 저장소 지우기",
"clearStorageDesc": "모든 로컬 저장소 데이터를 지웁니다",
"clearStorageConfirm": "지우기 확인",
"clearLogCache": "로그 캐시 지우기",
"clearLogCacheDesc": "모든 캐시된 로그 데이터를 지웁니다",
"clearLogCacheConfirm": "지우기 확인",
"importSettings": "설정 가져오기",
"exportSettings": "설정 내보내기",
"resetAllSettings": "모든 설정 재설정",
"resetAllSettingsDesc": "모든 설정을 기본값으로 되돌립니다",
"resetAllSettingsConfirm": "재설정 확인",
"clearStorageSuccess": "로컬 저장소를 지웠습니다",
"clearStorageFailed": "지우기 실패",
"clearLogSuccess": "로그 캐시를 지웠습니다",
"clearLogFailed": "지우기 실패",
"importSuccess": "가져오기 성공",
"importFailed": "가져오기 실패",
"exportSuccess": "내보내기 성공",
"exportFailed": "내보내기 실패",
"resetSuccess": "재설정 성공",
"resetFailed": "재설정 실패",
"storageItems": "{{count}}개 저장 항목",
"logCacheSizeDesc": "로그 뷰어가 캐시할 최대 로그 수를 제어합니다. 값이 클수록 메모리를 더 사용합니다.",
"logCacheSizeUnit": "개",
"dataSyncIntervalLabel": "홈 데이터 새로고침 간격",
"dataSyncIntervalUnit": "초",
"dataSyncIntervalDesc": "홈 화면 통계 데이터의 자동 새로고침 간격을 제어합니다",
"wsReconnectLabel": "WebSocket 재연결 간격",
"wsReconnectUnit": "초",
"wsReconnectDesc": "로그 WebSocket 연결 해제 후 재연결 기본 간격",
"wsMaxReconnectLabel": "WebSocket 최대 재연결 횟수",
"wsMaxReconnectUnit": "회",
"wsMaxReconnectDesc": "연결 실패 후 최대 재연결 시도 횟수",
"clearLogCacheFn": "로그 캐시 지우기",
"clearLocalCache": "로컬 캐시 지우기",
"confirmClearCache": "로컬 캐시 지우기 확인",
"confirmClearCacheDesc": "로그인 자격 증명을 제외한 모든 로컬 캐시 설정과 데이터를 지웁니다. 일부 기본 설정을 다시 구성해야 할 수 있습니다. 계속하시겠습니까?",
"confirmClear": "지우기 확인",
"importExportDesc": "현재 인터페이스 설정을 내보내 백업하거나, 이전에 내보낸 파일에서 복원하세요.",
"exporting": "내보내는 중...",
"importing": "가져오는 중...",
"resetAllSettingsBtn": "모든 설정을 기본값으로 재설정",
"confirmResetAll": "모든 설정 재설정 확인",
"confirmResetAllDesc": "테마, 색상, 애니메이션 등 모든 인터페이스 설정을 기본값으로 복원합니다. 로그인 상태에는 영향이 없습니다. 계속하시겠습니까?",
"configWizardDesc": "초기 설정 마법사를 다시 실행하여 시스템 기본 설정을 재구성할 수 있습니다.",
"rerunSetup": "초기 설정 다시 실행",
"confirmRerunSetup": "재구성 확인",
"confirmRerunSetupDesc": "초기 설정 마법사로 돌아갑니다. 시스템 기본 설정을 재구성할 수 있습니다. 계속하시겠습니까?",
"devToolsDesc": "아래 기능은 개발 및 디버깅 전용입니다. 페이지 충돌이나 비정상 동작을 유발할 수 있습니다.",
"triggerError": "테스트 오류 발생",
"confirmTriggerError": "오류 발생 확인",
"confirmTriggerErrorDesc": "React 오류를 수동으로 발생시켜 오류 경계 컴포넌트를 테스트합니다. 페이지를 새로고침하거나 홈으로 돌아가면 복구됩니다.",
"confirmTrigger": "발생 확인",
"logCleared": "로그 지워짐",
"logClearedDesc": "로그 캐시가 지워졌습니다",
"cacheCleared": "캐시 지워짐",
"cacheClearedDesc": "{{count}}개의 캐시 데이터를 지웠습니다",
"exportSuccessDesc": "설정을 JSON 파일로 내보냈습니다",
"exportFailedDesc": "설정을 내보낼 수 없습니다",
"importSuccessDesc": "{{imported}}개 설정을 가져왔습니다",
"importSkippedSuffix": ", {{skipped}}개 건너뜀",
"importRefreshHint": "참고",
"importRefreshHintDesc": "일부 설정은 페이지를 새로고침해야 완전히 적용됩니다",
"importNoDataDesc": "가져올 유효한 설정이 없습니다",
"importInvalidDesc": "유효하지 않은 파일 형식",
"resetDone": "재설정 완료",
"resetDoneDesc": "모든 설정이 기본값으로 복원되었습니다. 변경 사항을 적용하려면 페이지를 새로고침하세요."
},
"about": {
"openSource": "오픈 소스",
"aboutApp": "MaiBot Dashboard 정보",
"version": "버전:",
"author": "작성자",
"techStack": "기술 스택",
"frontendFramework": "프론트엔드 프레임워크",
"uiComponents": "UI 컴포넌트",
"backend": "백엔드",
"buildTool": "빌드 도구",
"openSourceThanks": "오픈 소스 라이브러리",
"openSourceLicense": "오픈 소스 라이선스",
"openSourceDesc": "이 프로젝트는 GitHub에서 오픈 소스입니다. Star ⭐로 응원해 주세요!",
"visitGitHub": "GitHub 방문",
"appDesc": "MaiBot의 현대적인 웹 관리 인터페이스",
"maimaiCore": "MaiBot 코어",
"uiFrameworkGroup": "UI 프레임워크 & 컴포넌트",
"routingStateGroup": "라우팅 & 상태 관리",
"formGroup": "폼 처리",
"utilsGroup": "유틸리티 라이브러리",
"animationGroup": "애니메이션",
"backendGroup": "백엔드 프레임워크",
"devToolsGroup": "개발자 도구",
"openSourceThanksDesc": "이 프로젝트는 다음 훌륭한 오픈 소스 라이브러리를 사용합니다. 기여에 감사드립니다:",
"licenseDesc": "이 프로젝트는 GNU General Public License v3.0으로 라이선스됩니다. 동일한 오픈 소스 라이선스를 유지하는 한 자유롭게 사용, 수정, 배포할 수 있습니다.",
"licenseDeps": "이 프로젝트의 모든 오픈 소스 라이브러리는 각각의 라이선스(MIT, Apache-2.0, BSD 등)를 따릅니다. 모든 오픈 소스 기여자에게 감사드립니다.",
"lib": {
"react": "UI 구축을 위한 라이브러리",
"shadcn": "우아한 React 컴포넌트 라이브러리",
"radix": "스타일 없는 접근 가능한 컴포넌트",
"tailwind": "유틸리티 우선 CSS 프레임워크",
"lucide": "아름다운 아이콘 라이브러리",
"tanstackRouter": "타입 안전한 라우팅 라이브러리",
"zustand": "경량 상태 관리 라이브러리",
"reactHookForm": "고성능 폼 라이브러리",
"zod": "TypeScript 우선 스키마 검증",
"clsx": "조건부 className 빌더",
"tailwindMerge": "Tailwind 클래스 이름 병합 도구",
"cva": "컴포넌트 변형 관리",
"dateFns": "현대적인 날짜 유틸리티 라이브러리",
"framerMotion": "React 애니메이션 라이브러리",
"vaul": "드로어 컴포넌트 애니메이션",
"fastapi": "현대적인 Python 웹 프레임워크",
"uvicorn": "ASGI 서버",
"pydantic": "데이터 검증 라이브러리",
"pythonMultipart": "파일 업로드 지원",
"typescript": "JavaScript의 슈퍼셋",
"vite": "차세대 프론트엔드 빌드 도구",
"eslint": "JavaScript 코드 린터",
"postcss": "CSS 변환 도구"
}
}
},
"auth": {
"title": "로그인",
"description": "계속하려면 액세스 토큰을 입력하세요",
"tokenLabel": "액세스 토큰",
"tokenPlaceholder": "액세스 토큰 입력",
"loginButton": "로그인",
"loggingIn": "로그인 중...",
"loginFailed": "로그인 실패",
"loginSuccess": "로그인 성공",
"checkingAuth": "로그인 상태 확인 중...",
"welcome": "MaiBot에 오신 것을 환영합니다",
"accessDesc": "시스템에 액세스하려면 액세스 토큰을 입력하세요",
"tokenRequired": "액세스 토큰을 입력해 주세요",
"verifyingLabel": "확인 중...",
"verifyEnter": "확인 후 입장",
"helpLink": "Token이 없습니다. 어디서 얻을 수 있나요?",
"helpTitle": "Access Token 얻는 방법",
"helpDesc": "Access Token은 MaiBot WebUI에 액세스하는 유일한 자격 증명입니다. 다음 방법으로 얻으세요",
"method1Title": "방법 1: 시작 로그 확인",
"method1Desc": "MaiBot 시작 시 콘솔에 WebUI Access Token이 표시됩니다.",
"method1Example1": "🔑 WebUI Access Token: abc123...",
"method1Example2": "💡 이 Token으로 WebUI에 로그인하세요",
"method2Title": "방법 2: 설정 파일 확인",
"method2Desc": "Token은 프로젝트 루트의 설정 파일에 저장됩니다:",
"method2FileHint": "이 파일을 열고 access_token 필드의 값을 복사하세요",
"securityTipTitle": "보안 안내",
"securityTip1": "Token을 안전하게 유지하고 타인에게 노출하지 마세요",
"securityTip2": "Token을 재설정하려면 로그인 후 시스템 설정으로 이동하세요",
"slowLink": "인터페이스가 느립니다. 어떻게 하나요?",
"disableAnimTitle": "배경 애니메이션 비활성화",
"disableAnimDesc": "배경 애니메이션은 저사양 디바이스에서 느리게 동작할 수 있습니다. 비활성화하면 화면이 훨씬 부드러워집니다.",
"disableAnimDetail": "비활성화 후 배경은 단색으로 바뀐지지만 모든 기능은 정상 작동합니다. 시스템 설정에서 언제든지 다시 활성화할 수 있습니다.",
"disableAnimBtn": "애니메이션 비활성화",
"verifyFailed": "Token 확인에 실패했습니다. 확인 후 다시 시도해 주세요.",
"connFailed": "서버에 연결하지 못했습니다. 네트워크 연결을 확인해 주세요.",
"switchToLight": "라이트 모드로 전환",
"switchToDark": "다크 모드로 전환"
},
"common": {
"loading": "로딩 중...",
"error": "오류",
"retry": "재시도",
"save": "저장",
"cancel": "취소",
"confirm": "확인",
"delete": "삭제",
"edit": "편집",
"close": "닫기",
"search": "검색",
"noData": "데이터 없음",
"success": "성공",
"failed": "실패"
},
"restart": {
"preparing": "재시작 준비 중",
"preparingDesc": "재시작 요청 전송 중...",
"preparingTip": "🔄 MaiBot 재시작을 준비 중...",
"restarting": "MaiBot 재시작 중",
"restartingDesc": "잠시 기다려주세요, MaiBot이 재시작 중입니다...",
"restartingTip": "🔄 설정을 저장했습니다, 메인 프로세스를 재시작 중...",
"checking": "서비스 상태 확인 중",
"checkingDesc": "서비스 복구 대기 중... ({{current}}/{{max}})",
"checkingTip": "⏳ 서비스 복구를 기다리는 중, 페이지를 닫지 마세요...",
"success": "재시작 성공",
"successDesc": "로그인 페이지로 이동 중...",
"successTip": "✅ 설정이 적용되었습니다, 서비스가 정상적으로 실행 중입니다",
"failed": "재시작 시간 초과",
"failedDesc": "서비스가 예상 시간 내에 복구되지 않았습니다",
"failedTip": "⚠️ 장시간 응답이 없으면 수동으로 재시작해 보세요",
"refreshPage": "페이지 새로고침",
"retryCheck": "재시도",
"elapsed": "경과 시간:"
},
"errorBoundary": {
"title": "문제가 발생했습니다",
"description": "앱이 예기치 않은 오류를 만났습니다. 페이지를 새로고침하거나 홈으로 돌아갈 수 있습니다.",
"refreshPage": "페이지 새로고침",
"goHome": "홈으로 이동",
"footer": "문제가 계속되면 오류 정보를 복사하여 개발자에게 보고해 주세요",
"copiedToClipboard": "클립보드에 복사됨",
"copyError": "오류 정보 복사"
},
"search": {
"placeholder": "페이지 검색...",
"title": "검색",
"noResults": "일치하는 페이지를 찾을 수 없습니다",
"startSearch": "키워드를 입력하여 검색 시작",
"navigate": "탐색",
"select": "선택",
"close": "닫기",
"categories": {
"overview": "개요",
"config": "설정",
"resources": "리소스",
"monitor": "모니터",
"extensions": "확장 기능",
"system": "시스템"
},
"items": {
"home": "홈",
"homeDesc": "대시보드 개요 보기",
"botConfig": "속 메인 설정",
"botConfigDesc": "속 핵심 설정 구성",
"modelProvider": "모델 공급자 설정",
"modelProviderDesc": "모델 공급자 구성",
"model": "모델 설정",
"modelDesc": "모델 매개변수 구성",
"emoji": "이모티콘 관리",
"emojiDesc": "속 이모티콘 관리",
"expression": "표현 관리",
"expressionDesc": "속 표현 관리",
"person": "인물 정보",
"personDesc": "인물 정보 관리",
"jargon": "슬랭 관리",
"jargonDesc": "속이 학습한 슬랭 관리",
"statistics": "통계 정보",
"statisticsDesc": "사용 통계 보기",
"plugins": "플러그인 마켓",
"pluginsDesc": "플러그인 탐색 및 설치",
"logs": "로그 뷰어",
"logsDesc": "시스템 로그 보기",
"settings": "설정",
"settingsDesc": "시스템 설정 구성"
}
},
"httpWarning": {
"title": "보안 경고:",
"message": "HTTP를 통해 MaiBot WebUI에 접속하고 있습니다",
"description": "이것이 공개 서버인 경우, 데이터(Token, 채팅 기록 등)가 전송 중에 가로챔질 수 있습니다. HTTPS를 사용하거나 로컈 네트워크에서만 접속하는 것을 강력히 권장합니다.",
"dismiss": "경고 닫기"
}
}

View File

@@ -0,0 +1,477 @@
{
"language": { "zh": "中文", "en": "English", "ja": "日本語", "ko": "한국어" },
"header": {
"collapseSidebar": "收起侧边栏",
"expandSidebar": "展开侧边栏",
"toggleConnection": "切换后端连接",
"viewAnnualSummary": "查看年度总结",
"annualSummary": "2025 年度总结",
"searchPlaceholder": "搜索...",
"viewDocs": "查看麦麦文档",
"docs": "麦麦文档",
"switchToLight": "切换到浅色模式",
"switchToDark": "切换到深色模式",
"logout": "登出系统",
"logoutLabel": "登出",
"notConnected": "未连接"
},
"sidebar": {
"groups": {
"overview": "概览",
"botConfig": "麦麦配置编辑",
"botResources": "麦麦资源管理",
"extensionsMonitor": "扩展与监控",
"system": "系统"
},
"menu": {
"home": "首页",
"botMainConfig": "麦麦主程序配置",
"aiModelProvider": "AI模型厂商配置",
"modelManagement": "模型管理与分配",
"adapterConfig": "麦麦适配器配置",
"emojiManagement": "表情包管理",
"expressionManagement": "表达方式管理",
"slangManagement": "黑话管理",
"personInfo": "人物信息管理",
"knowledgeGraph": "知识库图谱可视化",
"knowledgeBase": "麦麦知识库管理",
"pluginMarket": "插件市场",
"configTemplate": "配置模板市场",
"pluginConfig": "插件配置",
"logViewer": "日志查看器",
"plannerMonitor": "计划器&回复器监控",
"localChat": "本地聊天室",
"settings": "系统设置"
}
},
"layout": {
"verifyingLogin": "正在验证登录状态...",
"logoTitle": "MaiBot WebUI",
"logoTitleShort": "M"
},
"settings": {
"title": "系统设置",
"description": "管理您的应用偏好设置",
"tabs": {
"appearance": "外观",
"security": "安全",
"other": "其他",
"about": "关于"
},
"appearance": {
"themeMode": "主题模式",
"themeModeDesc": "浅色/深色/跟随系统",
"light": "浅色",
"dark": "深色",
"system": "跟随系统",
"accentColor": "主题色",
"resetDefault": "重置默认",
"colorPreview": "实时色板预览",
"styleTweaks": "界面样式微调",
"typography": "字体排版",
"visualEffects": "视觉效果",
"layout": "布局",
"animation": "动画",
"background": "背景设置",
"customCss": "自定义 CSS",
"animationEffect": "动画效果",
"importExportTheme": "主题导入/导出",
"importTheme": "导入主题",
"exportTheme": "导出主题",
"importSuccess": "导入成功",
"importFailed": "导入失败",
"resetSuccess": "重置成功",
"fontFamily": "字体",
"fontSize": "字号",
"borderRadius": "圆角",
"contentWidth": "内容宽度",
"sidebarWidth": "侧边栏宽度",
"animationSpeed": "动画速度",
"backgroundImage": "背景图片",
"backgroundBlur": "背景模糊",
"backgroundOpacity": "背景透明度",
"lightDesc": "始终使用浅色主题",
"darkDesc": "始终使用深色主题",
"systemDesc": "根据系统设置自动切换",
"accentPrimary": "主色调",
"accentHint": "点击色环选择或输入 HEX 值",
"resetTheme": "重置为默认",
"confirmResetTheme": "确认重置主题",
"confirmResetThemeDesc": "这将重置所有主题设置为默认值,包括颜色、字体、布局和自定义 CSS。此操作不可撤销确定要继续吗",
"confirmResetAction": "确认重置",
"cssWarningTitle": "以下内容已被安全过滤:",
"cssPlaceholder": "/* 在这里输入自定义 CSS */\n\n/* 例如: */\n/* .sidebar { background: #1a1a2e; } */",
"cssDescription": "编写自定义 CSS 来进一步个性化界面。危险的 CSS如 @import、url())将被自动过滤。",
"clearCss": "清除",
"exportDesc": "导出主题为 JSON 文件便于分享或备份,导入时会自动应用所有配置。",
"importSuccessDesc": "主题配置已导入,页面将自动刷新",
"resetSuccessDesc": "主题已重置为默认值",
"enableAnimations": "启用动画效果",
"enableAnimationsDesc": "关闭后将禁用所有过渡动画和特效,提升性能",
"loginWavesBackground": "登录页波浪背景",
"loginWavesBackgroundDesc": "关闭后登录页将使用纯色背景,适合低性能设备",
"inheritParentBg": "继承上级背景",
"inheritParentBgDesc": "开启后将使用上级层级的背景配置",
"fontFamilyLabel": "字体族 (Font Family)",
"fontFamilyPlaceholder": "选择字体族",
"fontFamilySystem": "系统默认 (System)",
"fontFamilySans": "无衬线 (Sans-serif)",
"fontFamilySerif": "衬线 (Serif)",
"fontFamilyMono": "等宽 (Monospace)",
"baseFontSize": "基准字体大小 (Base Size)",
"lineHeight": "行高 (Line Height)",
"lineHeightPlaceholder": "选择行高",
"lineHeightCompact": "紧凑 (1.2)",
"lineHeightNormal": "正常 (1.5)",
"lineHeightLoose": "宽松 (1.75)",
"borderRadiusLabel": "圆角大小 (Radius)",
"shadowLabel": "阴影强度 (Shadow)",
"shadowPlaceholder": "选择阴影强度",
"shadowNone": "无阴影 (None)",
"shadowSm": "轻微 (Small)",
"shadowMd": "中等 (Medium)",
"shadowLg": "强烈 (Large)",
"shadowXl": "极强 (Extra Large)",
"blurLabel": "模糊效果 (Blur)",
"sidebarWidthLabel": "侧边栏宽度 (Sidebar Width)",
"maxContentWidth": "内容区最大宽度 (Max Width)",
"spacingUnit": "基准间距 (Spacing Unit)",
"animationSpeedLabel": "动画速度 (Speed)",
"animationSpeedPlaceholder": "选择动画速度",
"animationFast": "快速 (100ms)",
"animationNormal": "正常 (300ms)",
"animationSlow": "慢速 (500ms)",
"animationOff": "关闭 (0ms)",
"bgPage": "页面",
"bgSidebar": "侧边栏",
"typographyGroup": "字体排版 (Typography)",
"visualGroup": "视觉效果 (Visual)",
"layoutGroup": "布局 (Layout)",
"animationGroup": "动画 (Animation)",
"backgroundGroup": "背景设置 (Backgrounds)"
},
"security": {
"currentToken": "当前 Access Token",
"yourToken": "您的访问令牌",
"regenerate": "重新生成",
"customToken": "自定义 Access Token",
"securityTip": "安全提示",
"cannotCopy": "无法复制",
"copySuccess": "复制成功",
"copyFailed": "复制失败",
"updateSuccess": "更新成功",
"updateFailed": "更新失败",
"generateSuccess": "生成成功",
"generateFailed": "生成失败",
"newToken": "新的 Access Token",
"confirmRegenerate": "确认重新生成 Token",
"confirmRegenerateDesc": "重新生成后,旧 Token 将失效,需重新登录",
"cancel": "取消",
"confirm": "确认",
"cannotCopyDesc": "Token 存储在安全 Cookie 中,请重新生成以获取新 Token",
"copySuccessDesc": "Token 已复制到剪贴板",
"copyFailedDesc": "请手动复制 Token",
"inputError": "输入错误",
"inputErrorDesc": "请输入新的 Token",
"formatError": "格式错误",
"formatErrorDesc": "Token 不符合要求: {{failedRules}}",
"updateSuccessDesc": "Access Token 已更新,即将跳转到登录页",
"updateFailedDesc": "无法更新 Token",
"updateFailedConn": "连接服务器失败",
"generateSuccessDesc": "新的 Access Token 已生成,请及时保存",
"generateFailedDesc": "无法生成新 Token",
"generateFailedConn": "连接服务器失败",
"cannotView": "无法查看",
"cannotViewDesc": "Token 存储在安全 Cookie 中,如需新 Token 请点击\"重新生成\"",
"hide": "隐藏",
"show": "显示",
"copyTip": "复制到剪贴板",
"regenerateShort": "生成",
"confirmRegenerateFullDesc": "这将生成一个新的 64 位安全令牌,并使当前 Token 立即失效。您需要使用新 Token 重新登录系统。此操作不可撤销,确定要继续吗?",
"confirmGenerate": "确认生成",
"tokenStorePlaceholder": "Token 存储在安全 Cookie 中",
"safekeepTip": "请妥善保管您的 Access Token不要泄露给他人",
"newTokenLabel": "新的访问令牌",
"customTokenPlaceholder": "输入自定义 Token",
"tokenReqTitle": "Token 安全要求:",
"tokenValid": "Token 格式正确,可以使用",
"updateBtn": "更新自定义 Token",
"updating": "更新中...",
"dialogTitle": "新的 Access Token",
"dialogDesc": "这是您的新 Token请立即保存。关闭此窗口后将跳转到登录页面。",
"dialogTokenLabel": "您的新 Token (64位安全令牌)",
"important": "重要提示",
"tip1": "此 Token 仅显示一次,关闭后无法再查看",
"tip2": "请立即复制并保存到安全的位置",
"tip3": "关闭窗口后将自动跳转到登录页面",
"tip4": "请使用新 Token 重新登录系统",
"copied": "已复制",
"copyToken": "复制 Token",
"savedClose": "我已保存,关闭",
"securityTip1": "重新生成 Token 会创建系统随机生成的 64 位安全令牌",
"securityTip2": "自定义 Token 必须满足所有安全要求才能使用",
"securityTip3": "更新 Token 后,旧的 Token 将立即失效",
"securityTip4": "请在安全的环境下查看和复制 Token",
"securityTip5": "如果怀疑 Token 泄露,请立即重新生成或更新",
"securityTip6": "建议使用系统生成的 Token 以获得最高安全性"
},
"other": {
"performance": "性能与存储",
"localStorage": "本地存储使用",
"logCache": "日志缓存大小",
"importExport": "导入/导出设置",
"configWizard": "配置向导",
"devTools": "开发者工具",
"clearStorage": "清空本地存储",
"clearStorageDesc": "清空所有本地存储数据",
"clearStorageConfirm": "确认清空",
"clearLogCache": "清空日志缓存",
"clearLogCacheDesc": "清空所有缓存的日志数据",
"clearLogCacheConfirm": "确认清空",
"importSettings": "导入设置",
"exportSettings": "导出设置",
"resetAllSettings": "重置所有设置",
"resetAllSettingsDesc": "将所有设置恢复到默认值",
"resetAllSettingsConfirm": "确认重置",
"clearStorageSuccess": "本地存储已清空",
"clearStorageFailed": "清空失败",
"clearLogSuccess": "日志缓存已清空",
"clearLogFailed": "清空失败",
"importSuccess": "导入成功",
"importFailed": "导入失败",
"exportSuccess": "导出成功",
"exportFailed": "导出失败",
"resetSuccess": "重置成功",
"resetFailed": "重置失败",
"storageItems": "{{count}} 个存储项",
"logCacheSizeDesc": "控制日志查看器最多缓存的日志条数,较大的值会占用更多内存",
"logCacheSizeUnit": "条",
"dataSyncIntervalLabel": "首页数据刷新间隔",
"dataSyncIntervalUnit": "秒",
"dataSyncIntervalDesc": "控制首页统计数据的自动刷新间隔",
"wsReconnectLabel": "WebSocket 重连间隔",
"wsReconnectUnit": "秒",
"wsReconnectDesc": "日志 WebSocket 连接断开后的重连基础间隔",
"wsMaxReconnectLabel": "WebSocket 最大重连次数",
"wsMaxReconnectUnit": "次",
"wsMaxReconnectDesc": "连接失败后的最大重连尝试次数",
"clearLogCacheFn": "清除日志缓存",
"clearLocalCache": "清除本地缓存",
"confirmClearCache": "确认清除本地缓存",
"confirmClearCacheDesc": "这将清除所有本地缓存的设置和数据(不包括登录凭证)。您可能需要重新配置部分偏好设置。确定要继续吗?",
"confirmClear": "确认清除",
"importExportDesc": "导出当前的界面设置以便备份,或从之前导出的文件中恢复设置。",
"exporting": "导出中...",
"importing": "导入中...",
"resetAllSettingsBtn": "重置所有设置为默认值",
"confirmResetAll": "确认重置所有设置",
"confirmResetAllDesc": "这将把所有界面设置恢复为默认值,包括主题、颜色、动画等偏好设置。此操作不会影响您的登录状态。确定要继续吗?",
"configWizardDesc": "重新进行初次配置向导,可以帮助您重新设置系统的基础配置。",
"rerunSetup": "重新进行初次配置",
"confirmRerunSetup": "确认重新配置",
"confirmRerunSetupDesc": "这将带您重新进入初次配置向导。您可以重新设置系统的基础配置项。确定要继续吗?",
"devToolsDesc": "以下功能仅供开发调试使用,可能会导致页面崩溃或异常。",
"triggerError": "触发测试错误",
"confirmTriggerError": "确认触发错误",
"confirmTriggerErrorDesc": "这将手动触发一个 React 错误,用于测试错误边界组件的显示效果。页面将显示错误界面,您可以通过刷新页面或点击返回首页来恢复。",
"confirmTrigger": "确认触发",
"logCleared": "日志已清除",
"logClearedDesc": "日志缓存已清空",
"cacheCleared": "缓存已清除",
"cacheClearedDesc": "已清除 {{count}} 项缓存数据",
"exportSuccessDesc": "设置已导出为 JSON 文件",
"exportFailedDesc": "无法导出设置",
"importSuccessDesc": "成功导入 {{imported}} 项设置",
"importSkippedSuffix": ",跳过 {{skipped}} 项",
"importRefreshHint": "提示",
"importRefreshHintDesc": "部分设置需要刷新页面才能完全生效",
"importNoDataDesc": "没有有效的设置项可导入",
"importInvalidDesc": "文件格式无效",
"resetDone": "已重置",
"resetDoneDesc": "所有设置已恢复为默认值,刷新页面以应用更改"
},
"about": {
"openSource": "开源项目",
"aboutApp": "关于 MaiBot Dashboard",
"version": "版本:",
"author": "作者",
"techStack": "技术栈",
"frontendFramework": "前端框架",
"uiComponents": "UI 组件",
"backend": "后端",
"buildTool": "构建工具",
"openSourceThanks": "开源库感谢",
"openSourceLicense": "开源许可",
"openSourceDesc": "本项目在 GitHub 开源,欢迎 Star ⭐ 支持!",
"visitGitHub": "前往 GitHub",
"appDesc": "麦麦MaiBot的现代化 Web 管理界面",
"maimaiCore": "MaiBot 核心",
"uiFrameworkGroup": "UI 框架与组件",
"routingStateGroup": "路由与状态管理",
"formGroup": "表单处理",
"utilsGroup": "工具库",
"animationGroup": "动画效果",
"backendGroup": "后端框架",
"devToolsGroup": "开发工具",
"openSourceThanksDesc": "本项目使用了以下优秀的开源库,感谢他们的贡献:",
"licenseDesc": "本项目采用 GNU General Public License v3.0 开源许可证。您可以自由地使用、修改和分发本软件,但必须保持相同的开源许可。",
"licenseDeps": "本项目依赖的所有开源库均遵循各自的开源许可证MIT、Apache-2.0、BSD 等)。感谢所有开源贡献者的无私奉献。",
"lib": {
"react": "用户界面构建库",
"shadcn": "优雅的 React 组件库",
"radix": "无样式的可访问组件库",
"tailwind": "实用优先的 CSS 框架",
"lucide": "精美的图标库",
"tanstackRouter": "类型安全的路由库",
"zustand": "轻量级状态管理",
"reactHookForm": "高性能表单库",
"zod": "TypeScript 优先的 schema 验证",
"clsx": "条件 className 构建工具",
"tailwindMerge": "Tailwind 类名合并工具",
"cva": "组件变体管理",
"dateFns": "现代化日期处理库",
"framerMotion": "React 动画库",
"vaul": "抽屉组件动画",
"fastapi": "现代化 Python Web 框架",
"uvicorn": "ASGI 服务器",
"pydantic": "数据验证库",
"pythonMultipart": "文件上传支持",
"typescript": "JavaScript 的超集",
"vite": "下一代前端构建工具",
"eslint": "JavaScript 代码检查工具",
"postcss": "CSS 转换工具"
}
}
},
"auth": {
"title": "登录",
"description": "请输入访问令牌以继续",
"tokenLabel": "Access Token",
"tokenPlaceholder": "请输入 Access Token",
"loginButton": "登录",
"loggingIn": "登录中...",
"loginFailed": "登录失败",
"loginSuccess": "登录成功",
"checkingAuth": "正在检查登录状态...",
"welcome": "欢迎使用 MaiBot",
"accessDesc": "请输入您的 Access Token 以继续访问系统",
"tokenRequired": "请输入 Access Token",
"verifyingLabel": "验证中...",
"verifyEnter": "验证并进入",
"helpLink": "我没有 Token我该去哪里获得 Token",
"helpTitle": "如何获取 Access Token",
"helpDesc": "Access Token 是访问 MaiBot WebUI 的唯一凭证,请按以下方式获取",
"method1Title": "方式一:查看启动日志",
"method1Desc": "在 MaiBot 启动时,控制台会显示 WebUI Access Token。",
"method1Example1": "🔑 WebUI Access Token: abc123...",
"method1Example2": "💡 请使用此 Token 登录 WebUI",
"method2Title": "方式二:查看配置文件",
"method2Desc": "Token 保存在项目根目录的配置文件中:",
"method2FileHint": "打开此文件,复制 access_token 字段的值",
"securityTipTitle": "安全提示",
"securityTip1": "请妥善保管您的 Token不要泄露给他人",
"securityTip2": "如需重置 Token请在登录后前往系统设置",
"slowLink": "我觉得这个界面很卡怎么办?",
"disableAnimTitle": "关闭背景动画",
"disableAnimDesc": "背景动画可能会在低性能设备上造成卡顿。关闭动画可以显著提升界面流畅度。",
"disableAnimDetail": "关闭动画后,背景将变为纯色,但不影响任何功能的使用。您可以随时在系统设置中重新开启动画。",
"disableAnimBtn": "关闭动画",
"verifyFailed": "Token 验证失败,请检查后重试",
"connFailed": "连接服务器失败,请检查网络连接",
"switchToLight": "切换到浅色模式",
"switchToDark": "切换到深色模式"
},
"common": {
"loading": "加载中...",
"error": "错误",
"retry": "重试",
"save": "保存",
"cancel": "取消",
"confirm": "确认",
"delete": "删除",
"edit": "编辑",
"close": "关闭",
"search": "搜索",
"noData": "暂无数据",
"success": "成功",
"failed": "失败"
},
"restart": {
"preparing": "准备重启",
"preparingDesc": "正在发送重启请求...",
"preparingTip": "🔄 正在准备重启麦麦...",
"restarting": "正在重启麦麦",
"restartingDesc": "请稍候,麦麦正在重启中...",
"restartingTip": "🔄 配置已保存,正在重启主程序...",
"checking": "检查服务状态",
"checkingDesc": "等待服务恢复... ({{current}}/{{max}})",
"checkingTip": "⏳ 正在等待服务恢复,请勿关闭页面...",
"success": "重启成功",
"successDesc": "正在跳转到登录页面...",
"successTip": "✅ 配置已生效,服务运行正常",
"failed": "重启超时",
"failedDesc": "服务未能在预期时间内恢复",
"failedTip": "⚠️ 如果长时间无响应,请尝试手动重启",
"refreshPage": "刷新页面",
"retryCheck": "重试检测",
"elapsed": "已用时:"
},
"errorBoundary": {
"title": "页面出现了问题",
"description": "应用程序遇到了意外错误。您可以尝试刷新页面或返回首页。",
"refreshPage": "刷新页面",
"goHome": "返回首页",
"footer": "如果问题持续存在,请将错误信息复制并反馈给开发者",
"copiedToClipboard": "已复制到剪贴板",
"copyError": "复制错误信息"
},
"search": {
"placeholder": "搜索页面...",
"title": "搜索",
"noResults": "未找到匹配的页面",
"startSearch": "输入关键词开始搜索",
"navigate": "导航",
"select": "选择",
"close": "关闭",
"categories": {
"overview": "概览",
"config": "配置",
"resources": "资源",
"monitor": "监控",
"extensions": "扩展",
"system": "系统"
},
"items": {
"home": "首页",
"homeDesc": "查看仪表板概览",
"botConfig": "麦麦主程序配置",
"botConfigDesc": "配置麦麦的核心设置",
"modelProvider": "麦麦模型提供商配置",
"modelProviderDesc": "配置模型提供商",
"model": "麦麦模型配置",
"modelDesc": "配置模型参数",
"emoji": "表情包管理",
"emojiDesc": "管理麦麦的表情包",
"expression": "表达方式管理",
"expressionDesc": "管理麦麦的表达方式",
"person": "人物信息管理",
"personDesc": "管理人物信息",
"jargon": "黑话管理",
"jargonDesc": "管理麦麦学习到的黑话和俚语",
"statistics": "统计信息",
"statisticsDesc": "查看使用统计",
"plugins": "插件市场",
"pluginsDesc": "浏览和安装插件",
"logs": "日志查看器",
"logsDesc": "查看系统日志",
"settings": "系统设置",
"settingsDesc": "配置系统参数"
}
},
"httpWarning": {
"title": "安全警告:",
"message": "您正在使用 HTTP 访问 MaiBot WebUI",
"description": "如果这是公网服务器,您的数据(包括 Token、聊天记录等可能在传输过程中被窃取。强烈建议使用 HTTPS 访问或仅在本地网络使用。",
"dismiss": "关闭警告"
}
}

View File

@@ -2,6 +2,7 @@ import { StrictMode, useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from '@tanstack/react-router'
import './index.css'
import './i18n'
import { router } from './router'
import { AssetStoreProvider } from './components/asset-provider'
import { ThemeProvider } from './components/theme-provider'

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import {
AlertCircle,
@@ -59,6 +60,7 @@ export function AuthPage() {
const [error, setError] = useState('')
const [checkingAuth, setCheckingAuth] = useState(true)
const navigate = useNavigate()
const { t } = useTranslation()
const { enableWavesBackground, setEnableWavesBackground } = useAnimation()
const { theme, setTheme } = useTheme()
@@ -100,7 +102,7 @@ export function AuthPage() {
setError('')
if (!token.trim()) {
setError('请输入 Access Token')
setError(t('auth.tokenRequired'))
return
}
@@ -160,12 +162,12 @@ export function AuthPage() {
}
} else {
console.error('Token 验证失败:', data.message)
setError(data.message || 'Token 验证失败,请检查后重试')
setError(data.message || t('auth.verifyFailed'))
}
} catch (err) {
console.error('Token 验证错误:', err)
setError(
err instanceof Error ? err.message : '连接服务器失败,请检查网络连接'
err instanceof Error ? err.message : t('auth.connFailed')
)
} finally {
setIsValidating(false)
@@ -177,7 +179,7 @@ export function AuthPage() {
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
{enableWavesBackground && <WavesBackground />}
<div className="text-muted-foreground">...</div>
<div className="text-muted-foreground">{t('auth.checkingAuth')}</div>
</div>
)
}
@@ -193,7 +195,7 @@ export function AuthPage() {
<button
onClick={toggleTheme}
className="absolute right-4 top-4 rounded-lg p-2 hover:bg-accent transition-colors z-10 text-foreground"
title={actualTheme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
title={actualTheme === 'dark' ? t('auth.switchToLight') : t('auth.switchToDark')}
>
{actualTheme === 'dark' ? (
<Sun className="h-5 w-5" strokeWidth={2.5} fill="none" />
@@ -209,9 +211,9 @@ export function AuthPage() {
</div>
<div className="space-y-2">
<CardTitle className="text-2xl font-bold">使 MaiBot</CardTitle>
<CardTitle className="text-2xl font-bold">{t('auth.welcome')}</CardTitle>
<CardDescription className="text-base">
Access Token 访
{t('auth.accessDesc')}
</CardDescription>
</div>
</CardHeader>
@@ -228,7 +230,7 @@ export function AuthPage() {
<Input
id="token"
type="password"
placeholder="请输入您的 Access Token"
placeholder={t('auth.tokenPlaceholder')}
value={token}
onChange={(e) => setToken(e.target.value)}
className={cn('pl-10', error && 'border-red-500 focus-visible:ring-red-500')}
@@ -252,10 +254,10 @@ export function AuthPage() {
{isValidating ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
...
{t('auth.verifyingLabel')}
</>
) : (
'验证并进入'
t('auth.verifyEnter')
)}
</Button>
@@ -264,17 +266,17 @@ export function AuthPage() {
<DialogTrigger asChild>
<button className="w-full text-center text-sm text-primary hover:text-primary/80 transition-colors underline-offset-4 hover:underline flex items-center justify-center gap-1">
<HelpCircle className="h-4 w-4" strokeWidth={2} fill="none" />
Token Token
{t('auth.helpLink')}
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Lock className="h-5 w-5 text-primary" strokeWidth={2} fill="none" />
Access Token
{t('auth.helpTitle')}
</DialogTitle>
<DialogDescription>
Access Token 访 MaiBot WebUI
{t('auth.helpDesc')}
</DialogDescription>
</DialogHeader>
@@ -284,13 +286,13 @@ export function AuthPage() {
<div className="flex items-start gap-3">
<Terminal className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
<div className="flex-1 space-y-2">
<h4 className="font-semibold text-sm"></h4>
<h4 className="font-semibold text-sm">{t('auth.method1Title')}</h4>
<p className="text-sm text-muted-foreground">
MaiBot WebUI Access Token
{t('auth.method1Desc')}
</p>
<div className="rounded bg-background p-2 font-mono text-xs">
<p className="text-muted-foreground">🔑 WebUI Access Token: abc123...</p>
<p className="text-muted-foreground">💡 使 Token WebUI</p>
<p className="text-muted-foreground">{t('auth.method1Example1')}</p>
<p className="text-muted-foreground">{t('auth.method1Example2')}</p>
</div>
</div>
</div>
@@ -301,15 +303,15 @@ export function AuthPage() {
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
<div className="flex-1 space-y-2">
<h4 className="font-semibold text-sm"></h4>
<h4 className="font-semibold text-sm">{t('auth.method2Title')}</h4>
<p className="text-sm text-muted-foreground">
Token
{t('auth.method2Desc')}
</p>
<div className="rounded bg-background p-2 font-mono text-xs break-all">
<code className="text-primary">data/webui.json</code>
</div>
<p className="text-xs text-muted-foreground">
<code className="px-1 py-0.5 bg-background rounded">access_token</code>
{t('auth.method2FileHint')} <code className="px-1 py-0.5 bg-background rounded">access_token</code>
</p>
</div>
</div>
@@ -320,10 +322,10 @@ export function AuthPage() {
<div className="flex gap-2">
<AlertCircle className="h-4 w-4 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
<div className="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
<p className="font-semibold"></p>
<p className="font-semibold">{t('auth.securityTipTitle')}</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li> Token</li>
<li> Token</li>
<li>{t('auth.securityTip1')}</li>
<li>{t('auth.securityTip2')}</li>
</ul>
</div>
</div>
@@ -337,30 +339,30 @@ export function AuthPage() {
<AlertDialogTrigger asChild>
<button className="w-full text-center text-sm text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline flex items-center justify-center gap-1">
<Zap className="h-4 w-4" strokeWidth={2} fill="none" />
{t('auth.slowLink')}
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-primary" strokeWidth={2} fill="none" />
{t('auth.disableAnimTitle')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('auth.disableAnimDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<div className="rounded-lg border bg-muted/50 p-4 space-y-2">
<p className="text-sm text-muted-foreground">
使
{t('auth.disableAnimDetail')}
</p>
</div>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => setEnableWavesBackground(false)}
>
{t('auth.disableAnimBtn')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -1,3 +1,5 @@
import { useTranslation } from 'react-i18next'
import { ScrollArea } from '@/components/ui/scroll-area'
import { APP_NAME, APP_VERSION } from '@/lib/version'
@@ -6,6 +8,8 @@ import { cn } from '@/lib/utils'
import { LibraryItem } from './LibraryItem'
export function AboutTab() {
const { t } = useTranslation()
return (
<div className="space-y-4 sm:space-y-6">
{/* GitHub 开源地址 */}
@@ -27,10 +31,10 @@ export function AboutTab() {
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg sm:text-xl font-bold text-foreground mb-2">
{t('settings.about.openSource')}
</h3>
<p className="text-sm sm:text-base text-muted-foreground mb-3">
GitHub Star
{t('settings.about.openSourceDesc')}
</p>
<a
href="https://github.com/Mai-with-u/MaiBot-Dashboard"
@@ -55,7 +59,7 @@ export function AboutTab() {
clipRule="evenodd"
/>
</svg>
GitHub
{t('settings.about.visitGitHub')}
<svg
className="h-4 w-4"
fill="none"
@@ -76,19 +80,19 @@ export function AboutTab() {
{/* 应用信息 */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"> {APP_NAME}</h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.aboutApp')} {APP_NAME}</h3>
<div className="space-y-2 text-xs sm:text-sm text-muted-foreground">
<p>: {APP_VERSION}</p>
<p>MaiBot Web </p>
<p>{t('settings.about.version')} {APP_VERSION}</p>
<p>{t('settings.about.appDesc')}</p>
</div>
</div>
{/* 作者信息 */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.author')}</h3>
<div className="space-y-3">
<div className="space-y-1">
<p className="text-sm font-medium">MaiBot </p>
<p className="text-sm font-medium">{t('settings.about.maimaiCore')}</p>
<p className="text-xs sm:text-sm text-muted-foreground">Mai-with-u</p>
</div>
<div className="space-y-1">
@@ -100,10 +104,10 @@ export function AboutTab() {
{/* 技术栈 */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.techStack')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs sm:text-sm text-muted-foreground">
<div className="space-y-1.5">
<p className="font-medium text-foreground"></p>
<p className="font-medium text-foreground">{t('settings.about.frontendFramework')}</p>
<ul className="space-y-0.5 list-disc list-inside">
<li>React 19.2.0</li>
<li>TypeScript 5.7.2</li>
@@ -112,7 +116,7 @@ export function AboutTab() {
</ul>
</div>
<div className="space-y-1.5">
<p className="font-medium text-foreground">UI </p>
<p className="font-medium text-foreground">{t('settings.about.uiComponents')}</p>
<ul className="space-y-0.5 list-disc list-inside">
<li>shadcn/ui</li>
<li>Radix UI</li>
@@ -121,7 +125,7 @@ export function AboutTab() {
</ul>
</div>
<div className="space-y-1.5">
<p className="font-medium text-foreground"></p>
<p className="font-medium text-foreground">{t('settings.about.backend')}</p>
<ul className="space-y-0.5 list-disc list-inside">
<li>Python 3.12+</li>
<li>FastAPI</li>
@@ -130,7 +134,7 @@ export function AboutTab() {
</ul>
</div>
<div className="space-y-1.5">
<p className="font-medium text-foreground"></p>
<p className="font-medium text-foreground">{t('settings.about.buildTool')}</p>
<ul className="space-y-0.5 list-disc list-inside">
<li>Bun / npm</li>
<li>ESLint 9.17.0</li>
@@ -142,81 +146,81 @@ export function AboutTab() {
{/* 开源感谢 */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.openSourceThanks')}</h3>
<p className="text-xs sm:text-sm text-muted-foreground mb-3">
使
{t('settings.about.openSourceThanksDesc')}
</p>
<ScrollArea className="h-[300px] sm:h-[400px]">
<div className="space-y-4 pr-4">
{/* UI 框架 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">UI </p>
<p className="text-sm font-medium text-foreground">{t('settings.about.uiFrameworkGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="React" description="用户界面构建库" license="MIT" />
<LibraryItem name="shadcn/ui" description="优雅的 React 组件库" license="MIT" />
<LibraryItem name="Radix UI" description="无样式的可访问组件库" license="MIT" />
<LibraryItem name="Tailwind CSS" description="实用优先的 CSS 框架" license="MIT" />
<LibraryItem name="Lucide React" description="精美的图标库" license="ISC" />
<LibraryItem name="React" description={t('settings.about.lib.react')} license="MIT" />
<LibraryItem name="shadcn/ui" description={t('settings.about.lib.shadcn')} license="MIT" />
<LibraryItem name="Radix UI" description={t('settings.about.lib.radix')} license="MIT" />
<LibraryItem name="Tailwind CSS" description={t('settings.about.lib.tailwind')} license="MIT" />
<LibraryItem name="Lucide React" description={t('settings.about.lib.lucide')} license="ISC" />
</div>
</div>
{/* 路由与状态 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-sm font-medium text-foreground">{t('settings.about.routingStateGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="TanStack Router" description="类型安全的路由库" license="MIT" />
<LibraryItem name="Zustand" description="轻量级状态管理" license="MIT" />
<LibraryItem name="TanStack Router" description={t('settings.about.lib.tanstackRouter')} license="MIT" />
<LibraryItem name="Zustand" description={t('settings.about.lib.zustand')} license="MIT" />
</div>
</div>
{/* 表单与验证 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-sm font-medium text-foreground">{t('settings.about.formGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="React Hook Form" description="高性能表单库" license="MIT" />
<LibraryItem name="Zod" description="TypeScript 优先的 schema 验证" license="MIT" />
<LibraryItem name="React Hook Form" description={t('settings.about.lib.reactHookForm')} license="MIT" />
<LibraryItem name="Zod" description={t('settings.about.lib.zod')} license="MIT" />
</div>
</div>
{/* 工具库 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-sm font-medium text-foreground">{t('settings.about.utilsGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="clsx" description="条件 className 构建工具" license="MIT" />
<LibraryItem name="tailwind-merge" description="Tailwind 类名合并工具" license="MIT" />
<LibraryItem name="class-variance-authority" description="组件变体管理" license="Apache-2.0" />
<LibraryItem name="date-fns" description="现代化日期处理库" license="MIT" />
<LibraryItem name="clsx" description={t('settings.about.lib.clsx')} license="MIT" />
<LibraryItem name="tailwind-merge" description={t('settings.about.lib.tailwindMerge')} license="MIT" />
<LibraryItem name="class-variance-authority" description={t('settings.about.lib.cva')} license="Apache-2.0" />
<LibraryItem name="date-fns" description={t('settings.about.lib.dateFns')} license="MIT" />
</div>
</div>
{/* 动画 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-sm font-medium text-foreground">{t('settings.about.animationGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="Framer Motion" description="React 动画库" license="MIT" />
<LibraryItem name="vaul" description="抽屉组件动画" license="MIT" />
<LibraryItem name="Framer Motion" description={t('settings.about.lib.framerMotion')} license="MIT" />
<LibraryItem name="vaul" description={t('settings.about.lib.vaul')} license="MIT" />
</div>
</div>
{/* 后端相关 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-sm font-medium text-foreground">{t('settings.about.backendGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="FastAPI" description="现代化 Python Web 框架" license="MIT" />
<LibraryItem name="Uvicorn" description="ASGI 服务器" license="BSD-3-Clause" />
<LibraryItem name="Pydantic" description="数据验证库" license="MIT" />
<LibraryItem name="python-multipart" description="文件上传支持" license="Apache-2.0" />
<LibraryItem name="FastAPI" description={t('settings.about.lib.fastapi')} license="MIT" />
<LibraryItem name="Uvicorn" description={t('settings.about.lib.uvicorn')} license="BSD-3-Clause" />
<LibraryItem name="Pydantic" description={t('settings.about.lib.pydantic')} license="MIT" />
<LibraryItem name="python-multipart" description={t('settings.about.lib.pythonMultipart')} license="Apache-2.0" />
</div>
</div>
{/* 开发工具 */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground"></p>
<p className="text-sm font-medium text-foreground">{t('settings.about.devToolsGroup')}</p>
<div className="grid gap-2 text-xs sm:text-sm">
<LibraryItem name="TypeScript" description="JavaScript 的超集" license="Apache-2.0" />
<LibraryItem name="Vite" description="下一代前端构建工具" license="MIT" />
<LibraryItem name="ESLint" description="JavaScript 代码检查工具" license="MIT" />
<LibraryItem name="PostCSS" description="CSS 转换工具" license="MIT" />
<LibraryItem name="TypeScript" description={t('settings.about.lib.typescript')} license="Apache-2.0" />
<LibraryItem name="Vite" description={t('settings.about.lib.vite')} license="MIT" />
<LibraryItem name="ESLint" description={t('settings.about.lib.eslint')} license="MIT" />
<LibraryItem name="PostCSS" description={t('settings.about.lib.postcss')} license="MIT" />
</div>
</div>
</div>
@@ -225,7 +229,7 @@ export function AboutTab() {
{/* 许可证 */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.openSourceLicense')}</h3>
<div className="space-y-3">
<div className="rounded-lg bg-primary/5 border border-primary/20 p-3 sm:p-4">
<div className="flex items-start gap-2 sm:gap-3">
@@ -239,15 +243,13 @@ export function AboutTab() {
MaiBot WebUI
</p>
<p className="text-xs sm:text-sm text-muted-foreground">
GNU General Public License v3.0
使
{t('settings.about.licenseDesc')}
</p>
</div>
</div>
</div>
<p className="text-xs sm:text-sm text-muted-foreground">
MITApache-2.0BSD
{t('settings.about.licenseDeps')}
</p>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { useState, useMemo, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { AlertTriangle, Download, RotateCcw, Trash2, Upload } from 'lucide-react'
import { useAnimation } from '@/hooks/use-animation'
@@ -77,6 +78,7 @@ export function AppearanceTab() {
const { theme, setTheme, themeConfig, updateThemeConfig, resolvedTheme, resetTheme } = useTheme()
const { enableAnimations, setEnableAnimations, enableWavesBackground, setEnableWavesBackground } = useAnimation()
const { toast } = useToast()
const { t } = useTranslation()
const [localCSS, setLocalCSS] = useState(themeConfig.customCSS || '')
const [cssWarnings, setCssWarnings] = useState<string[]>([])
@@ -157,10 +159,10 @@ export function AppearanceTab() {
const result = importThemeJSON(json)
if (result.success) {
// 导入成功后需要刷新页面使配置生效(因为 ThemeProvider 需要重新读取 localStorage
toast({ title: '导入成功', description: '主题配置已导入,页面将自动刷新' })
toast({ title: t('settings.appearance.importSuccess'), description: t('settings.appearance.importSuccessDesc') })
setTimeout(() => window.location.reload(), 1000)
} else {
toast({ title: '导入失败', description: result.errors.join('; '), variant: 'destructive' })
toast({ title: t('settings.appearance.importFailed'), description: result.errors.join('; '), variant: 'destructive' })
}
}
reader.readAsText(file)
@@ -172,7 +174,7 @@ export function AppearanceTab() {
resetTheme()
setLocalCSS('')
setCssWarnings([])
toast({ title: '重置成功', description: '主题已重置为默认值' })
toast({ title: t('settings.appearance.resetSuccess'), description: t('settings.appearance.resetSuccessDesc') })
}
const previewTokens = useMemo(() => {
@@ -216,28 +218,28 @@ export function AppearanceTab() {
<div className="space-y-6 sm:space-y-8">
{/* 主题模式 */}
<div>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.themeMode')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<ThemeOption
value="light"
current={theme}
onChange={setTheme}
label="浅色"
description="始终使用浅色主题"
label={t('settings.appearance.light')}
description={t('settings.appearance.lightDesc')}
/>
<ThemeOption
value="dark"
current={theme}
onChange={setTheme}
label="深色"
description="始终使用深色主题"
label={t('settings.appearance.dark')}
description={t('settings.appearance.darkDesc')}
/>
<ThemeOption
value="system"
current={theme}
onChange={setTheme}
label="跟随系统"
description="根据系统设置自动切换"
label={t('settings.appearance.system')}
description={t('settings.appearance.systemDesc')}
/>
</div>
</div>
@@ -245,7 +247,7 @@ export function AppearanceTab() {
{/* 主题色配置 */}
<div>
<div className="flex items-center justify-between mb-3 sm:mb-4">
<h3 className="text-base sm:text-lg font-semibold"></h3>
<h3 className="text-base sm:text-lg font-semibold">{t('settings.appearance.accentColor')}</h3>
<Button
variant="outline"
size="sm"
@@ -254,7 +256,7 @@ export function AppearanceTab() {
className="h-8"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
{t('settings.appearance.resetDefault')}
</Button>
</div>
@@ -271,8 +273,8 @@ export function AppearanceTab() {
/>
</div>
<div className="space-y-1">
<Label htmlFor="accent-color-input" className="font-medium"></Label>
<p className="text-xs text-muted-foreground"> HEX </p>
<Label htmlFor="accent-color-input" className="font-medium">{t('settings.appearance.accentPrimary')}</Label>
<p className="text-xs text-muted-foreground">{t('settings.appearance.accentHint')}</p>
</div>
</div>
@@ -290,7 +292,7 @@ export function AppearanceTab() {
{/* 实时色板预览 */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground"></h4>
<h4 className="text-sm font-medium text-muted-foreground">{t('settings.appearance.colorPreview')}</h4>
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-3">
<ColorTokenPreview name="primary" value={previewTokens.primary} foreground={previewTokens['primary-foreground']} />
<ColorTokenPreview name="secondary" value={previewTokens.secondary} foreground={previewTokens['secondary-foreground']} />
@@ -307,13 +309,13 @@ export function AppearanceTab() {
{/* 样式微调 */}
<div>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.styleTweaks')}</h3>
<Accordion type="single" collapsible className="w-full">
{/* 1. 字体排版 (Typography) */}
<AccordionItem value="typography">
<AccordionTrigger> (Typography)</AccordionTrigger>
<AccordionTrigger>{t('settings.appearance.typographyGroup')}</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 pt-2">
<div className="flex justify-end">
@@ -325,12 +327,12 @@ export function AppearanceTab() {
className="h-8 text-xs"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
{t('settings.appearance.resetDefault')}
</Button>
</div>
<div className="space-y-2">
<Label> (Font Family)</Label>
<Label>{t('settings.appearance.fontFamilyLabel')}</Label>
<Select
value={(() => {
const fontFamily = getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-family-base', '')
@@ -351,20 +353,20 @@ export function AppearanceTab() {
}}
>
<SelectTrigger>
<SelectValue placeholder="选择字体族" />
<SelectValue placeholder={t('settings.appearance.fontFamilyPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="system"> (System)</SelectItem>
<SelectItem value="sans">线 (Sans-serif)</SelectItem>
<SelectItem value="serif">线 (Serif)</SelectItem>
<SelectItem value="mono"> (Monospace)</SelectItem>
<SelectItem value="system">{t('settings.appearance.fontFamilySystem')}</SelectItem>
<SelectItem value="sans">{t('settings.appearance.fontFamilySans')}</SelectItem>
<SelectItem value="serif">{t('settings.appearance.fontFamilySerif')}</SelectItem>
<SelectItem value="mono">{t('settings.appearance.fontFamilyMono')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Base Size)</Label>
<Label>{t('settings.appearance.baseFontSize')}</Label>
<span className="text-sm text-muted-foreground">
{parseFloat(getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-size-base', '1')) * 16}px
</span>
@@ -384,7 +386,7 @@ export function AppearanceTab() {
</div>
<div className="space-y-2">
<Label> (Line Height)</Label>
<Label>{t('settings.appearance.lineHeight')}</Label>
<Select
value={String(getTokenValue(themeConfig.tokenOverrides, 'typography', 'line-height-normal', 1.5))}
onValueChange={(val) => {
@@ -394,12 +396,12 @@ export function AppearanceTab() {
}}
>
<SelectTrigger>
<SelectValue placeholder="选择行高" />
<SelectValue placeholder={t('settings.appearance.lineHeightPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="1.2"> (1.2)</SelectItem>
<SelectItem value="1.5"> (1.5)</SelectItem>
<SelectItem value="1.75"> (1.75)</SelectItem>
<SelectItem value="1.2">{t('settings.appearance.lineHeightCompact')}</SelectItem>
<SelectItem value="1.5">{t('settings.appearance.lineHeightNormal')}</SelectItem>
<SelectItem value="1.75">{t('settings.appearance.lineHeightLoose')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -409,7 +411,7 @@ export function AppearanceTab() {
{/* 2. 视觉效果 (Visual) */}
<AccordionItem value="visual">
<AccordionTrigger> (Visual)</AccordionTrigger>
<AccordionTrigger>{t('settings.appearance.visualGroup')}</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 pt-2">
<div className="flex justify-end">
@@ -421,13 +423,13 @@ export function AppearanceTab() {
className="h-8 text-xs"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
{t('settings.appearance.resetDefault')}
</Button>
</div>
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Radius)</Label>
<Label>{t('settings.appearance.borderRadiusLabel')}</Label>
<span className="text-sm text-muted-foreground">
{Math.round(parseFloat(getTokenValue(themeConfig.tokenOverrides, 'visual', 'radius-md', '0.375')) * 16)}px
</span>
@@ -447,7 +449,7 @@ export function AppearanceTab() {
</div>
<div className="space-y-2">
<Label> (Shadow)</Label>
<Label>{t('settings.appearance.shadowLabel')}</Label>
<Select
value={(() => {
const shadowMd = String(getTokenValue(themeConfig.tokenOverrides, 'visual', 'shadow-md', ''))
@@ -470,20 +472,20 @@ export function AppearanceTab() {
}}
>
<SelectTrigger>
<SelectValue placeholder="选择阴影强度" />
<SelectValue placeholder={t('settings.appearance.shadowPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
<SelectItem value="sm"> (Small)</SelectItem>
<SelectItem value="md"> (Medium)</SelectItem>
<SelectItem value="lg"> (Large)</SelectItem>
<SelectItem value="xl"> (Extra Large)</SelectItem>
<SelectItem value="none">{t('settings.appearance.shadowNone')}</SelectItem>
<SelectItem value="sm">{t('settings.appearance.shadowSm')}</SelectItem>
<SelectItem value="md">{t('settings.appearance.shadowMd')}</SelectItem>
<SelectItem value="lg">{t('settings.appearance.shadowLg')}</SelectItem>
<SelectItem value="xl">{t('settings.appearance.shadowXl')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="blur-switch"> (Blur)</Label>
<Label htmlFor="blur-switch">{t('settings.appearance.blurLabel')}</Label>
<Switch
id="blur-switch"
checked={getTokenValue(themeConfig.tokenOverrides, 'visual', 'blur-md', '0px') !== '0px'}
@@ -500,7 +502,7 @@ export function AppearanceTab() {
{/* 3. 布局 (Layout) */}
<AccordionItem value="layout">
<AccordionTrigger> (Layout)</AccordionTrigger>
<AccordionTrigger>{t('settings.appearance.layoutGroup')}</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 pt-2">
<div className="flex justify-end">
@@ -512,13 +514,13 @@ export function AppearanceTab() {
className="h-8 text-xs"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
{t('settings.appearance.resetDefault')}
</Button>
</div>
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Sidebar Width)</Label>
<Label>{t('settings.appearance.sidebarWidthLabel')}</Label>
<span className="text-sm text-muted-foreground">
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'sidebar-width', '16rem')}
</span>
@@ -539,7 +541,7 @@ export function AppearanceTab() {
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Max Width)</Label>
<Label>{t('settings.appearance.maxContentWidth')}</Label>
<span className="text-sm text-muted-foreground">
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'max-content-width', '1280px')}
</span>
@@ -560,7 +562,7 @@ export function AppearanceTab() {
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Spacing Unit)</Label>
<Label>{t('settings.appearance.spacingUnit')}</Label>
<span className="text-sm text-muted-foreground">
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'space-unit', '0.25rem')}
</span>
@@ -584,7 +586,7 @@ export function AppearanceTab() {
{/* 4. 动画 (Animation) */}
<AccordionItem value="animation">
<AccordionTrigger> (Animation)</AccordionTrigger>
<AccordionTrigger>{t('settings.appearance.animationGroup')}</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 pt-2">
<div className="flex justify-end">
@@ -596,12 +598,12 @@ export function AppearanceTab() {
className="h-8 text-xs"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
{t('settings.appearance.resetDefault')}
</Button>
</div>
<div className="space-y-2">
<Label> (Speed)</Label>
<Label>{t('settings.appearance.animationSpeedLabel')}</Label>
<Select
value={(() => {
const duration = String(getTokenValue(themeConfig.tokenOverrides, 'animation', 'anim-duration-normal', '300ms'))
@@ -629,13 +631,13 @@ export function AppearanceTab() {
}}
>
<SelectTrigger>
<SelectValue placeholder="选择动画速度" />
<SelectValue placeholder={t('settings.appearance.animationSpeedPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="fast"> (100ms)</SelectItem>
<SelectItem value="normal"> (300ms)</SelectItem>
<SelectItem value="slow"> (500ms)</SelectItem>
<SelectItem value="off"> (0ms)</SelectItem>
<SelectItem value="fast">{t('settings.appearance.animationFast')}</SelectItem>
<SelectItem value="normal">{t('settings.appearance.animationNormal')}</SelectItem>
<SelectItem value="slow">{t('settings.appearance.animationSlow')}</SelectItem>
<SelectItem value="off">{t('settings.appearance.animationOff')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -645,13 +647,13 @@ export function AppearanceTab() {
{/* 5. 背景设置 (Backgrounds) */}
<AccordionItem value="backgrounds">
<AccordionTrigger> (Backgrounds)</AccordionTrigger>
<AccordionTrigger>{t('settings.appearance.backgroundGroup')}</AccordionTrigger>
<AccordionContent>
<div className="pt-2">
<Tabs defaultValue="page">
<TabsList className="w-full grid grid-cols-5">
<TabsTrigger value="page"></TabsTrigger>
<TabsTrigger value="sidebar"></TabsTrigger>
<TabsTrigger value="page">{t('settings.appearance.bgPage')}</TabsTrigger>
<TabsTrigger value="sidebar">{t('settings.appearance.bgSidebar')}</TabsTrigger>
<TabsTrigger value="header">Header</TabsTrigger>
<TabsTrigger value="card">Card</TabsTrigger>
<TabsTrigger value="dialog">Dialog</TabsTrigger>
@@ -662,8 +664,8 @@ export function AppearanceTab() {
{layerId !== 'page' && (
<div className="flex items-center justify-between rounded-lg border bg-muted/30 px-4 py-3">
<div className="space-y-0.5">
<Label className="text-sm font-medium"></Label>
<p className="text-xs text-muted-foreground">使</p>
<Label className="text-sm font-medium">{t('settings.appearance.inheritParentBg')}</Label>
<p className="text-xs text-muted-foreground">{t('settings.appearance.inheritParentBgDesc')}</p>
</div>
<Switch
checked={bgConfig[layerId]?.inherit ?? false}
@@ -696,9 +698,9 @@ export function AppearanceTab() {
<div>
<div className="flex items-center justify-between mb-3 sm:mb-4">
<div>
<h3 className="text-base sm:text-lg font-semibold"> CSS</h3>
<h3 className="text-base sm:text-lg font-semibold">{t('settings.appearance.customCss')}</h3>
<p className="text-sm text-muted-foreground mt-1">
CSS CSS @importurl()
{t('settings.appearance.cssDescription')}
</p>
</div>
<Button
@@ -712,7 +714,7 @@ export function AppearanceTab() {
disabled={!themeConfig.customCSS}
>
<Trash2 className="h-4 w-4 mr-1" />
{t('settings.appearance.clearCss')}
</Button>
</div>
@@ -721,7 +723,7 @@ export function AppearanceTab() {
value={localCSS}
language="css"
height="250px"
placeholder={`/* 在这里输入自定义 CSS */\n\n/* 例如: */\n/* .sidebar { background: #1a1a2e; } */`}
placeholder={t('settings.appearance.cssPlaceholder')}
onChange={handleCSSChange}
/>
@@ -729,7 +731,7 @@ export function AppearanceTab() {
<div className="rounded-md bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 p-3">
<div className="flex items-center gap-2 text-yellow-800 dark:text-yellow-200 text-sm font-medium mb-1">
<AlertTriangle className="h-4 w-4" />
{t('settings.appearance.cssWarningTitle')}
</div>
<ul className="text-xs text-yellow-700 dark:text-yellow-300 space-y-0.5 ml-6 list-disc">
{cssWarnings.map((w, i) => <li key={i}>{w}</li>)}
@@ -741,17 +743,17 @@ export function AppearanceTab() {
{/* 动效设置 */}
<div>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.animationEffect')}</h3>
<div className="space-y-2 sm:space-y-3">
{/* 全局动画开关 */}
<div className="rounded-lg border bg-card p-3 sm:p-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5 flex-1">
<Label htmlFor="animations" className="text-base font-medium cursor-pointer">
{t('settings.appearance.enableAnimations')}
</Label>
<p className="text-sm text-muted-foreground">
{t('settings.appearance.enableAnimationsDesc')}
</p>
</div>
<Switch
@@ -767,10 +769,10 @@ export function AppearanceTab() {
<div className="flex items-center justify-between">
<div className="space-y-0.5 flex-1">
<Label htmlFor="waves-background" className="text-base font-medium cursor-pointer">
{t('settings.appearance.loginWavesBackground')}
</Label>
<p className="text-sm text-muted-foreground">
使
{t('settings.appearance.loginWavesBackgroundDesc')}
</p>
</div>
<Switch
@@ -785,7 +787,7 @@ export function AppearanceTab() {
{/* 主题导入/导出 */}
<div>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">/</h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.importExportTheme')}</h3>
<div className="rounded-lg border bg-card p-3 sm:p-4 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{/* 导出按钮 */}
@@ -795,7 +797,7 @@ export function AppearanceTab() {
className="gap-2"
>
<Download className="h-4 w-4" />
{t('settings.appearance.exportTheme')}
</Button>
{/* 导入按钮 */}
@@ -805,7 +807,7 @@ export function AppearanceTab() {
className="gap-2"
>
<Upload className="h-4 w-4" />
{t('settings.appearance.importTheme')}
</Button>
{/* 重置按钮 */}
@@ -816,20 +818,20 @@ export function AppearanceTab() {
className="gap-2"
>
<RotateCcw className="h-4 w-4" />
{t('settings.appearance.resetTheme')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t('settings.appearance.confirmResetTheme')}</AlertDialogTitle>
<AlertDialogDescription>
CSS
{t('settings.appearance.confirmResetThemeDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleResetTheme}>
{t('settings.appearance.confirmResetAction')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -846,7 +848,7 @@ export function AppearanceTab() {
/>
<p className="text-xs text-muted-foreground">
JSON 便
{t('settings.appearance.exportDesc')}
</p>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { AlertTriangle, Database, Download, HardDrive, RefreshCw, RotateCcw, Trash2, Upload } from 'lucide-react'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
@@ -14,6 +15,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
// 其他设置标签页
export function OtherTab() {
const { t } = useTranslation()
const navigate = useNavigate()
const { toast } = useToast()
const [isResetting, setIsResetting] = useState(false)
@@ -73,8 +75,8 @@ export function OtherTab() {
const handleClearLogCache = () => {
logWebSocket.clearLogs()
toast({
title: '日志已清除',
description: '日志缓存已清空',
title: t('settings.other.logCleared'),
description: t('settings.other.logClearedDesc'),
})
}
@@ -83,8 +85,8 @@ export function OtherTab() {
const result = clearLocalCache()
refreshStorageUsage()
toast({
title: '缓存已清除',
description: `已清除 ${result.clearedKeys.length} 项缓存数据`,
title: t('settings.other.cacheCleared'),
description: t('settings.other.cacheClearedDesc', { count: result.clearedKeys.length }),
})
}
@@ -104,14 +106,14 @@ export function OtherTab() {
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast({
title: '导出成功',
description: '设置已导出为 JSON 文件',
title: t('settings.other.exportSuccess'),
description: t('settings.other.exportSuccessDesc'),
})
} catch (error) {
console.error('导出设置失败:', error)
toast({
title: '导出失败',
description: '无法导出设置',
title: t('settings.other.exportFailed'),
description: t('settings.other.exportFailedDesc'),
variant: 'destructive',
})
} finally {
@@ -141,29 +143,29 @@ export function OtherTab() {
refreshStorageUsage()
toast({
title: '导入成功',
description: `成功导入 ${result.imported.length} 项设置${result.skipped.length > 0 ? `,跳过 ${result.skipped.length}` : ''}`,
title: t('settings.other.importSuccess'),
description: t('settings.other.importSuccessDesc', { imported: result.imported.length }) + (result.skipped.length > 0 ? t('settings.other.importSkippedSuffix', { skipped: result.skipped.length }) : ''),
})
// 提示用户刷新页面以应用所有更改
if (result.imported.includes('theme') || result.imported.includes('accentColor')) {
toast({
title: '提示',
description: '部分设置需要刷新页面才能完全生效',
title: t('settings.other.importRefreshHint'),
description: t('settings.other.importRefreshHintDesc'),
})
}
} else {
toast({
title: '导入失败',
description: '没有有效的设置项可导入',
title: t('settings.other.importFailed'),
description: t('settings.other.importNoDataDesc'),
variant: 'destructive',
})
}
} catch (error) {
console.error('导入设置失败:', error)
toast({
title: '导入失败',
description: '文件格式无效',
title: t('settings.other.importFailed'),
description: t('settings.other.importInvalidDesc'),
variant: 'destructive',
})
} finally {
@@ -187,8 +189,8 @@ export function OtherTab() {
setDataSyncInterval(DEFAULT_SETTINGS.dataSyncInterval)
refreshStorageUsage()
toast({
title: '已重置',
description: '所有设置已恢复为默认值,刷新页面以应用更改',
title: t('settings.other.resetDone'),
description: t('settings.other.resetDoneDesc'),
})
}
@@ -205,8 +207,8 @@ export function OtherTab() {
if (response.ok && data.success) {
toast({
title: '重置成功',
description: '即将进入初次配置向导',
title: t('settings.other.resetSuccess'),
description: t('settings.other.clearStorageSuccess'),
})
// 延迟跳转到配置向导
@@ -215,16 +217,16 @@ export function OtherTab() {
}, 1000)
} else {
toast({
title: '重置失败',
description: data.message || '无法重置配置状态',
title: t('settings.other.resetFailed'),
description: data.message || t('settings.other.clearStorageFailed'),
variant: 'destructive',
})
}
} catch (error) {
console.error('重置配置状态错误:', error)
toast({
title: '重置失败',
description: '连接服务器失败',
title: t('settings.other.resetFailed'),
description: t('settings.other.clearStorageFailed'),
variant: 'destructive',
})
} finally {
@@ -238,7 +240,7 @@ export function OtherTab() {
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4 flex items-center gap-2">
<Database className="h-5 w-5" />
{t('settings.other.performance')}
</h3>
<div className="space-y-4 sm:space-y-5">
{/* 存储使用情况 */}
@@ -246,21 +248,21 @@ export function OtherTab() {
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium flex items-center gap-2">
<HardDrive className="h-4 w-4" />
使
{t('settings.other.localStorage')}
</span>
<Button variant="ghost" size="sm" onClick={refreshStorageUsage} className="h-7 px-2">
<RefreshCw className="h-3 w-3" />
</Button>
</div>
<div className="text-2xl font-bold text-primary">{formatBytes(storageUsage.used)}</div>
<p className="text-xs text-muted-foreground mt-1">{storageUsage.items} </p>
<p className="text-xs text-muted-foreground mt-1">{t('settings.other.storageItems', { count: storageUsage.items })}</p>
</div>
{/* 日志缓存大小 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium"></Label>
<span className="text-sm text-muted-foreground">{logCacheSize} </span>
<Label className="text-sm font-medium">{t('settings.other.logCache')}</Label>
<span className="text-sm text-muted-foreground">{logCacheSize} {t('settings.other.logCacheSizeUnit')}</span>
</div>
<Slider
value={[logCacheSize]}
@@ -271,15 +273,15 @@ export function OtherTab() {
className="w-full"
/>
<p className="text-xs text-muted-foreground">
{t('settings.other.logCacheSizeDesc')}
</p>
</div>
{/* 数据刷新间隔 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium"></Label>
<span className="text-sm text-muted-foreground">{dataSyncInterval} </span>
<Label className="text-sm font-medium">{t('settings.other.dataSyncIntervalLabel')}</Label>
<span className="text-sm text-muted-foreground">{dataSyncInterval} {t('settings.other.dataSyncIntervalUnit')}</span>
</div>
<Slider
value={[dataSyncInterval]}
@@ -290,15 +292,15 @@ export function OtherTab() {
className="w-full"
/>
<p className="text-xs text-muted-foreground">
{t('settings.other.dataSyncIntervalDesc')}
</p>
</div>
{/* WebSocket 重连间隔 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">WebSocket </Label>
<span className="text-sm text-muted-foreground">{wsReconnectInterval / 1000} </span>
<Label className="text-sm font-medium">{t('settings.other.wsReconnectLabel')}</Label>
<span className="text-sm text-muted-foreground">{wsReconnectInterval / 1000} {t('settings.other.wsReconnectUnit')}</span>
</div>
<Slider
value={[wsReconnectInterval]}
@@ -309,15 +311,15 @@ export function OtherTab() {
className="w-full"
/>
<p className="text-xs text-muted-foreground">
WebSocket
{t('settings.other.wsReconnectDesc')}
</p>
</div>
{/* WebSocket 最大重连次数 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">WebSocket </Label>
<span className="text-sm text-muted-foreground">{wsMaxReconnectAttempts} </span>
<Label className="text-sm font-medium">{t('settings.other.wsMaxReconnectLabel')}</Label>
<span className="text-sm text-muted-foreground">{wsMaxReconnectAttempts} {t('settings.other.wsMaxReconnectUnit')}</span>
</div>
<Slider
value={[wsMaxReconnectAttempts]}
@@ -328,7 +330,7 @@ export function OtherTab() {
className="w-full"
/>
<p className="text-xs text-muted-foreground">
{t('settings.other.wsMaxReconnectDesc')}
</p>
</div>
@@ -336,27 +338,26 @@ export function OtherTab() {
<div className="flex flex-wrap gap-2 pt-2">
<Button variant="outline" size="sm" onClick={handleClearLogCache} className="gap-2">
<Trash2 className="h-4 w-4" />
{t('settings.other.clearLogCacheFn')}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Trash2 className="h-4 w-4" />
{t('settings.other.clearLocalCache')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t('settings.other.confirmClearCache')}</AlertDialogTitle>
<AlertDialogDescription>
{t('settings.other.confirmClearCacheDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleClearLocalCache}>
{t('settings.other.confirmClear')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -369,11 +370,11 @@ export function OtherTab() {
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4 flex items-center gap-2">
<Download className="h-5 w-5" />
/
{t('settings.other.importExport')}
</h3>
<div className="space-y-4">
<p className="text-xs sm:text-sm text-muted-foreground">
便
{t('settings.other.importExportDesc')}
</p>
<div className="flex flex-wrap gap-2">
@@ -384,7 +385,7 @@ export function OtherTab() {
className="gap-2"
>
<Download className="h-4 w-4" />
{isExporting ? '导出中...' : '导出设置'}
{isExporting ? t('settings.other.exporting') : t('settings.other.exportSettings')}
</Button>
<input
@@ -401,7 +402,7 @@ export function OtherTab() {
className="gap-2"
>
<Upload className="h-4 w-4" />
{isImporting ? '导入中...' : '导入设置'}
{isImporting ? t('settings.other.importing') : t('settings.other.importSettings')}
</Button>
</div>
@@ -411,21 +412,20 @@ export function OtherTab() {
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 text-destructive hover:text-destructive">
<RotateCcw className="h-4 w-4" />
{t('settings.other.resetAllSettingsBtn')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t('settings.other.confirmResetAll')}</AlertDialogTitle>
<AlertDialogDescription>
{t('settings.other.confirmResetAllDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleResetAllSettings}>
{t('settings.other.resetAllSettingsConfirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -436,31 +436,31 @@ export function OtherTab() {
{/* 配置向导 */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"></h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.other.configWizard')}</h3>
<div className="space-y-3 sm:space-y-4">
<div className="space-y-2">
<p className="text-xs sm:text-sm text-muted-foreground">
{t('settings.other.configWizardDesc')}
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" disabled={isResetting} className="gap-2">
<RotateCcw className={cn('h-4 w-4', isResetting && 'animate-spin')} />
{t('settings.other.rerunSetup')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t('settings.other.confirmRerunSetup')}</AlertDialogTitle>
<AlertDialogDescription>
{t('settings.other.confirmRerunSetupDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleResetSetup}>
{t('settings.other.resetAllSettingsConfirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -472,36 +472,35 @@ export function OtherTab() {
<div className="rounded-lg border border-dashed border-yellow-500/50 bg-yellow-500/5 p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4 flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
{t('settings.other.devTools')}
</h3>
<div className="space-y-3 sm:space-y-4">
<div className="space-y-2">
<p className="text-xs sm:text-sm text-muted-foreground">
使
{t('settings.other.devToolsDesc')}
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="gap-2">
<AlertTriangle className="h-4 w-4" />
{t('settings.other.triggerError')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t('settings.other.confirmTriggerError')}</AlertDialogTitle>
<AlertDialogDescription>
React
{t('settings.other.confirmTriggerErrorDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => setShouldThrowError(true)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{t('settings.other.confirmTrigger')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -9,6 +9,7 @@ import {
XCircle,
} from 'lucide-react'
import { useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
@@ -38,6 +39,7 @@ import {
} from '@/components/ui/alert-dialog'
export function SecurityTab() {
const { t } = useTranslation()
const navigate = useNavigate()
const [currentToken, setCurrentToken] = useState('')
const [newToken, setNewToken] = useState('')
@@ -58,8 +60,8 @@ export function SecurityTab() {
const copyToClipboard = async (text: string) => {
if (!currentToken) {
toast({
title: '无法复制',
description: 'Token 存储在安全 Cookie 中,请重新生成以获取新 Token',
title: t('settings.security.cannotCopy'),
description: t('settings.security.cannotCopyDesc'),
variant: 'destructive',
})
return
@@ -68,14 +70,14 @@ export function SecurityTab() {
await navigator.clipboard.writeText(text)
setCopied(true)
toast({
title: '复制成功',
description: 'Token 已复制到剪贴板',
title: t('settings.security.copySuccess'),
description: t('settings.security.copySuccessDesc'),
})
setTimeout(() => setCopied(false), 2000)
} catch {
toast({
title: '复制失败',
description: '请手动复制 Token',
title: t('settings.security.copyFailed'),
description: t('settings.security.copyFailedDesc'),
variant: 'destructive',
})
}
@@ -85,8 +87,8 @@ export function SecurityTab() {
const handleUpdateToken = async () => {
if (!newToken.trim()) {
toast({
title: '输入错误',
description: '请输入新的 Token',
title: t('settings.security.inputError'),
description: t('settings.security.inputErrorDesc'),
variant: 'destructive',
})
return
@@ -100,8 +102,8 @@ export function SecurityTab() {
.join(', ')
toast({
title: '格式错误',
description: `Token 不符合要求: ${failedRules}`,
title: t('settings.security.formatError'),
description: t('settings.security.formatErrorDesc', { failedRules }),
variant: 'destructive',
})
return
@@ -129,8 +131,8 @@ export function SecurityTab() {
setCurrentToken(newToken.trim())
toast({
title: '更新成功',
description: 'Access Token 已更新,即将跳转到登录页',
title: t('settings.security.updateSuccess'),
description: t('settings.security.updateSuccessDesc'),
})
// 延迟跳转到登录页
@@ -139,16 +141,16 @@ export function SecurityTab() {
}, 1500)
} else {
toast({
title: '更新失败',
description: data.message || '无法更新 Token',
title: t('settings.security.updateFailed'),
description: data.message || t('settings.security.updateFailedDesc'),
variant: 'destructive',
})
}
} catch (err) {
console.error('更新 Token 错误:', err)
toast({
title: '更新失败',
description: '连接服务器失败',
title: t('settings.security.updateFailed'),
description: t('settings.security.updateFailedConn'),
variant: 'destructive',
})
} finally {
@@ -181,21 +183,21 @@ export function SecurityTab() {
setTokenCopied(false)
toast({
title: '生成成功',
description: '新的 Access Token 已生成,请及时保存',
title: t('settings.security.generateSuccess'),
description: t('settings.security.generateSuccessDesc'),
})
} else {
toast({
title: '生成失败',
description: data.message || '无法生成新 Token',
title: t('settings.security.generateFailed'),
description: data.message || t('settings.security.generateFailedDesc'),
variant: 'destructive',
})
}
} catch (err) {
console.error('生成 Token 错误:', err)
toast({
title: '生成失败',
description: '连接服务器失败',
title: t('settings.security.generateFailed'),
description: t('settings.security.generateFailedConn'),
variant: 'destructive',
})
} finally {
@@ -209,13 +211,13 @@ export function SecurityTab() {
await navigator.clipboard.writeText(generatedToken)
setTokenCopied(true)
toast({
title: '复制成功',
description: 'Token 已复制到剪贴板',
title: t('settings.security.copySuccess'),
description: t('settings.security.copySuccessDesc'),
})
} catch {
toast({
title: '复制失败',
description: '请手动复制 Token',
title: t('settings.security.copyFailed'),
description: t('settings.security.copyFailedDesc'),
variant: 'destructive',
})
}
@@ -251,10 +253,10 @@ export function SecurityTab() {
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
Access Token
{t('settings.security.dialogTitle')}
</DialogTitle>
<DialogDescription>
Token
{t('settings.security.dialogDesc')}
</DialogDescription>
</DialogHeader>
@@ -262,7 +264,7 @@ export function SecurityTab() {
{/* Token 显示区域 */}
<div className="rounded-lg border-2 border-primary/20 bg-primary/5 p-4">
<Label className="text-xs text-muted-foreground mb-2 block">
Token (64)
{t('settings.security.dialogTokenLabel')}
</Label>
<div className="font-mono text-sm break-all select-all bg-background p-3 rounded border">
{generatedToken}
@@ -274,12 +276,12 @@ export function SecurityTab() {
<div className="flex gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
<div className="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
<p className="font-semibold"></p>
<p className="font-semibold">{t('settings.security.important')}</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li> Token </li>
<li></li>
<li></li>
<li>使 Token </li>
<li>{t('settings.security.tip1')}</li>
<li>{t('settings.security.tip2')}</li>
<li>{t('settings.security.tip3')}</li>
<li>{t('settings.security.tip4')}</li>
</ul>
</div>
</div>
@@ -295,17 +297,17 @@ export function SecurityTab() {
{tokenCopied ? (
<>
<Check className="h-4 w-4 text-green-500" />
{t('settings.security.copied')}
</>
) : (
<>
<Copy className="h-4 w-4" />
Token
{t('settings.security.copyToken')}
</>
)}
</Button>
<Button onClick={handleCloseDialog}>
{t('settings.security.savedClose')}
</Button>
</DialogFooter>
</DialogContent>
@@ -313,10 +315,10 @@ export function SecurityTab() {
{/* 当前 Token */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"> Access Token</h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.security.currentToken')}</h3>
<div className="space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label htmlFor="current-token" className="text-sm">访</Label>
<Label htmlFor="current-token" className="text-sm">{t('settings.security.yourToken')}</Label>
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Input
@@ -325,7 +327,7 @@ export function SecurityTab() {
value={currentToken || '••••••••••••••••••••••••••••••••'}
readOnly
className="pr-10 font-mono text-sm"
placeholder="Token 存储在安全 Cookie 中"
placeholder={t('settings.security.tokenStorePlaceholder')}
/>
<button
onClick={() => {
@@ -333,13 +335,13 @@ export function SecurityTab() {
setShowCurrentToken(!showCurrentToken)
} else {
toast({
title: '无法查看',
description: 'Token 存储在安全 Cookie 中,如需新 Token 请点击"重新生成"',
title: t('settings.security.cannotView'),
description: t('settings.security.cannotViewDesc'),
})
}
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 hover:bg-accent rounded"
title={showCurrentToken ? '隐藏' : '显示'}
title={showCurrentToken ? t('settings.security.hide') : t('settings.security.show')}
>
{showCurrentToken ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
@@ -353,7 +355,7 @@ export function SecurityTab() {
variant="outline"
size="icon"
onClick={() => copyToClipboard(currentToken)}
title="复制到剪贴板"
title={t('settings.security.copyTip')}
className="flex-shrink-0"
disabled={!currentToken}
>
@@ -371,22 +373,21 @@ export function SecurityTab() {
className="gap-2 flex-1 sm:flex-none"
>
<RefreshCw className={cn('h-4 w-4', isRegenerating && 'animate-spin')} />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
<span className="hidden sm:inline">{t('settings.security.regenerate')}</span>
<span className="sm:hidden">{t('settings.security.regenerateShort')}</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> Token</AlertDialogTitle>
<AlertDialogTitle>{t('settings.security.confirmRegenerate')}</AlertDialogTitle>
<AlertDialogDescription>
64 使 Token
使 Token
{t('settings.security.confirmRegenerateFullDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('settings.security.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={executeRegenerateToken}>
{t('settings.security.confirmGenerate')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -394,7 +395,7 @@ export function SecurityTab() {
</div>
</div>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Access Token
{t('settings.security.safekeepTip')}
</p>
</div>
</div>
@@ -402,10 +403,10 @@ export function SecurityTab() {
{/* 更新 Token */}
<div className="rounded-lg border bg-card p-4 sm:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4"> Access Token</h3>
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.security.customToken')}</h3>
<div className="space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label htmlFor="new-token" className="text-sm">访</Label>
<Label htmlFor="new-token" className="text-sm">{t('settings.security.newTokenLabel')}</Label>
<div className="relative">
<Input
id="new-token"
@@ -413,12 +414,12 @@ export function SecurityTab() {
value={newToken}
onChange={(e) => setNewToken(e.target.value)}
className="pr-10 font-mono text-sm"
placeholder="输入自定义 Token"
placeholder={t('settings.security.customTokenPlaceholder')}
/>
<button
onClick={() => setShowNewToken(!showNewToken)}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 hover:bg-accent rounded"
title={showNewToken ? '隐藏' : '显示'}
title={showNewToken ? t('settings.security.hide') : t('settings.security.show')}
>
{showNewToken ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
@@ -431,7 +432,7 @@ export function SecurityTab() {
{/* Token 验证规则显示 */}
{newToken && (
<div className="mt-3 space-y-2 p-3 rounded-lg bg-muted/50">
<p className="text-sm font-medium text-foreground">Token :</p>
<p className="text-sm font-medium text-foreground">{t('settings.security.tokenReqTitle')}</p>
<div className="space-y-1.5">
{tokenValidation.rules.map((rule) => (
<div key={rule.id} className="flex items-center gap-2 text-sm">
@@ -452,7 +453,7 @@ export function SecurityTab() {
<div className="mt-2 pt-2 border-t border-border">
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
<Check className="h-4 w-4" />
<span className="font-medium">Token 使</span>
<span className="font-medium">{t('settings.security.tokenValid')}</span>
</div>
</div>
)}
@@ -464,21 +465,21 @@ export function SecurityTab() {
disabled={isUpdating || !tokenValidation.isValid || !newToken}
className="w-full sm:w-auto"
>
{isUpdating ? '更新中...' : '更新自定义 Token'}
{isUpdating ? t('settings.security.updating') : t('settings.security.updateBtn')}
</Button>
</div>
</div>
{/* 安全提示 */}
<div className="rounded-lg border border-yellow-200 dark:border-yellow-900 bg-yellow-50 dark:bg-yellow-950/30 p-3 sm:p-4">
<h4 className="text-sm sm:text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2"></h4>
<h4 className="text-sm sm:text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">{t('settings.security.securityTip')}</h4>
<ul className="text-xs sm:text-sm text-yellow-800 dark:text-yellow-300 space-y-1 list-disc list-inside">
<li> Token 64 </li>
<li> Token 使</li>
<li> Token Token </li>
<li> Token</li>
<li>怀 Token </li>
<li>使 Token </li>
<li>{t('settings.security.securityTip1')}</li>
<li>{t('settings.security.securityTip2')}</li>
<li>{t('settings.security.securityTip3')}</li>
<li>{t('settings.security.securityTip4')}</li>
<li>{t('settings.security.securityTip5')}</li>
<li>{t('settings.security.securityTip6')}</li>
</ul>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { Info, Palette, Settings, Shield } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
@@ -9,13 +10,14 @@ import { OtherTab } from './OtherTab'
import { SecurityTab } from './SecurityTab'
export function SettingsPage() {
const { t } = useTranslation()
return (
<div className="space-y-4 sm:space-y-6 p-4 sm:p-6">
{/* 页面标题 */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base"></p>
<h1 className="text-2xl sm:text-3xl font-bold">{t('settings.title')}</h1>
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">{t('settings.description')}</p>
</div>
</div>
@@ -24,19 +26,19 @@ export function SettingsPage() {
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-4 gap-0.5 sm:gap-1 h-auto p-1">
<TabsTrigger value="appearance" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
<Palette className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
<span></span>
<span>{t('settings.tabs.appearance')}</span>
</TabsTrigger>
<TabsTrigger value="security" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
<Shield className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
<span></span>
<span>{t('settings.tabs.security')}</span>
</TabsTrigger>
<TabsTrigger value="other" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
<Settings className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
<span></span>
<span>{t('settings.tabs.other')}</span>
</TabsTrigger>
<TabsTrigger value="about" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
<Info className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
<span></span>
<span>{t('settings.tabs.about')}</span>
</TabsTrigger>
</TabsList>