feat: complete i18n support for all frontend pages and components
Add comprehensive bilingual (zh-TW/en-US) support across the entire frontend: Pages updated: - AdminDashboardPage: All 63+ strings translated - TaskHistoryPage: All 80+ strings translated - TaskDetailPage: All 90+ strings translated - AuditLogsPage: All audit log UI translated - ResultsPage/ProcessingPage: Fixed i18n integration - UploadPage: Step indicators and file list UI translated Components updated: - TaskNotFound: Task deletion messages - FileUpload: Prompts and file size limits - ProcessingTrackSelector: Processing mode options with analysis info - Layout: Navigation descriptions - ProtectedRoute: Loading and access denied messages - PDFViewer: Page navigation and error messages Locale files: Added ~200 new translation keys to both zh-TW.json and en-US.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiClientV2 } from '@/services/apiV2'
|
||||
import type { SystemStats, UserWithStats, TopUser, TranslationStats } from '@/types/apiV2'
|
||||
import {
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
export default function AdminDashboardPage() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [stats, setStats] = useState<SystemStats | null>(null)
|
||||
const [users, setUsers] = useState<UserWithStats[]>([])
|
||||
@@ -61,7 +63,7 @@ export default function AdminDashboardPage() {
|
||||
setTranslationStats(translationStatsData)
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch admin data:', err)
|
||||
setError(err.response?.data?.detail || '載入管理員資料失敗')
|
||||
setError(err.response?.data?.detail || t('admin.loadFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -71,11 +73,11 @@ export default function AdminDashboardPage() {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
// Format date
|
||||
// Format date based on current locale
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-TW')
|
||||
return date.toLocaleString(i18n.language === 'zh-TW' ? 'zh-TW' : 'en-US')
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -83,7 +85,7 @@ export default function AdminDashboardPage() {
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-12 h-12 animate-spin text-blue-600 mx-auto mb-4" />
|
||||
<p className="text-gray-600">載入管理員儀表板...</p>
|
||||
<p className="text-gray-600">{t('admin.loadingDashboard')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -95,7 +97,7 @@ export default function AdminDashboardPage() {
|
||||
<div className="flex items-center gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<XCircle className="w-5 h-5 text-red-600" />
|
||||
<div>
|
||||
<p className="text-red-600 font-semibold">載入失敗</p>
|
||||
<p className="text-red-600 font-semibold">{t('errors.loadFailed')}</p>
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,18 +112,18 @@ export default function AdminDashboardPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-8 h-8 text-blue-600" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">管理員儀表板</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('admin.title')}</h1>
|
||||
</div>
|
||||
<p className="text-gray-600 mt-1">系統統計與用戶管理</p>
|
||||
<p className="text-gray-600 mt-1">{t('admin.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => navigate('/admin/audit-logs')} variant="outline">
|
||||
<Activity className="w-4 h-4 mr-2" />
|
||||
審計日誌
|
||||
{t('admin.auditLogs')}
|
||||
</Button>
|
||||
<Button onClick={fetchData} variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,13 +135,13 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
總用戶數
|
||||
{t('admin.totalUsers')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.total_users}</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
活躍: {stats.active_users}
|
||||
{t('admin.activeUsers')}: {stats.active_users}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -148,7 +150,7 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<ClipboardList className="w-4 h-4" />
|
||||
總任務數
|
||||
{t('admin.totalTasks')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -160,7 +162,7 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
待處理
|
||||
{t('admin.pendingTasks')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -174,7 +176,7 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4" />
|
||||
處理中
|
||||
{t('admin.processingTasks')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -188,7 +190,7 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
已完成
|
||||
{t('admin.completedTasks')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -196,7 +198,7 @@ export default function AdminDashboardPage() {
|
||||
{stats.task_stats.completed}
|
||||
</div>
|
||||
<p className="text-xs text-red-600 mt-1">
|
||||
失敗: {stats.task_stats.failed}
|
||||
{t('admin.failedTasks')}: {stats.task_stats.failed}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -209,42 +211,42 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Languages className="w-5 h-5" />
|
||||
翻譯統計
|
||||
{t('admin.translationStats.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>翻譯 API 使用量與計費追蹤</CardDescription>
|
||||
<CardDescription>{t('admin.translationStats.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-blue-600 mb-1">
|
||||
<Languages className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">總翻譯次數</span>
|
||||
<span className="text-sm font-medium">{t('admin.translationStats.totalTranslations')}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-700">
|
||||
{translationStats.total_translations.toLocaleString()}
|
||||
</div>
|
||||
<p className="text-xs text-blue-500 mt-1">
|
||||
近30天: {translationStats.last_30_days.count}
|
||||
{t('admin.translationStats.last30Days')}: {translationStats.last_30_days.count}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-purple-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-purple-600 mb-1">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">總 Token 數</span>
|
||||
<span className="text-sm font-medium">{t('admin.translationStats.totalTokens')}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-purple-700">
|
||||
{translationStats.total_tokens.toLocaleString()}
|
||||
</div>
|
||||
<p className="text-xs text-purple-500 mt-1">
|
||||
近30天: {translationStats.last_30_days.tokens.toLocaleString()}
|
||||
{t('admin.translationStats.last30Days')}: {translationStats.last_30_days.tokens.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-green-600 mb-1">
|
||||
<ClipboardList className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">總字元數</span>
|
||||
<span className="text-sm font-medium">{t('admin.translationStats.totalCharacters')}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-700">
|
||||
{translationStats.total_characters.toLocaleString()}
|
||||
@@ -254,7 +256,7 @@ export default function AdminDashboardPage() {
|
||||
<div className="p-4 bg-amber-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-amber-600 mb-1">
|
||||
<Coins className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">預估成本</span>
|
||||
<span className="text-sm font-medium">{t('admin.translationStats.estimatedCost')}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-amber-700">
|
||||
${translationStats.estimated_cost.toFixed(2)}
|
||||
@@ -266,11 +268,11 @@ export default function AdminDashboardPage() {
|
||||
{/* Language Breakdown */}
|
||||
{translationStats.by_language.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">語言分佈</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">{t('admin.translationStats.languageBreakdown')}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{translationStats.by_language.map((lang) => (
|
||||
<Badge key={lang.language} variant="outline" className="px-3 py-1">
|
||||
{lang.language}: {lang.count} 次 ({lang.tokens.toLocaleString()} tokens)
|
||||
{lang.language}: {lang.count} {t('admin.translationStats.count')} ({lang.tokens.toLocaleString()} {t('admin.translationStats.tokens')})
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -280,42 +282,42 @@ export default function AdminDashboardPage() {
|
||||
{/* Recent Translations */}
|
||||
{translationStats.recent_translations.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">最近翻譯記錄</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">{t('admin.translationStats.recentTranslations')}</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>任務 ID</TableHead>
|
||||
<TableHead>目標語言</TableHead>
|
||||
<TableHead className="text-right">Token 數</TableHead>
|
||||
<TableHead className="text-right">字元數</TableHead>
|
||||
<TableHead className="text-right">成本</TableHead>
|
||||
<TableHead className="text-right">處理時間</TableHead>
|
||||
<TableHead>時間</TableHead>
|
||||
<TableHead>{t('admin.table.taskId')}</TableHead>
|
||||
<TableHead>{t('admin.table.targetLang')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.table.tokenCount')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.table.charCount')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.table.cost')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.table.processingTime')}</TableHead>
|
||||
<TableHead>{t('admin.table.time')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{translationStats.recent_translations.slice(0, 10).map((t) => (
|
||||
<TableRow key={t.id}>
|
||||
{translationStats.recent_translations.slice(0, 10).map((tr) => (
|
||||
<TableRow key={tr.id}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{t.task_id.substring(0, 8)}...
|
||||
{tr.task_id.substring(0, 8)}...
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{t.target_lang}</Badge>
|
||||
<Badge variant="secondary">{tr.target_lang}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{t.total_tokens.toLocaleString()}
|
||||
{tr.total_tokens.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{t.total_characters.toLocaleString()}
|
||||
{tr.total_characters.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium text-amber-600">
|
||||
${t.estimated_cost.toFixed(4)}
|
||||
${tr.estimated_cost.toFixed(4)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{t.processing_time_seconds.toFixed(1)}s
|
||||
{tr.processing_time_seconds.toFixed(1)}s
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">
|
||||
{new Date(t.created_at).toLocaleString('zh-TW')}
|
||||
{formatDate(tr.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -333,9 +335,9 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
活躍用戶排行
|
||||
{t('admin.topUsers.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>任務數量最多的用戶</CardDescription>
|
||||
<CardDescription>{t('admin.topUsers.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
@@ -343,9 +345,9 @@ export default function AdminDashboardPage() {
|
||||
<TableRow>
|
||||
<TableHead className="w-12">#</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>顯示名稱</TableHead>
|
||||
<TableHead className="text-right">總任務</TableHead>
|
||||
<TableHead className="text-right">已完成</TableHead>
|
||||
<TableHead>{t('admin.topUsers.displayName')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.topUsers.totalTasks')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.topUsers.completedTasks')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -377,26 +379,26 @@ export default function AdminDashboardPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
最近用戶
|
||||
{t('admin.recentUsers.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>最新註冊的用戶列表</CardDescription>
|
||||
<CardDescription>{t('admin.recentUsers.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{users.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>暫無用戶</p>
|
||||
<p>{t('admin.recentUsers.noUsers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>顯示名稱</TableHead>
|
||||
<TableHead>註冊時間</TableHead>
|
||||
<TableHead>最後登入</TableHead>
|
||||
<TableHead>狀態</TableHead>
|
||||
<TableHead className="text-right">任務數</TableHead>
|
||||
<TableHead>{t('admin.recentUsers.displayName')}</TableHead>
|
||||
<TableHead>{t('admin.recentUsers.registeredAt')}</TableHead>
|
||||
<TableHead>{t('admin.recentUsers.lastLogin')}</TableHead>
|
||||
<TableHead>{t('admin.recentUsers.status')}</TableHead>
|
||||
<TableHead className="text-right">{t('admin.recentUsers.taskCount')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -412,14 +414,14 @@ export default function AdminDashboardPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.is_active ? 'default' : 'secondary'}>
|
||||
{user.is_active ? '活躍' : '停用'}
|
||||
{user.is_active ? t('common.active') : t('common.inactive')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div>
|
||||
<div className="font-semibold">{user.task_count}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
完成: {user.completed_tasks} | 失敗: {user.failed_tasks}
|
||||
{t('admin.recentUsers.completedCount')}: {user.completed_tasks} | {t('admin.recentUsers.failedCount')}: {user.failed_tasks}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user