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:
@@ -56,7 +56,7 @@ const LANGUAGE_OPTIONS = [
|
||||
|
||||
export default function TaskDetailPage() {
|
||||
const { taskId } = useParams<{ taskId: string }>()
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { toast } = useToast()
|
||||
|
||||
@@ -124,16 +124,16 @@ export default function TaskDetailPage() {
|
||||
setIsTranslating(false)
|
||||
setTranslationProgress(100)
|
||||
toast({
|
||||
title: '翻譯完成',
|
||||
description: `文件已翻譯為 ${LANGUAGE_OPTIONS.find(l => l.value === targetLang)?.label || targetLang}`,
|
||||
title: t('translation.translationComplete'),
|
||||
description: `${LANGUAGE_OPTIONS.find(l => l.value === targetLang)?.label || targetLang}`,
|
||||
variant: 'success',
|
||||
})
|
||||
refetchTranslations()
|
||||
} else if (status.status === 'failed') {
|
||||
setIsTranslating(false)
|
||||
toast({
|
||||
title: '翻譯失敗',
|
||||
description: status.error_message || '未知錯誤',
|
||||
title: t('translation.translationFailed'),
|
||||
description: status.error_message || t('common.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export default function TaskDetailPage() {
|
||||
}, 2000)
|
||||
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [isTranslating, taskId, targetLang, toast, refetchTranslations])
|
||||
}, [isTranslating, taskId, targetLang, toast, refetchTranslations, t])
|
||||
|
||||
// Construct PDF URL for preview - memoize to prevent unnecessary reloads
|
||||
// Must be called unconditionally before any early returns (React hooks rule)
|
||||
@@ -162,24 +162,24 @@ export default function TaskDetailPage() {
|
||||
if (!track) return null
|
||||
switch (track) {
|
||||
case 'direct':
|
||||
return <Badge variant="default" className="bg-blue-600">直接提取</Badge>
|
||||
return <Badge variant="default" className="bg-blue-600">{t('taskDetail.track.direct')}</Badge>
|
||||
case 'ocr':
|
||||
return <Badge variant="default" className="bg-purple-600">OCR</Badge>
|
||||
case 'hybrid':
|
||||
return <Badge variant="default" className="bg-orange-600">混合</Badge>
|
||||
return <Badge variant="default" className="bg-orange-600">{t('taskDetail.track.hybrid')}</Badge>
|
||||
default:
|
||||
return <Badge variant="secondary">自動</Badge>
|
||||
return <Badge variant="secondary">{t('taskDetail.track.auto')}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getTrackDescription = (track?: ProcessingTrack) => {
|
||||
switch (track) {
|
||||
case 'direct':
|
||||
return 'PyMuPDF 直接提取'
|
||||
return t('taskDetail.track.directDesc')
|
||||
case 'ocr':
|
||||
return 'PP-StructureV3 OCR'
|
||||
case 'hybrid':
|
||||
return '混合處理'
|
||||
return t('taskDetail.track.hybridDesc')
|
||||
default:
|
||||
return 'OCR'
|
||||
}
|
||||
@@ -191,7 +191,7 @@ export default function TaskDetailPage() {
|
||||
await apiClientV2.downloadPDF(taskId, 'layout')
|
||||
toast({
|
||||
title: t('export.exportSuccess'),
|
||||
description: '版面 PDF 已下載',
|
||||
description: t('taskDetail.layoutPdf'),
|
||||
variant: 'success',
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -209,7 +209,7 @@ export default function TaskDetailPage() {
|
||||
await apiClientV2.downloadPDF(taskId, 'reflow')
|
||||
toast({
|
||||
title: t('export.exportSuccess'),
|
||||
description: '流式 PDF 已下載',
|
||||
description: t('taskDetail.reflowPdf'),
|
||||
variant: 'success',
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -239,7 +239,7 @@ export default function TaskDetailPage() {
|
||||
setIsTranslating(false)
|
||||
setTranslationProgress(100)
|
||||
toast({
|
||||
title: '翻譯已存在',
|
||||
title: t('translation.translationExists'),
|
||||
description: response.message,
|
||||
variant: 'success',
|
||||
})
|
||||
@@ -247,15 +247,15 @@ export default function TaskDetailPage() {
|
||||
} else {
|
||||
setTranslationStatus(response.status)
|
||||
toast({
|
||||
title: '開始翻譯',
|
||||
description: '翻譯任務已啟動,請稍候...',
|
||||
title: t('translation.translationStarted'),
|
||||
description: t('translation.translationStartedDesc'),
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
setIsTranslating(false)
|
||||
setTranslationStatus(null)
|
||||
toast({
|
||||
title: '啟動翻譯失敗',
|
||||
title: t('errors.startFailed'),
|
||||
description: error.response?.data?.detail || t('errors.networkError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
@@ -267,14 +267,14 @@ export default function TaskDetailPage() {
|
||||
try {
|
||||
await apiClientV2.deleteTranslation(taskId, lang)
|
||||
toast({
|
||||
title: '刪除成功',
|
||||
description: `翻譯結果 (${lang}) 已刪除`,
|
||||
title: t('translation.deleteSuccess'),
|
||||
description: t('translation.translationDeleted', { lang }),
|
||||
variant: 'success',
|
||||
})
|
||||
refetchTranslations()
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: '刪除失敗',
|
||||
title: t('errors.deleteFailed'),
|
||||
description: error.response?.data?.detail || t('errors.networkError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
@@ -285,15 +285,15 @@ export default function TaskDetailPage() {
|
||||
if (!taskId) return
|
||||
try {
|
||||
await apiClientV2.downloadTranslatedPdf(taskId, lang, format)
|
||||
const formatLabel = format === 'layout' ? '版面' : '流式'
|
||||
const formatLabel = format === 'layout' ? t('taskDetail.layoutPdf') : t('taskDetail.reflowPdf')
|
||||
toast({
|
||||
title: '下載成功',
|
||||
description: `翻譯 ${formatLabel} PDF (${lang}) 已下載`,
|
||||
title: t('common.downloadSuccess'),
|
||||
description: `${formatLabel} (${lang})`,
|
||||
variant: 'success',
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: '下載失敗',
|
||||
title: t('common.downloadFailed'),
|
||||
description: error.response?.data?.detail || t('errors.networkError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
@@ -305,13 +305,13 @@ export default function TaskDetailPage() {
|
||||
try {
|
||||
await apiClientV2.downloadVisualization(taskId)
|
||||
toast({
|
||||
title: '下載成功',
|
||||
description: '辨識結果圖片已下載',
|
||||
title: t('common.downloadSuccess'),
|
||||
description: t('taskDetail.visualizationDownloaded'),
|
||||
variant: 'success',
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: '下載失敗',
|
||||
title: t('common.downloadFailed'),
|
||||
description: error.response?.data?.detail || t('errors.networkError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
@@ -321,28 +321,28 @@ export default function TaskDetailPage() {
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Badge variant="default" className="bg-green-600">已完成</Badge>
|
||||
return <Badge variant="default" className="bg-green-600">{t('taskDetail.status.completed')}</Badge>
|
||||
case 'processing':
|
||||
return <Badge variant="default">處理中</Badge>
|
||||
return <Badge variant="default">{t('taskDetail.status.processing')}</Badge>
|
||||
case 'failed':
|
||||
return <Badge variant="destructive">失敗</Badge>
|
||||
return <Badge variant="destructive">{t('taskDetail.status.failed')}</Badge>
|
||||
default:
|
||||
return <Badge variant="secondary">待處理</Badge>
|
||||
return <Badge variant="secondary">{t('taskDetail.status.pending')}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getTranslationStatusText = (status: TranslationStatus | null) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '準備中...'
|
||||
return t('translation.status.preparing')
|
||||
case 'loading_model':
|
||||
return '載入翻譯模型...'
|
||||
return t('translation.status.loadingModel')
|
||||
case 'translating':
|
||||
return '翻譯中...'
|
||||
return t('translation.status.translating')
|
||||
case 'completed':
|
||||
return '完成'
|
||||
return t('translation.status.complete')
|
||||
case 'failed':
|
||||
return '失敗'
|
||||
return t('taskDetail.status.failed')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
@@ -350,7 +350,7 @@ export default function TaskDetailPage() {
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-TW')
|
||||
return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-TW')
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -358,7 +358,7 @@ export default function TaskDetailPage() {
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">載入任務詳情...</p>
|
||||
<p className="text-muted-foreground">{t('taskDetail.loadingTask')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -372,12 +372,12 @@ export default function TaskDetailPage() {
|
||||
<div className="flex justify-center mb-4">
|
||||
<AlertCircle className="w-16 h-16 text-destructive" />
|
||||
</div>
|
||||
<CardTitle>任務不存在</CardTitle>
|
||||
<CardTitle>{t('taskDetail.taskNotFound')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground">找不到任務 ID: {taskId}</p>
|
||||
<p className="text-muted-foreground">{t('taskDetail.taskNotFoundDesc', { id: taskId })}</p>
|
||||
<Button onClick={() => navigate('/tasks')}>
|
||||
返回任務歷史
|
||||
{t('taskDetail.returnToHistory')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -397,19 +397,19 @@ export default function TaskDetailPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" onClick={() => navigate('/tasks')} className="gap-2">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="page-title">任務詳情</h1>
|
||||
<h1 className="page-title">{t('taskDetail.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
任務 ID: <span className="font-mono text-primary">{taskId}</span>
|
||||
{t('taskDetail.taskId', { id: taskId })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm" className="gap-2">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
{getStatusBadge(taskDetail.status)}
|
||||
</div>
|
||||
@@ -421,35 +421,35 @@ export default function TaskDetailPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
任務資訊
|
||||
{t('taskDetail.taskInfo')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">檔案名稱</p>
|
||||
<p className="font-medium">{taskDetail.filename || '未知檔案'}</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.filename')}</p>
|
||||
<p className="font-medium">{taskDetail.filename || t('common.unknownFile')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">建立時間</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.createdAt')}</p>
|
||||
<p className="font-medium">{formatDate(taskDetail.created_at)}</p>
|
||||
</div>
|
||||
{taskDetail.completed_at && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">完成時間</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.completedAt')}</p>
|
||||
<p className="font-medium">{formatDate(taskDetail.completed_at)}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">任務狀態</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.taskStatus')}</p>
|
||||
{getStatusBadge(taskDetail.status)}
|
||||
</div>
|
||||
{(taskDetail.processing_track || processingMetadata?.processing_track) && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">處理軌道</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.processingTrack')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{getTrackBadge(taskDetail.processing_track || processingMetadata?.processing_track)}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
@@ -460,13 +460,13 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
{taskDetail.processing_time_ms && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">處理時間</p>
|
||||
<p className="font-medium">{(taskDetail.processing_time_ms / 1000).toFixed(2)} 秒</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.processingTime')}</p>
|
||||
<p className="font-medium">{(taskDetail.processing_time_ms / 1000).toFixed(2)} {t('common.seconds')}</p>
|
||||
</div>
|
||||
)}
|
||||
{taskDetail.updated_at && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">最後更新</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.lastUpdated')}</p>
|
||||
<p className="font-medium">{formatDate(taskDetail.updated_at)}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -481,18 +481,18 @@ export default function TaskDetailPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Download className="w-5 h-5" />
|
||||
下載結果
|
||||
{t('taskDetail.downloadResults')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button onClick={handleDownloadLayoutPDF} className="gap-2 h-20 flex-col">
|
||||
<Download className="w-8 h-8" />
|
||||
<span>版面 PDF</span>
|
||||
<span>{t('taskDetail.layoutPdf')}</span>
|
||||
</Button>
|
||||
<Button onClick={handleDownloadReflowPDF} variant="outline" className="gap-2 h-20 flex-col">
|
||||
<Download className="w-8 h-8" />
|
||||
<span>流式 PDF</span>
|
||||
<span>{t('taskDetail.reflowPdf')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Visualization download for OCR Track */}
|
||||
@@ -504,7 +504,7 @@ export default function TaskDetailPage() {
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<Image className="w-4 h-4" />
|
||||
下載辨識結果圖片 (ZIP)
|
||||
{t('taskDetail.downloadVisualization')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -518,7 +518,7 @@ export default function TaskDetailPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Languages className="w-5 h-5" />
|
||||
文件翻譯
|
||||
{t('translation.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -526,14 +526,14 @@ export default function TaskDetailPage() {
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">目標語言:</span>
|
||||
<span className="text-sm text-muted-foreground">{t('translation.targetLanguage')}</span>
|
||||
<Select
|
||||
value={targetLang}
|
||||
onValueChange={setTargetLang}
|
||||
disabled={isTranslating}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="選擇語言" />
|
||||
<SelectValue placeholder={t('translation.selectLanguage')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGE_OPTIONS.map(lang => (
|
||||
@@ -554,7 +554,7 @@ export default function TaskDetailPage() {
|
||||
) : (
|
||||
<Languages className="w-4 h-4" />
|
||||
)}
|
||||
{isTranslating ? getTranslationStatusText(translationStatus) : '開始翻譯'}
|
||||
{isTranslating ? getTranslationStatusText(translationStatus) : t('translation.startTranslation')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -572,7 +572,7 @@ export default function TaskDetailPage() {
|
||||
{/* Existing Translations */}
|
||||
{translationList && translationList.translations.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">已完成的翻譯:</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('translation.completedTranslations')}</p>
|
||||
<div className="space-y-2">
|
||||
{translationList.translations.map((item: TranslationListItem) => (
|
||||
<div
|
||||
@@ -586,7 +586,7 @@ export default function TaskDetailPage() {
|
||||
{LANGUAGE_OPTIONS.find(l => l.value === item.target_lang)?.label || item.target_lang}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
({item.statistics.translated_elements} 元素, {item.statistics.processing_time_seconds.toFixed(1)}s)
|
||||
({item.statistics.translated_elements} elements, {item.statistics.processing_time_seconds.toFixed(1)}s)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -598,7 +598,7 @@ export default function TaskDetailPage() {
|
||||
className="gap-1"
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
流式 PDF
|
||||
{t('taskDetail.reflowPdf')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -616,7 +616,7 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
使用雲端翻譯服務進行多語言翻譯,支援多種目標語言。
|
||||
{t('translation.description')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -628,7 +628,7 @@ export default function TaskDetailPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
錯誤訊息
|
||||
{t('taskDetail.errorMessage')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -642,8 +642,8 @@ export default function TaskDetailPage() {
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<Loader2 className="w-16 h-16 animate-spin text-primary mx-auto mb-4" />
|
||||
<p className="text-lg font-semibold">正在處理中...</p>
|
||||
<p className="text-muted-foreground mt-2">請稍候,OCR 處理需要一些時間</p>
|
||||
<p className="text-lg font-semibold">{t('taskDetail.processingInProgress')}</p>
|
||||
<p className="text-muted-foreground mt-2">{t('taskDetail.processingInProgressDesc')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -658,7 +658,7 @@ export default function TaskDetailPage() {
|
||||
<Clock className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">處理時間</p>
|
||||
<p className="text-xs text-muted-foreground">{t('taskDetail.processingTime')}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{processingMetadata?.processing_time_seconds?.toFixed(2) ||
|
||||
(taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0')}s
|
||||
@@ -675,7 +675,7 @@ export default function TaskDetailPage() {
|
||||
<Layers className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">頁數</p>
|
||||
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.pageCount')}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{processingMetadata?.page_count || '-'}
|
||||
</p>
|
||||
@@ -691,7 +691,7 @@ export default function TaskDetailPage() {
|
||||
<FileSearch className="w-5 h-5 text-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">文本區域</p>
|
||||
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.textRegions')}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{processingMetadata?.total_text_regions || '-'}
|
||||
</p>
|
||||
@@ -707,7 +707,7 @@ export default function TaskDetailPage() {
|
||||
<Table2 className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">表格</p>
|
||||
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.tables')}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{processingMetadata?.total_tables || '-'}
|
||||
</p>
|
||||
@@ -723,7 +723,7 @@ export default function TaskDetailPage() {
|
||||
<Image className="w-5 h-5 text-orange-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">圖片</p>
|
||||
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.images')}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{processingMetadata?.total_images || '-'}
|
||||
</p>
|
||||
@@ -739,7 +739,7 @@ export default function TaskDetailPage() {
|
||||
<BarChart3 className="w-5 h-5 text-cyan-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">平均置信度</p>
|
||||
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.avgConfidence')}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{processingMetadata?.average_confidence
|
||||
? `${(processingMetadata.average_confidence * 100).toFixed(0)}%`
|
||||
@@ -755,7 +755,7 @@ export default function TaskDetailPage() {
|
||||
{/* Result Preview */}
|
||||
{isCompleted && (
|
||||
<PDFViewer
|
||||
title={`OCR 結果預覽 - ${taskDetail.filename || '未知檔案'}`}
|
||||
title={t('taskDetail.ocrPreview', { filename: taskDetail.filename || t('common.unknownFile') })}
|
||||
pdfUrl={pdfUrl}
|
||||
httpHeaders={pdfHttpHeaders}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user