From 81a0a3ab0f76c2770c743da06820e48539c3674f Mon Sep 17 00:00:00 2001 From: egg Date: Sun, 14 Dec 2025 11:56:18 +0800 Subject: [PATCH] feat: complete i18n support for all frontend pages and components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/components/FileUpload.tsx | 4 +- frontend/src/components/Layout.tsx | 10 +- frontend/src/components/PDFViewer.tsx | 14 +- .../components/ProcessingTrackSelector.tsx | 34 +- frontend/src/components/ProtectedRoute.tsx | 10 +- frontend/src/components/TaskNotFound.tsx | 10 +- frontend/src/i18n/locales/en-US.json | 385 +++++++++++++++++- frontend/src/i18n/locales/zh-TW.json | 385 +++++++++++++++++- frontend/src/pages/AdminDashboardPage.tsx | 118 +++--- frontend/src/pages/AuditLogsPage.tsx | 95 +++-- frontend/src/pages/ProcessingPage.tsx | 38 +- frontend/src/pages/ResultsPage.tsx | 48 +-- frontend/src/pages/TaskDetailPage.tsx | 154 +++---- frontend/src/pages/TaskHistoryPage.tsx | 125 +++--- frontend/src/pages/UploadPage.tsx | 32 +- 15 files changed, 1111 insertions(+), 351 deletions(-) diff --git a/frontend/src/components/FileUpload.tsx b/frontend/src/components/FileUpload.tsx index 501f3b1..a4ad9f0 100644 --- a/frontend/src/components/FileUpload.tsx +++ b/frontend/src/components/FileUpload.tsx @@ -110,7 +110,7 @@ export default function FileUpload({ {t('upload.dragAndDrop')}

- 或點擊選擇檔案 + {t('upload.orClickToSelect')}

{/* Supported formats */} @@ -132,7 +132,7 @@ export default function FileUpload({

- 最大檔案大小: 50MB · 最多 {maxFiles} 個檔案 + {t('upload.maxFileSizeWithCount', { maxFiles })}

)} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 8bdd68d..80ccde0 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -35,11 +35,11 @@ export default function Layout() { } const navLinks = [ - { to: '/upload', label: t('nav.upload'), icon: Upload, description: '上傳檔案', adminOnly: false }, - { to: '/processing', label: t('nav.processing'), icon: Activity, description: '處理進度', adminOnly: false }, - { to: '/results', label: t('nav.results'), icon: FileText, description: '查看結果', adminOnly: false }, - { to: '/tasks', label: '任務歷史', icon: History, description: '查看任務記錄', adminOnly: false }, - { to: '/admin', label: '管理員儀表板', icon: Shield, description: '系統管理', adminOnly: true }, + { to: '/upload', label: t('nav.upload'), icon: Upload, description: t('upload.title'), adminOnly: false }, + { to: '/processing', label: t('nav.processing'), icon: Activity, description: t('processing.title'), adminOnly: false }, + { to: '/results', label: t('nav.results'), icon: FileText, description: t('results.title'), adminOnly: false }, + { to: '/tasks', label: t('nav.taskHistory'), icon: History, description: t('taskHistory.subtitle'), adminOnly: false }, + { to: '/admin', label: t('nav.adminDashboard'), icon: Shield, description: t('admin.subtitle'), adminOnly: true }, ] // Filter nav links based on admin status diff --git a/frontend/src/components/PDFViewer.tsx b/frontend/src/components/PDFViewer.tsx index 538e215..bc781cd 100644 --- a/frontend/src/components/PDFViewer.tsx +++ b/frontend/src/components/PDFViewer.tsx @@ -1,5 +1,6 @@ import { useState, useCallback, useMemo, useRef, useEffect } from 'react' import { Document, Page, pdfjs } from 'react-pdf' +import { useTranslation } from 'react-i18next' // Type alias for PDFDocumentProxy to avoid direct pdfjs-dist import issues type PDFDocumentProxy = ReturnType extends Promise ? T : never @@ -22,6 +23,7 @@ interface PDFViewerProps { } export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDFViewerProps) { + const { t } = useTranslation() const [numPages, setNumPages] = useState(0) const [pageNumber, setPageNumber] = useState(1) const [scale, setScale] = useState(1.0) @@ -55,10 +57,10 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF const onDocumentLoadError = useCallback((err: Error) => { console.error('Error loading PDF:', err) - setError('無法載入 PDF 檔案。請稍後再試。') + setError(t('pdfViewer.loadError')) setDocumentLoaded(false) pdfDocRef.current = null - }, []) + }, [t]) const goToPreviousPage = useCallback(() => { setPageNumber((prev) => Math.max(prev - 1, 1)) @@ -97,7 +99,7 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF - 第 {pageNumber} 頁 / 共 {numPages || '...'} 頁 + {t('pdfViewer.pageInfo', { current: pageNumber, total: numPages || '...' })} diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index ce7d35e..8709920 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -11,7 +11,8 @@ "settings": "Settings", "logout": "Logout", "taskHistory": "Task History", - "adminDashboard": "Admin Dashboard" + "adminDashboard": "Admin Dashboard", + "auditLogs": "Audit Logs" }, "auth": { "login": "Login", @@ -24,15 +25,19 @@ "loggingIn": "Signing in...", "usernamePlaceholder": "Enter your username", "passwordPlaceholder": "Enter your password", - "supportedFormats": "Supported formats: PDF, Images, Office documents" + "supportedFormats": "Supported formats: PDF, Images, Office documents", + "sessionExpired": "Session expired. Please login again.", + "redirecting": "Redirecting to login page..." }, "upload": { "title": "Upload Files", + "subtitle": "Select files for OCR processing. Supports images, PDFs and Office documents", "dragAndDrop": "Drag and drop files here, or click to select", "dropFilesHere": "Drop files here to upload", "invalidFiles": "Some file formats are not supported", "supportedFormats": "Supported formats: PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX", "maxFileSize": "Maximum file size: 50MB", + "maxFileSizeWithCount": "Maximum file size: 50MB · Up to {{maxFiles}} files", "uploadButton": "Start Upload", "uploading": "Uploading...", "uploadSuccess": "Upload successful", @@ -41,7 +46,30 @@ "clearAll": "Clear All", "removeFile": "Remove", "selectedFiles": "Selected Files", - "filesUploaded": "Successfully uploaded {{count}} file(s)" + "filesUploaded": "Successfully uploaded {{count}} file(s)", + "orClickToSelect": "or click to select files", + "selectFilesButton": "Select Files", + "continueToProcess": "Continue to Process", + "goToProcessing": "Go to Processing", + "uploadMoreFiles": "Upload More Files", + "selectAtLeastOne": "Please select at least one file", + "steps": { + "selectFiles": "Select Files", + "selectFilesDesc": "Upload files to process", + "confirmUpload": "Confirm Upload", + "confirmUploadDesc": "Review and start processing", + "processingComplete": "Processing Complete", + "processingCompleteDesc": "View results and export" + }, + "fileList": { + "summary": "{{count}} file(s) selected, total size {{size}}", + "unknownType": "Unknown type", + "ready": "Ready", + "removeFile": "Remove file", + "confirmPrompt": "Please confirm files are correct before clicking upload", + "cancel": "Cancel", + "startUpload": "Start Upload" + } }, "processing": { "title": "OCR Processing", @@ -55,6 +83,11 @@ "failed": "Failed", "pending": "Pending", "estimatedTime": "Estimated Time Remaining", + "ocrStarted": "OCR processing started", + "loadingTask": "Loading task information...", + "analyzingDocument": "Analyzing document type...", + "noBatchMessage": "No task selected. Please upload files first.", + "goToUpload": "Go to Upload Page", "settings": { "title": "Processing Settings", "language": "Recognition Language", @@ -151,9 +184,23 @@ "viewJSON": "View JSON", "downloadPDF": "Download PDF", "preview": "Preview", + "noBatchMessage": "No task selected. Please upload and process files first.", + "goToUpload": "Go to Upload Page", "noResults": "No results yet", "textBlocks": "Text Blocks", - "layoutInfo": "Layout Info" + "layoutInfo": "Layout Info", + "pdfDownloaded": "PDF downloaded", + "markdownDownloaded": "Markdown downloaded", + "jsonDownloaded": "JSON downloaded", + "loadingResults": "Loading task results...", + "processingStatus": "Processing Status", + "taskType": "Task Type", + "processingInProgress": "Processing in progress...", + "processingInProgressDesc": "Please wait, OCR processing takes some time", + "waitingToProcess": "Waiting to process", + "waitingToProcessDesc": "Go to the processing page to start OCR", + "goToProcessing": "Go to Processing", + "viewTaskHistory": "View Task History" }, "export": { "title": "Export Results", @@ -217,7 +264,37 @@ "back": "Back", "next": "Next", "previous": "Previous", - "submit": "Submit" + "submit": "Submit", + "retry": "Retry", + "viewDetails": "View Details", + "unknownFile": "Unknown file", + "unknownError": "Unknown error", + "seconds": "seconds", + "total": "Total", + "active": "Active", + "inactive": "Inactive", + "all": "All", + "none": "None", + "enabled": "Enabled", + "disabled": "Disabled", + "yes": "Yes", + "no": "No", + "or": "or", + "and": "and", + "show": "Show", + "hide": "Hide", + "more": "More", + "less": "Less", + "downloadFailed": "Download failed", + "downloadSuccess": "Download successful", + "taskDeleted": "Task Deleted", + "taskDeletedDesc": "This task has been deleted or does not exist. Please upload a new file to create a new task.", + "taskIdLabel": "Task ID: {{id}}", + "goToUpload": "Go to Upload Page", + "verifying": "Verifying...", + "accessDenied": "Access Denied", + "accessDeniedDesc": "You do not have permission to access this page", + "backToHome": "Back to Home" }, "errors": { "networkError": "Network error. Please try again later.", @@ -229,12 +306,54 @@ "unsupportedFormat": "Unsupported format", "uploadFailed": "Upload failed", "processingFailed": "Processing failed", - "exportFailed": "Export failed" + "exportFailed": "Export failed", + "loadFailed": "Load failed", + "deleteFailed": "Delete failed", + "startFailed": "Start failed", + "cancelFailed": "Cancel failed", + "retryFailed": "Retry failed" }, "translation": { - "title": "Translation", + "title": "Document Translation", "comingSoon": "Coming Soon", - "description": "Document translation feature is under development" + "description": "Translate documents using cloud translation services, supporting multiple target languages.", + "targetLanguage": "Target Language", + "selectLanguage": "Select Language", + "startTranslation": "Start Translation", + "translating": "Translating...", + "translationComplete": "Translation complete", + "translationFailed": "Translation failed", + "translationExists": "Translation already exists", + "translationStarted": "Translation started", + "translationStartedDesc": "Translation task started, please wait...", + "completedTranslations": "Completed Translations", + "deleteTranslation": "Delete Translation", + "deleteSuccess": "Delete successful", + "translationDeleted": "Translation ({{lang}}) deleted", + "downloadTranslatedPdf": "Translated {{format}} PDF ({{lang}}) downloaded", + "status": { + "preparing": "Preparing...", + "loadingModel": "Loading translation model...", + "translating": "Translating...", + "complete": "Complete", + "failed": "Failed" + }, + "stats": "{{elements}} elements, {{time}}s", + "languages": { + "en": "English", + "ja": "日本語", + "ko": "한국어", + "zh-TW": "繁體中文", + "zh-CN": "简体中文", + "de": "Deutsch", + "fr": "Français", + "es": "Español", + "pt": "Português", + "it": "Italiano", + "ru": "Русский", + "vi": "Tiếng Việt", + "th": "ภาษาไทย" + } }, "batch": { "title": "Batch Processing", @@ -267,5 +386,255 @@ "processingError": "Batch Processing Error", "processingCancelled": "Batch Processing Cancelled", "concurrencyInfo": "Direct Track: max 5 parallel, OCR Track: sequential (GPU limitation)" + }, + "admin": { + "title": "Admin Dashboard", + "subtitle": "System Statistics and User Management", + "loadingDashboard": "Loading admin dashboard...", + "loadFailed": "Failed to load admin data", + "auditLogs": "Audit Logs", + "totalUsers": "Total Users", + "totalTasks": "Total Tasks", + "pendingTasks": "Pending", + "processingTasks": "Processing", + "completedTasks": "Completed", + "failedTasks": "Failed", + "activeUsers": "Active", + "translationStats": { + "title": "Translation Statistics", + "description": "Translation API usage and billing tracking", + "totalTranslations": "Total Translations", + "totalTokens": "Total Tokens", + "totalCharacters": "Total Characters", + "estimatedCost": "Estimated Cost", + "last30Days": "Last 30 days", + "languageBreakdown": "Language Breakdown", + "recentTranslations": "Recent Translations", + "count": "times", + "tokens": "tokens" + }, + "topUsers": { + "title": "Top Users", + "description": "Users with most tasks", + "displayName": "Display Name", + "totalTasks": "Total Tasks", + "completedTasks": "Completed" + }, + "recentUsers": { + "title": "Recent Users", + "description": "Recently registered users", + "noUsers": "No users", + "displayName": "Display Name", + "registeredAt": "Registered At", + "lastLogin": "Last Login", + "status": "Status", + "taskCount": "Tasks", + "completedCount": "Completed", + "failedCount": "Failed" + }, + "table": { + "taskId": "Task ID", + "targetLang": "Target Language", + "tokenCount": "Tokens", + "charCount": "Characters", + "cost": "Cost", + "processingTime": "Processing Time", + "time": "Time" + } + }, + "taskHistory": { + "title": "Task History", + "subtitle": "View and manage your OCR tasks", + "loadFailed": "Failed to load tasks", + "deleteConfirm": "Are you sure you want to delete this task?", + "deleteFailed": "Failed to delete task", + "noTasksToDelete": "No tasks to delete", + "deleteAllConfirm": "Are you sure you want to delete all {{count}} tasks? This action cannot be undone!", + "allTasksDeleted": "All tasks deleted", + "downloadPdfFailed": "Failed to download PDF", + "startTaskFailed": "Failed to start task", + "cancelConfirm": "Are you sure you want to cancel this task?", + "cancelFailed": "Failed to cancel task", + "retryFailed": "Failed to retry task", + "deleteAll": "Delete All", + "filterConditions": "Filter", + "statusFilter": "Status", + "filenameFilter": "Filename", + "searchFilename": "Search filename", + "startDate": "Start Date", + "endDate": "End Date", + "clearFilter": "Clear Filter", + "taskList": "Task List", + "taskCountInfo": "{{total}} tasks (Page {{page}})", + "noTasks": "No tasks", + "table": { + "filename": "Filename", + "status": "Status", + "createdAt": "Created At", + "completedAt": "Completed At", + "processingTime": "Processing Time", + "actions": "Actions" + }, + "actions": { + "startProcessing": "Start Processing", + "cancel": "Cancel", + "retry": "Retry", + "downloadLayoutPdf": "Download Layout PDF", + "layoutPdf": "Layout", + "downloadReflowPdf": "Download Reflow PDF", + "reflowPdf": "Reflow", + "viewDetails": "View Details", + "delete": "Delete" + }, + "pagination": { + "showing": "Showing {{start}} - {{end}} of {{total}}", + "previous": "Previous", + "next": "Next" + }, + "status": { + "all": "All", + "pending": "Pending", + "processing": "Processing", + "completed": "Completed", + "failed": "Failed" + }, + "unnamed": "Unnamed file" + }, + "taskDetail": { + "title": "Task Details", + "taskId": "Task ID: {{id}}", + "loadingTask": "Loading task details...", + "taskNotFound": "Task not found", + "taskNotFoundDesc": "Task ID not found: {{id}}", + "returnToHistory": "Return to Task History", + "taskInfo": "Task Information", + "filename": "Filename", + "createdAt": "Created At", + "completedAt": "Completed At", + "taskStatus": "Task Status", + "processingTrack": "Processing Track", + "processingTime": "Processing Time", + "lastUpdated": "Last Updated", + "downloadResults": "Download Results", + "layoutPdf": "Layout PDF", + "reflowPdf": "Reflow PDF", + "downloadVisualization": "Download Recognition Images (ZIP)", + "visualizationDownloaded": "Recognition images downloaded", + "errorMessage": "Error Message", + "processingInProgress": "Processing in progress...", + "processingInProgressDesc": "Please wait, OCR processing takes some time", + "ocrPreview": "OCR Result Preview - {{filename}}", + "stats": { + "processingTime": "Processing Time", + "pageCount": "Pages", + "textRegions": "Text Regions", + "tables": "Tables", + "images": "Images", + "avgConfidence": "Avg Confidence" + }, + "track": { + "ocr": "OCR Scan", + "direct": "Direct Extract", + "hybrid": "Hybrid", + "auto": "Auto", + "ocrDesc": "PaddleOCR Text Recognition", + "directDesc": "PyMuPDF Direct Extract", + "hybridDesc": "Hybrid Processing" + }, + "status": { + "completed": "Completed", + "processing": "Processing", + "failed": "Failed", + "pending": "Pending" + } + }, + "auditLogs": { + "title": "Audit Logs", + "subtitle": "System operation records and security audit", + "loadFailed": "Failed to load audit logs", + "loadingLogs": "Loading audit logs...", + "filterConditions": "Filter", + "actionFilter": "Action Type", + "userFilter": "User", + "allActions": "All Actions", + "allUsers": "All Users", + "categoryFilter": "Category", + "allCategories": "All", + "statusFilter": "Status", + "allStatuses": "All", + "startDate": "Start Date", + "endDate": "End Date", + "clearFilter": "Clear Filter", + "logList": "Log List", + "logCountInfo": "{{total}} records (Page {{page}})", + "noLogs": "No logs", + "category": { + "auth": "Authentication", + "task": "Task", + "file": "File", + "admin": "Admin", + "system": "System" + }, + "table": { + "time": "Time", + "user": "User", + "action": "Action", + "target": "Target", + "ip": "IP Address", + "status": "Status", + "category": "Category", + "resource": "Resource", + "errorMessage": "Error Message" + }, + "status": { + "success": "Success", + "failed": "Failed" + }, + "pagination": { + "showing": "Showing {{start}} - {{end}} of {{total}}", + "previous": "Previous", + "next": "Next" + } + }, + "processingTrack": { + "title": "Processing Mode", + "subtitle": "Select processing method or let the system decide", + "auto": { + "label": "Auto Detect", + "description": "System automatically analyzes document type and chooses the best processing method" + }, + "direct": { + "label": "Direct Extract (DIRECT)", + "description": "Extract text directly from PDF text layer, suitable for editable PDFs" + }, + "ocr": { + "label": "OCR Recognition", + "description": "Use optical character recognition for scanned documents or images" + }, + "recommended": "Recommended", + "selected": "Selected", + "overrideWarning": "You have overridden system recommendation. System originally recommended using \"{{track}}\" for this document.", + "analysisConfidence": "Analysis confidence: {{value}}%", + "pageCount": "Pages: {{value}}", + "textCoverage": "Text coverage: {{value}}%", + "note": "Auto mode analyzes PDF content: uses OCR for scanned documents, direct extraction for digital PDFs." + }, + "pdfViewer": { + "loading": "Loading PDF...", + "loadError": "Failed to load PDF", + "noPreview": "Cannot preview", + "page": "Page", + "of": "/", + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out", + "fitWidth": "Fit Width", + "fitPage": "Fit Page", + "pageInfo": "Page {{current}} / {{total}}", + "error": "Error", + "pageLoadError": "Failed to load page {{page}}" + }, + "languageSwitcher": { + "zhTW": "繁體中文", + "enUS": "English" } } diff --git a/frontend/src/i18n/locales/zh-TW.json b/frontend/src/i18n/locales/zh-TW.json index d477de6..78cea24 100644 --- a/frontend/src/i18n/locales/zh-TW.json +++ b/frontend/src/i18n/locales/zh-TW.json @@ -11,7 +11,8 @@ "settings": "設定", "logout": "登出", "taskHistory": "任務歷史", - "adminDashboard": "管理員儀表板" + "adminDashboard": "管理員儀表板", + "auditLogs": "審計日誌" }, "auth": { "login": "登入", @@ -24,15 +25,19 @@ "loggingIn": "登入中...", "usernamePlaceholder": "輸入您的使用者名稱", "passwordPlaceholder": "輸入您的密碼", - "supportedFormats": "支援格式:PDF、圖片、Office 文件" + "supportedFormats": "支援格式:PDF、圖片、Office 文件", + "sessionExpired": "登入已過期,請重新登入", + "redirecting": "正在跳轉至登入頁面..." }, "upload": { "title": "上傳檔案", + "subtitle": "選擇要進行 OCR 處理的檔案,支援圖片、PDF 和 Office 文件", "dragAndDrop": "拖曳檔案至此,或點擊選擇檔案", "dropFilesHere": "放開以上傳檔案", "invalidFiles": "部分檔案格式不支援", "supportedFormats": "支援格式:PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX", "maxFileSize": "單檔最大 50MB", + "maxFileSizeWithCount": "最大檔案大小: 50MB · 最多 {{maxFiles}} 個檔案", "uploadButton": "開始上傳", "uploading": "上傳中...", "uploadSuccess": "上傳成功", @@ -41,7 +46,30 @@ "clearAll": "清除全部", "removeFile": "移除", "selectedFiles": "已選擇的檔案", - "filesUploaded": "成功上傳 {{count}} 個檔案" + "filesUploaded": "成功上傳 {{count}} 個檔案", + "orClickToSelect": "或點擊選擇檔案", + "selectFilesButton": "選擇檔案", + "continueToProcess": "繼續處理", + "goToProcessing": "前往處理頁面", + "uploadMoreFiles": "上傳更多檔案", + "selectAtLeastOne": "請選擇至少一個檔案", + "steps": { + "selectFiles": "選擇檔案", + "selectFilesDesc": "上傳要處理的文件", + "confirmUpload": "確認並上傳", + "confirmUploadDesc": "檢查並開始處理", + "processingComplete": "處理完成", + "processingCompleteDesc": "查看結果並導出" + }, + "fileList": { + "summary": "已選擇 {{count}} 個檔案,總大小 {{size}}", + "unknownType": "未知類型", + "ready": "準備就緒", + "removeFile": "移除檔案", + "confirmPrompt": "請確認檔案無誤後點擊上傳按鈕開始處理", + "cancel": "取消", + "startUpload": "開始上傳並處理" + } }, "processing": { "title": "OCR 處理中", @@ -55,6 +83,11 @@ "failed": "處理失敗", "pending": "等待中", "estimatedTime": "預計剩餘時間", + "ocrStarted": "OCR 處理已開始", + "loadingTask": "載入任務資訊...", + "analyzingDocument": "分析文件類型中...", + "noBatchMessage": "尚未選擇任何任務。請先上傳檔案以建立任務。", + "goToUpload": "前往上傳頁面", "settings": { "title": "處理設定", "language": "識別語言", @@ -151,9 +184,23 @@ "viewJSON": "檢視 JSON", "downloadPDF": "下載 PDF", "preview": "預覽", + "noBatchMessage": "尚未選擇任何任務。請先上傳並處理檔案。", + "goToUpload": "前往上傳頁面", "noResults": "尚無處理結果", "textBlocks": "文字區塊", - "layoutInfo": "版面資訊" + "layoutInfo": "版面資訊", + "pdfDownloaded": "PDF 已下載", + "markdownDownloaded": "Markdown 已下載", + "jsonDownloaded": "JSON 已下載", + "loadingResults": "載入任務結果...", + "processingStatus": "處理狀態", + "taskType": "任務類型", + "processingInProgress": "正在處理中...", + "processingInProgressDesc": "請稍候,OCR 處理需要一些時間", + "waitingToProcess": "等待處理", + "waitingToProcessDesc": "請前往處理頁面啟動 OCR 處理", + "goToProcessing": "前往處理頁面", + "viewTaskHistory": "查看任務歷史" }, "export": { "title": "匯出結果", @@ -217,7 +264,37 @@ "back": "返回", "next": "下一步", "previous": "上一步", - "submit": "提交" + "submit": "提交", + "retry": "重試", + "viewDetails": "查看詳情", + "unknownFile": "未知檔案", + "unknownError": "未知錯誤", + "seconds": "秒", + "total": "總計", + "active": "活躍", + "inactive": "停用", + "all": "全部", + "none": "無", + "enabled": "啟用", + "disabled": "停用", + "yes": "是", + "no": "否", + "or": "或", + "and": "和", + "show": "顯示", + "hide": "隱藏", + "more": "更多", + "less": "更少", + "downloadFailed": "下載失敗", + "downloadSuccess": "下載成功", + "taskDeleted": "任務已刪除", + "taskDeletedDesc": "此任務已被刪除或不存在。請上傳新檔案以建立新任務。", + "taskIdLabel": "任務 ID: {{id}}", + "goToUpload": "前往上傳頁面", + "verifying": "驗證中...", + "accessDenied": "訪問被拒絕", + "accessDeniedDesc": "您沒有權限訪問此頁面", + "backToHome": "返回首頁" }, "errors": { "networkError": "網路錯誤,請稍後再試", @@ -229,12 +306,54 @@ "unsupportedFormat": "不支援的格式", "uploadFailed": "上傳失敗", "processingFailed": "處理失敗", - "exportFailed": "匯出失敗" + "exportFailed": "匯出失敗", + "loadFailed": "載入失敗", + "deleteFailed": "刪除失敗", + "startFailed": "啟動失敗", + "cancelFailed": "取消失敗", + "retryFailed": "重試失敗" }, "translation": { - "title": "翻譯功能", + "title": "文件翻譯", "comingSoon": "即將推出", - "description": "文件翻譯功能正在開發中,敬請期待" + "description": "使用雲端翻譯服務進行多語言翻譯,支援多種目標語言。", + "targetLanguage": "目標語言", + "selectLanguage": "選擇語言", + "startTranslation": "開始翻譯", + "translating": "翻譯中...", + "translationComplete": "翻譯完成", + "translationFailed": "翻譯失敗", + "translationExists": "翻譯已存在", + "translationStarted": "開始翻譯", + "translationStartedDesc": "翻譯任務已啟動,請稍候...", + "completedTranslations": "已完成的翻譯", + "deleteTranslation": "刪除翻譯", + "deleteSuccess": "刪除成功", + "translationDeleted": "翻譯結果 ({{lang}}) 已刪除", + "downloadTranslatedPdf": "翻譯 {{format}} PDF ({{lang}}) 已下載", + "status": { + "preparing": "準備中...", + "loadingModel": "載入翻譯模型...", + "translating": "翻譯中...", + "complete": "完成", + "failed": "失敗" + }, + "stats": "{{elements}} 元素, {{time}}s", + "languages": { + "en": "English", + "ja": "日本語", + "ko": "한국어", + "zh-TW": "繁體中文", + "zh-CN": "简体中文", + "de": "Deutsch", + "fr": "Français", + "es": "Español", + "pt": "Português", + "it": "Italiano", + "ru": "Русский", + "vi": "Tiếng Việt", + "th": "ภาษาไทย" + } }, "batch": { "title": "批次處理", @@ -267,5 +386,255 @@ "processingError": "批次處理錯誤", "processingCancelled": "批次處理已取消", "concurrencyInfo": "Direct Track 最多 5 並行處理,OCR Track 依序處理 (GPU 限制)" + }, + "admin": { + "title": "管理員儀表板", + "subtitle": "系統統計與用戶管理", + "loadingDashboard": "載入管理員儀表板...", + "loadFailed": "載入管理員資料失敗", + "auditLogs": "審計日誌", + "totalUsers": "總用戶數", + "totalTasks": "總任務數", + "pendingTasks": "待處理", + "processingTasks": "處理中", + "completedTasks": "已完成", + "failedTasks": "失敗", + "activeUsers": "活躍", + "translationStats": { + "title": "翻譯統計", + "description": "翻譯 API 使用量與計費追蹤", + "totalTranslations": "總翻譯次數", + "totalTokens": "總 Token 數", + "totalCharacters": "總字元數", + "estimatedCost": "預估成本", + "last30Days": "近30天", + "languageBreakdown": "語言分佈", + "recentTranslations": "最近翻譯記錄", + "count": "次", + "tokens": "tokens" + }, + "topUsers": { + "title": "活躍用戶排行", + "description": "任務數量最多的用戶", + "displayName": "顯示名稱", + "totalTasks": "總任務", + "completedTasks": "已完成" + }, + "recentUsers": { + "title": "最近用戶", + "description": "最新註冊的用戶列表", + "noUsers": "暫無用戶", + "displayName": "顯示名稱", + "registeredAt": "註冊時間", + "lastLogin": "最後登入", + "status": "狀態", + "taskCount": "任務數", + "completedCount": "完成", + "failedCount": "失敗" + }, + "table": { + "taskId": "任務 ID", + "targetLang": "目標語言", + "tokenCount": "Token 數", + "charCount": "字元數", + "cost": "成本", + "processingTime": "處理時間", + "time": "時間" + } + }, + "taskHistory": { + "title": "任務歷史", + "subtitle": "查看和管理您的 OCR 任務", + "loadFailed": "載入任務失敗", + "deleteConfirm": "確定要刪除此任務嗎?", + "deleteFailed": "刪除任務失敗", + "noTasksToDelete": "沒有可刪除的任務", + "deleteAllConfirm": "確定要刪除所有 {{count}} 個任務嗎?此操作無法復原!", + "allTasksDeleted": "所有任務已刪除", + "downloadPdfFailed": "下載 PDF 檔案失敗", + "startTaskFailed": "啟動任務失敗", + "cancelConfirm": "確定要取消此任務嗎?", + "cancelFailed": "取消任務失敗", + "retryFailed": "重試任務失敗", + "deleteAll": "刪除全部", + "filterConditions": "篩選條件", + "statusFilter": "狀態", + "filenameFilter": "檔案名稱", + "searchFilename": "搜尋檔案名稱", + "startDate": "開始日期", + "endDate": "結束日期", + "clearFilter": "清除篩選", + "taskList": "任務列表", + "taskCountInfo": "共 {{total}} 個任務 (顯示第 {{page}} 頁)", + "noTasks": "暫無任務", + "table": { + "filename": "檔案名稱", + "status": "狀態", + "createdAt": "建立時間", + "completedAt": "完成時間", + "processingTime": "處理時間", + "actions": "操作" + }, + "actions": { + "startProcessing": "開始處理", + "cancel": "取消", + "retry": "重試", + "downloadLayoutPdf": "下載版面 PDF", + "layoutPdf": "版面", + "downloadReflowPdf": "下載流式 PDF", + "reflowPdf": "流式", + "viewDetails": "查看詳情", + "delete": "刪除" + }, + "pagination": { + "showing": "顯示 {{start}} - {{end}} / 共 {{total}} 個", + "previous": "上一頁", + "next": "下一頁" + }, + "status": { + "all": "全部", + "pending": "待處理", + "processing": "處理中", + "completed": "已完成", + "failed": "失敗" + }, + "unnamed": "未命名檔案" + }, + "taskDetail": { + "title": "任務詳情", + "taskId": "任務 ID: {{id}}", + "loadingTask": "載入任務詳情...", + "taskNotFound": "任務不存在", + "taskNotFoundDesc": "找不到任務 ID: {{id}}", + "returnToHistory": "返回任務歷史", + "taskInfo": "任務資訊", + "filename": "檔案名稱", + "createdAt": "建立時間", + "completedAt": "完成時間", + "taskStatus": "任務狀態", + "processingTrack": "處理軌道", + "processingTime": "處理時間", + "lastUpdated": "最後更新", + "downloadResults": "下載結果", + "layoutPdf": "版面 PDF", + "reflowPdf": "流式 PDF", + "downloadVisualization": "下載辨識結果圖片 (ZIP)", + "visualizationDownloaded": "辨識結果圖片已下載", + "errorMessage": "錯誤訊息", + "processingInProgress": "正在處理中...", + "processingInProgressDesc": "請稍候,OCR 處理需要一些時間", + "ocrPreview": "OCR 結果預覽 - {{filename}}", + "stats": { + "processingTime": "處理時間", + "pageCount": "頁數", + "textRegions": "文本區域", + "tables": "表格", + "images": "圖片", + "avgConfidence": "平均置信度" + }, + "track": { + "ocr": "OCR 掃描", + "direct": "直接提取", + "hybrid": "混合", + "auto": "自動", + "ocrDesc": "PaddleOCR 文字識別", + "directDesc": "PyMuPDF 直接提取", + "hybridDesc": "混合處理" + }, + "status": { + "completed": "已完成", + "processing": "處理中", + "failed": "失敗", + "pending": "等待中" + } + }, + "auditLogs": { + "title": "審計日誌", + "subtitle": "系統操作記錄與安全審計", + "loadFailed": "載入審計日誌失敗", + "loadingLogs": "載入審計日誌...", + "filterConditions": "篩選條件", + "actionFilter": "操作類型", + "userFilter": "用戶", + "allActions": "所有操作", + "allUsers": "所有用戶", + "categoryFilter": "類別", + "allCategories": "全部", + "statusFilter": "狀態", + "allStatuses": "全部", + "startDate": "開始日期", + "endDate": "結束日期", + "clearFilter": "清除篩選", + "logList": "日誌列表", + "logCountInfo": "共 {{total}} 筆記錄 (顯示第 {{page}} 頁)", + "noLogs": "暫無日誌記錄", + "category": { + "auth": "認證", + "task": "任務", + "file": "檔案", + "admin": "管理", + "system": "系統" + }, + "table": { + "time": "時間", + "user": "用戶", + "action": "操作", + "target": "目標", + "ip": "IP 位址", + "status": "狀態", + "category": "類別", + "resource": "資源", + "errorMessage": "錯誤訊息" + }, + "status": { + "success": "成功", + "failed": "失敗" + }, + "pagination": { + "showing": "顯示 {{start}} - {{end}} / 共 {{total}} 筆", + "previous": "上一頁", + "next": "下一頁" + } + }, + "processingTrack": { + "title": "處理方式選擇", + "subtitle": "選擇文件的處理方式,或讓系統自動判斷", + "auto": { + "label": "自動選擇", + "description": "根據文件類型自動選擇最佳處理方式" + }, + "direct": { + "label": "直接提取 (DIRECT)", + "description": "從 PDF 中直接提取文字圖層,適用於可編輯 PDF" + }, + "ocr": { + "label": "OCR 識別", + "description": "使用光學字元識別處理圖片或掃描文件" + }, + "recommended": "系統建議", + "selected": "已選擇", + "overrideWarning": "您已覆蓋系統建議。系統原本建議使用「{{track}}」方式處理此文件。", + "analysisConfidence": "文件分析信心度: {{value}}%", + "pageCount": "頁數: {{value}}", + "textCoverage": "文字覆蓋率: {{value}}%", + "note": "自動模式會根據 PDF 內容判斷:若為掃描件則使用 OCR,若為數位 PDF 則直接擷取。" + }, + "pdfViewer": { + "loading": "載入 PDF 中...", + "loadError": "載入 PDF 失敗", + "noPreview": "無法預覽", + "page": "頁", + "of": "/", + "zoomIn": "放大", + "zoomOut": "縮小", + "fitWidth": "符合寬度", + "fitPage": "符合頁面", + "pageInfo": "第 {{current}} 頁 / 共 {{total}} 頁", + "error": "錯誤", + "pageLoadError": "無法載入第 {{page}} 頁" + }, + "languageSwitcher": { + "zhTW": "繁體中文", + "enUS": "English" } } diff --git a/frontend/src/pages/AdminDashboardPage.tsx b/frontend/src/pages/AdminDashboardPage.tsx index 352fe99..ae50c84 100644 --- a/frontend/src/pages/AdminDashboardPage.tsx +++ b/frontend/src/pages/AdminDashboardPage.tsx @@ -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(null) const [users, setUsers] = useState([]) @@ -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() {
-

載入管理員儀表板...

+

{t('admin.loadingDashboard')}

) @@ -95,7 +97,7 @@ export default function AdminDashboardPage() {
-

載入失敗

+

{t('errors.loadFailed')}

{error}

@@ -110,18 +112,18 @@ export default function AdminDashboardPage() {
-

管理員儀表板

+

{t('admin.title')}

-

系統統計與用戶管理

+

{t('admin.subtitle')}

@@ -133,13 +135,13 @@ export default function AdminDashboardPage() { - 總用戶數 + {t('admin.totalUsers')}
{stats.total_users}

- 活躍: {stats.active_users} + {t('admin.activeUsers')}: {stats.active_users}

@@ -148,7 +150,7 @@ export default function AdminDashboardPage() { - 總任務數 + {t('admin.totalTasks')} @@ -160,7 +162,7 @@ export default function AdminDashboardPage() { - 待處理 + {t('admin.pendingTasks')} @@ -174,7 +176,7 @@ export default function AdminDashboardPage() { - 處理中 + {t('admin.processingTasks')} @@ -188,7 +190,7 @@ export default function AdminDashboardPage() { - 已完成 + {t('admin.completedTasks')} @@ -196,7 +198,7 @@ export default function AdminDashboardPage() { {stats.task_stats.completed}

- 失敗: {stats.task_stats.failed} + {t('admin.failedTasks')}: {stats.task_stats.failed}

@@ -209,42 +211,42 @@ export default function AdminDashboardPage() { - 翻譯統計 + {t('admin.translationStats.title')} - 翻譯 API 使用量與計費追蹤 + {t('admin.translationStats.description')}
- 總翻譯次數 + {t('admin.translationStats.totalTranslations')}
{translationStats.total_translations.toLocaleString()}

- 近30天: {translationStats.last_30_days.count} + {t('admin.translationStats.last30Days')}: {translationStats.last_30_days.count}

- 總 Token 數 + {t('admin.translationStats.totalTokens')}
{translationStats.total_tokens.toLocaleString()}

- 近30天: {translationStats.last_30_days.tokens.toLocaleString()} + {t('admin.translationStats.last30Days')}: {translationStats.last_30_days.tokens.toLocaleString()}

- 總字元數 + {t('admin.translationStats.totalCharacters')}
{translationStats.total_characters.toLocaleString()} @@ -254,7 +256,7 @@ export default function AdminDashboardPage() {
- 預估成本 + {t('admin.translationStats.estimatedCost')}
${translationStats.estimated_cost.toFixed(2)} @@ -266,11 +268,11 @@ export default function AdminDashboardPage() { {/* Language Breakdown */} {translationStats.by_language.length > 0 && (
-

語言分佈

+

{t('admin.translationStats.languageBreakdown')}

{translationStats.by_language.map((lang) => ( - {lang.language}: {lang.count} 次 ({lang.tokens.toLocaleString()} tokens) + {lang.language}: {lang.count} {t('admin.translationStats.count')} ({lang.tokens.toLocaleString()} {t('admin.translationStats.tokens')}) ))}
@@ -280,42 +282,42 @@ export default function AdminDashboardPage() { {/* Recent Translations */} {translationStats.recent_translations.length > 0 && (
-

最近翻譯記錄

+

{t('admin.translationStats.recentTranslations')}

- 任務 ID - 目標語言 - Token 數 - 字元數 - 成本 - 處理時間 - 時間 + {t('admin.table.taskId')} + {t('admin.table.targetLang')} + {t('admin.table.tokenCount')} + {t('admin.table.charCount')} + {t('admin.table.cost')} + {t('admin.table.processingTime')} + {t('admin.table.time')} - {translationStats.recent_translations.slice(0, 10).map((t) => ( - + {translationStats.recent_translations.slice(0, 10).map((tr) => ( + - {t.task_id.substring(0, 8)}... + {tr.task_id.substring(0, 8)}... - {t.target_lang} + {tr.target_lang} - {t.total_tokens.toLocaleString()} + {tr.total_tokens.toLocaleString()} - {t.total_characters.toLocaleString()} + {tr.total_characters.toLocaleString()} - ${t.estimated_cost.toFixed(4)} + ${tr.estimated_cost.toFixed(4)} - {t.processing_time_seconds.toFixed(1)}s + {tr.processing_time_seconds.toFixed(1)}s - {new Date(t.created_at).toLocaleString('zh-TW')} + {formatDate(tr.created_at)} ))} @@ -333,9 +335,9 @@ export default function AdminDashboardPage() { - 活躍用戶排行 + {t('admin.topUsers.title')} - 任務數量最多的用戶 + {t('admin.topUsers.description')}
@@ -343,9 +345,9 @@ export default function AdminDashboardPage() { # Email - 顯示名稱 - 總任務 - 已完成 + {t('admin.topUsers.displayName')} + {t('admin.topUsers.totalTasks')} + {t('admin.topUsers.completedTasks')} @@ -377,26 +379,26 @@ export default function AdminDashboardPage() { - 最近用戶 + {t('admin.recentUsers.title')} - 最新註冊的用戶列表 + {t('admin.recentUsers.description')} {users.length === 0 ? (
-

暫無用戶

+

{t('admin.recentUsers.noUsers')}

) : (
Email - 顯示名稱 - 註冊時間 - 最後登入 - 狀態 - 任務數 + {t('admin.recentUsers.displayName')} + {t('admin.recentUsers.registeredAt')} + {t('admin.recentUsers.lastLogin')} + {t('admin.recentUsers.status')} + {t('admin.recentUsers.taskCount')} @@ -412,14 +414,14 @@ export default function AdminDashboardPage() { - {user.is_active ? '活躍' : '停用'} + {user.is_active ? t('common.active') : t('common.inactive')}
{user.task_count}
- 完成: {user.completed_tasks} | 失敗: {user.failed_tasks} + {t('admin.recentUsers.completedCount')}: {user.completed_tasks} | {t('admin.recentUsers.failedCount')}: {user.failed_tasks}
diff --git a/frontend/src/pages/AuditLogsPage.tsx b/frontend/src/pages/AuditLogsPage.tsx index 586704e..4ee0716 100644 --- a/frontend/src/pages/AuditLogsPage.tsx +++ b/frontend/src/pages/AuditLogsPage.tsx @@ -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 { AuditLog } from '@/types/apiV2' import { @@ -33,6 +34,7 @@ import { NativeSelect } from '@/components/ui/select' export default function AuditLogsPage() { const navigate = useNavigate() + const { t, i18n } = useTranslation() const [logs, setLogs] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') @@ -71,7 +73,7 @@ export default function AuditLogsPage() { setHasMore(response.has_more) } catch (err: any) { console.error('Failed to fetch audit logs:', err) - setError(err.response?.data?.detail || '載入審計日誌失敗') + setError(err.response?.data?.detail || t('auditLogs.loadFailed')) } finally { setLoading(false) } @@ -89,22 +91,24 @@ export default function AuditLogsPage() { // Format date const formatDate = (dateStr: string) => { const date = new Date(dateStr) - return date.toLocaleString('zh-TW') + return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-TW') } // Get category badge const getCategoryBadge = (category: string) => { - const variants: Record = { - auth: { variant: 'default', label: '認證' }, - task: { variant: 'secondary', label: '任務' }, - file: { variant: 'secondary', label: '檔案' }, - admin: { variant: 'destructive', label: '管理' }, - system: { variant: 'secondary', label: '系統' }, + const variants: Record = { + auth: { variant: 'default', labelKey: 'auditLogs.category.auth' }, + task: { variant: 'secondary', labelKey: 'auditLogs.category.task' }, + file: { variant: 'secondary', labelKey: 'auditLogs.category.file' }, + admin: { variant: 'destructive', labelKey: 'auditLogs.category.admin' }, + system: { variant: 'secondary', labelKey: 'auditLogs.category.system' }, } - const config = variants[category] || { variant: 'secondary', label: category } - - return {config.label} + const config = variants[category] + if (config) { + return {t(config.labelKey)} + } + return {category} } return ( @@ -120,16 +124,16 @@ export default function AuditLogsPage() { className="mr-2" > - 返回 + {t('common.back')} -

審計日誌

+

{t('auditLogs.title')}

-

系統操作記錄與審計追蹤

+

{t('auditLogs.subtitle')}

@@ -138,13 +142,13 @@ export default function AuditLogsPage() { - 篩選條件 + {t('auditLogs.filterConditions')}
- + ) => { @@ -152,18 +156,18 @@ export default function AuditLogsPage() { handleFilterChange() }} options={[ - { value: 'all', label: '全部' }, - { value: 'auth', label: '認證' }, - { value: 'task', label: '任務' }, - { value: 'file', label: '檔案' }, - { value: 'admin', label: '管理' }, - { value: 'system', label: '系統' }, + { value: 'all', label: t('auditLogs.allCategories') }, + { value: 'auth', label: t('auditLogs.category.auth') }, + { value: 'task', label: t('auditLogs.category.task') }, + { value: 'file', label: t('auditLogs.category.file') }, + { value: 'admin', label: t('auditLogs.category.admin') }, + { value: 'system', label: t('auditLogs.category.system') }, ]} />
- + ) => { @@ -171,9 +175,9 @@ export default function AuditLogsPage() { handleFilterChange() }} options={[ - { value: 'all', label: '全部' }, - { value: 'true', label: '成功' }, - { value: 'false', label: '失敗' }, + { value: 'all', label: t('auditLogs.allStatuses') }, + { value: 'true', label: t('auditLogs.status.success') }, + { value: 'false', label: t('auditLogs.status.failed') }, ]} />
@@ -190,7 +194,7 @@ export default function AuditLogsPage() { handleFilterChange() }} > - 清除篩選 + {t('auditLogs.clearFilter')}
)} @@ -208,9 +212,9 @@ export default function AuditLogsPage() { {/* Audit Logs List */} - 審計日誌記錄 + {t('auditLogs.logList')} - 共 {total} 筆記錄 {hasMore && `(顯示第 ${page} 頁)`} + {t('auditLogs.logCountInfo', { total, page })} @@ -221,20 +225,20 @@ export default function AuditLogsPage() { ) : logs.length === 0 ? (
-

暫無審計日誌

+

{t('auditLogs.noLogs')}

) : ( <>
- 時間 - 用戶 - 類別 - 操作 - 資源 - 狀態 - 錯誤訊息 + {t('auditLogs.table.time')} + {t('auditLogs.table.user')} + {t('auditLogs.table.category')} + {t('auditLogs.table.action')} + {t('auditLogs.table.resource')} + {t('auditLogs.table.status')} + {t('auditLogs.table.errorMessage')} @@ -269,12 +273,12 @@ export default function AuditLogsPage() { {log.success ? ( - 成功 + {t('auditLogs.status.success')} ) : ( - 失敗 + {t('auditLogs.status.failed')} )} @@ -293,8 +297,11 @@ export default function AuditLogsPage() { {/* Pagination */}
- 顯示 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} / 共{' '} - {total} 筆 + {t('auditLogs.pagination.showing', { + start: (page - 1) * pageSize + 1, + end: Math.min(page * pageSize, total), + total, + })}
diff --git a/frontend/src/pages/ProcessingPage.tsx b/frontend/src/pages/ProcessingPage.tsx index d5eb9e2..232957a 100644 --- a/frontend/src/pages/ProcessingPage.tsx +++ b/frontend/src/pages/ProcessingPage.tsx @@ -124,8 +124,8 @@ function SingleTaskProcessing() { updateTaskStatus(taskId, 'processing', forceTrack || undefined) } toast({ - title: '開始處理', - description: 'OCR 處理已開始', + title: t('processing.startProcessing'), + description: t('processing.ocrStarted'), variant: 'success', }) }, @@ -200,7 +200,7 @@ function SingleTaskProcessing() {
-

載入任務資訊...

+

{t('processing.loadingTask')}

) @@ -226,13 +226,13 @@ function SingleTaskProcessing() {

- {t('processing.noBatchMessage', { defaultValue: '尚未選擇任何任務。請先上傳檔案以建立任務。' })} + {t('processing.noBatchMessage')}

@@ -252,7 +252,7 @@ function SingleTaskProcessing() {

{t('processing.title')}

- 任務 ID: {taskId} + {t('taskDetail.taskId', { id: taskId })} {taskDetail?.filename && ` · ${taskDetail.filename}`}

@@ -260,13 +260,13 @@ function SingleTaskProcessing() { {isCompleted && (
- 處理完成 + {t('processing.completed')}
)} {isProcessing && (
- 處理中 + {t('processing.processing')}
)} @@ -311,9 +311,9 @@ function SingleTaskProcessing() {
-

檔案名稱

+

{t('taskDetail.filename')}

- {taskDetail.filename || '未知檔案'} + {taskDetail.filename || t('common.unknownFile')}

@@ -325,7 +325,7 @@ function SingleTaskProcessing() {
-

處理時間

+

{t('taskDetail.processingTime')}

{(taskDetail.processing_time_ms / 1000).toFixed(2)}s

@@ -342,7 +342,7 @@ function SingleTaskProcessing() {
-

處理失敗

+

{t('processing.failed')}

{taskDetail.error_message}

@@ -380,7 +380,7 @@ function SingleTaskProcessing() { size="lg" > - 查看任務歷史 + {t('results.viewTaskHistory')} )}
@@ -396,24 +396,24 @@ function SingleTaskProcessing() {
- 任務詳情 + {t('taskDetail.title')}
- 任務狀態 + {t('taskDetail.taskStatus')} {getStatusBadge(taskDetail.status)}
- 建立時間 + {t('taskDetail.createdAt')} {new Date(taskDetail.created_at).toLocaleString('zh-TW')}
{taskDetail.updated_at && (
- 更新時間 + {t('taskDetail.lastUpdated')} {new Date(taskDetail.updated_at).toLocaleString('zh-TW')} @@ -421,7 +421,7 @@ function SingleTaskProcessing() { )} {taskDetail.completed_at && (
- 完成時間 + {t('taskDetail.completedAt')} {new Date(taskDetail.completed_at).toLocaleString('zh-TW')} @@ -439,7 +439,7 @@ function SingleTaskProcessing() { {isAnalyzing && (
- 分析文件類型中... + {t('processing.analyzingDocument')}
)} diff --git a/frontend/src/pages/ResultsPage.tsx b/frontend/src/pages/ResultsPage.tsx index bdc6a0a..28a3d24 100644 --- a/frontend/src/pages/ResultsPage.tsx +++ b/frontend/src/pages/ResultsPage.tsx @@ -47,7 +47,7 @@ export default function ResultsPage() { await apiClientV2.downloadPDF(taskId) toast({ title: t('export.exportSuccess'), - description: 'PDF 已下載', + description: t('results.pdfDownloaded'), variant: 'success', }) } catch (error: any) { @@ -65,7 +65,7 @@ export default function ResultsPage() { await apiClientV2.downloadMarkdown(taskId) toast({ title: t('export.exportSuccess'), - description: 'Markdown 已下載', + description: t('results.markdownDownloaded'), variant: 'success', }) } catch (error: any) { @@ -83,7 +83,7 @@ export default function ResultsPage() { await apiClientV2.downloadJSON(taskId) toast({ title: t('export.exportSuccess'), - description: 'JSON 已下載', + description: t('results.jsonDownloaded'), variant: 'success', }) } catch (error: any) { @@ -98,13 +98,13 @@ export default function ResultsPage() { const getStatusBadge = (status: string) => { switch (status) { case 'completed': - return 已完成 + return {t('taskDetail.status.completed')} case 'processing': - return 處理中 + return {t('taskDetail.status.processing')} case 'failed': - return 失敗 + return {t('taskDetail.status.failed')} default: - return 待處理 + return {t('taskDetail.status.pending')} } } @@ -114,7 +114,7 @@ export default function ResultsPage() {
-

載入任務結果...

+

{t('results.loadingResults')}

) @@ -140,10 +140,10 @@ export default function ResultsPage() {

- {t('results.noBatchMessage', { defaultValue: '尚未選擇任何任務。請先上傳並處理檔案。' })} + {t('results.noBatchMessage')}

@@ -157,11 +157,11 @@ export default function ResultsPage() {
- 任務不存在 + {t('taskDetail.taskNotFound')} @@ -179,7 +179,7 @@ export default function ResultsPage() {

{t('results.title')}

- 任務 ID: {taskId} + {t('taskDetail.taskId', { id: taskId })} {taskDetail.filename && ` · ${taskDetail.filename}`}

@@ -215,7 +215,7 @@ export default function ResultsPage() {
-

處理時間

+

{t('results.processingTime')}

{taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0'}s

@@ -231,8 +231,8 @@ export default function ResultsPage() {
-

處理狀態

-

成功

+

{t('results.processingStatus')}

+

{t('common.success')}

@@ -245,7 +245,7 @@ export default function ResultsPage() {
-

任務類型

+

{t('results.taskType')}

OCR

@@ -257,7 +257,7 @@ export default function ResultsPage() { {/* Results Preview */} {isCompleted ? ( @@ -265,15 +265,15 @@ export default function ResultsPage() { -

正在處理中...

-

請稍候,OCR 處理需要一些時間

+

{t('results.processingInProgress')}

+

{t('results.processingInProgressDesc')}

) : taskDetail.status === 'failed' ? ( -

處理失敗

+

{t('processing.failed')}

{taskDetail.error_message && (

{taskDetail.error_message}

)} @@ -283,10 +283,10 @@ export default function ResultsPage() { -

等待處理

-

請前往處理頁面啟動 OCR 處理

+

{t('results.waitingToProcess')}

+

{t('results.waitingToProcessDesc')}

diff --git a/frontend/src/pages/TaskDetailPage.tsx b/frontend/src/pages/TaskDetailPage.tsx index 05f93b5..98d967f 100644 --- a/frontend/src/pages/TaskDetailPage.tsx +++ b/frontend/src/pages/TaskDetailPage.tsx @@ -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 直接提取 + return {t('taskDetail.track.direct')} case 'ocr': return OCR case 'hybrid': - return 混合 + return {t('taskDetail.track.hybrid')} default: - return 自動 + return {t('taskDetail.track.auto')} } } 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 已完成 + return {t('taskDetail.status.completed')} case 'processing': - return 處理中 + return {t('taskDetail.status.processing')} case 'failed': - return 失敗 + return {t('taskDetail.status.failed')} default: - return 待處理 + return {t('taskDetail.status.pending')} } } 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() {
-

載入任務詳情...

+

{t('taskDetail.loadingTask')}

) @@ -372,12 +372,12 @@ export default function TaskDetailPage() {
- 任務不存在 + {t('taskDetail.taskNotFound')} -

找不到任務 ID: {taskId}

+

{t('taskDetail.taskNotFoundDesc', { id: taskId })}

@@ -397,19 +397,19 @@ export default function TaskDetailPage() {
-

任務詳情

+

{t('taskDetail.title')}

- 任務 ID: {taskId} + {t('taskDetail.taskId', { id: taskId })}

{getStatusBadge(taskDetail.status)}
@@ -421,35 +421,35 @@ export default function TaskDetailPage() { - 任務資訊 + {t('taskDetail.taskInfo')}
-

檔案名稱

-

{taskDetail.filename || '未知檔案'}

+

{t('taskDetail.filename')}

+

{taskDetail.filename || t('common.unknownFile')}

-

建立時間

+

{t('taskDetail.createdAt')}

{formatDate(taskDetail.created_at)}

{taskDetail.completed_at && (
-

完成時間

+

{t('taskDetail.completedAt')}

{formatDate(taskDetail.completed_at)}

)}
-

任務狀態

+

{t('taskDetail.taskStatus')}

{getStatusBadge(taskDetail.status)}
{(taskDetail.processing_track || processingMetadata?.processing_track) && (
-

處理軌道

+

{t('taskDetail.processingTrack')}

{getTrackBadge(taskDetail.processing_track || processingMetadata?.processing_track)} @@ -460,13 +460,13 @@ export default function TaskDetailPage() { )} {taskDetail.processing_time_ms && (
-

處理時間

-

{(taskDetail.processing_time_ms / 1000).toFixed(2)} 秒

+

{t('taskDetail.processingTime')}

+

{(taskDetail.processing_time_ms / 1000).toFixed(2)} {t('common.seconds')}

)} {taskDetail.updated_at && (
-

最後更新

+

{t('taskDetail.lastUpdated')}

{formatDate(taskDetail.updated_at)}

)} @@ -481,18 +481,18 @@ export default function TaskDetailPage() { - 下載結果 + {t('taskDetail.downloadResults')}
{/* Visualization download for OCR Track */} @@ -504,7 +504,7 @@ export default function TaskDetailPage() { className="w-full gap-2" > - 下載辨識結果圖片 (ZIP) + {t('taskDetail.downloadVisualization')}
)} @@ -518,7 +518,7 @@ export default function TaskDetailPage() { - 文件翻譯 + {t('translation.title')} @@ -526,14 +526,14 @@ export default function TaskDetailPage() {
- 目標語言: + {t('translation.targetLanguage')}
- +
- + - 清除篩選 + {t('taskHistory.clearFilter')}
)} @@ -413,9 +415,9 @@ export default function TaskHistoryPage() { {/* Task List */} - 任務列表 + {t('taskHistory.taskList')} - 共 {total} 個任務 {hasMore && `(顯示第 ${page} 頁)`} + {t('common.total')} {total} {hasMore && `(${t('taskHistory.pagination.showing', { start: (page - 1) * pageSize + 1, end: Math.min(page * pageSize, total), total })})`} @@ -426,26 +428,26 @@ export default function TaskHistoryPage() { ) : tasks.length === 0 ? (
-

暫無任務

+

{t('taskHistory.noTasks')}

) : ( <>
- 檔案名稱 - 狀態 - 建立時間 - 完成時間 - 處理時間 - 操作 + {t('taskHistory.filenameFilter')} + {t('taskHistory.statusFilter')} + {t('taskHistory.table.createdAt')} + {t('taskHistory.table.completedAt')} + {t('taskHistory.table.processingTime')} + {t('taskHistory.table.actions')} {tasks.map((task) => ( - {task.filename || '未命名檔案'} + {task.filename || t('taskHistory.unnamed')} {getStatusBadge(task.status)} @@ -466,7 +468,7 @@ export default function TaskHistoryPage() { variant="outline" size="sm" onClick={() => handleStartTask(task.task_id)} - title="開始處理" + title={t('taskHistory.actions.startProcessing')} > @@ -474,7 +476,7 @@ export default function TaskHistoryPage() { variant="outline" size="sm" onClick={() => handleCancelTask(task.task_id)} - title="取消" + title={t('taskHistory.actions.cancel')} > @@ -485,7 +487,7 @@ export default function TaskHistoryPage() { variant="outline" size="sm" onClick={() => handleCancelTask(task.task_id)} - title="取消" + title={t('taskHistory.actions.cancel')} > @@ -495,7 +497,7 @@ export default function TaskHistoryPage() { variant="outline" size="sm" onClick={() => handleRetryTask(task.task_id)} - title="重試" + title={t('taskHistory.actions.retry')} > @@ -507,25 +509,25 @@ export default function TaskHistoryPage() { variant="outline" size="sm" onClick={() => handleDownloadPDF(task.task_id, 'layout')} - title="下載版面 PDF" + title={t('taskHistory.actions.downloadLayoutPdf')} > - 版面 + {t('taskHistory.actions.layoutPdf')} @@ -536,7 +538,7 @@ export default function TaskHistoryPage() { variant="outline" size="sm" onClick={() => handleDelete(task.task_id)} - title="刪除" + title={t('taskHistory.actions.delete')} > @@ -550,8 +552,7 @@ export default function TaskHistoryPage() { {/* Pagination */}
- 顯示 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} / 共{' '} - {total} 個 + {t('taskHistory.pagination.showing', { start: (page - 1) * pageSize + 1, end: Math.min(page * pageSize, total), total })}
diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx index 94684c8..6f7c867 100644 --- a/frontend/src/pages/UploadPage.tsx +++ b/frontend/src/pages/UploadPage.tsx @@ -82,7 +82,7 @@ export default function UploadPage() { if (selectedFiles.length === 0) { toast({ title: t('errors.validationError'), - description: '請選擇至少一個檔案', + description: t('upload.selectAtLeastOne'), variant: 'destructive', }) return @@ -122,7 +122,7 @@ export default function UploadPage() {

{t('upload.title')}

- 選擇要進行 OCR 處理的檔案,支援圖片、PDF 和 Office 文件 + {t('upload.subtitle')}

@@ -135,8 +135,8 @@ export default function UploadPage() { {selectedFiles.length === 0 ? '1' : }
-
選擇檔案
-
上傳要處理的文件
+
{t('upload.steps.selectFiles')}
+
{t('upload.steps.selectFilesDesc')}
@@ -151,8 +151,8 @@ export default function UploadPage() {
0 ? 'text-foreground' : 'text-muted-foreground' - }`}>確認並上傳
-
檢查並開始處理
+ }`}>{t('upload.steps.confirmUpload')}
+
{t('upload.steps.confirmUploadDesc')}
@@ -163,8 +163,8 @@ export default function UploadPage() { 3
-
處理完成
-
查看結果並導出
+
{t('upload.steps.processingComplete')}
+
{t('upload.steps.processingCompleteDesc')}
@@ -193,7 +193,7 @@ export default function UploadPage() { {t('upload.selectedFiles')}

- 已選擇 {selectedFiles.length} 個檔案,總大小 {formatFileSize(totalSize)} + {t('upload.fileList.summary', { count: selectedFiles.length, size: formatFileSize(totalSize) })}

@@ -205,7 +205,7 @@ export default function UploadPage() { className="gap-2" > - 清空全部 + {t('upload.clearAll')} @@ -228,13 +228,13 @@ export default function UploadPage() { {file.name}

- {formatFileSize(file.size)} · {file.type || '未知類型'} + {formatFileSize(file.size)} · {file.type || t('upload.fileList.unknownType')}

{/* Status badge */}
- 準備就緒 + {t('upload.fileList.ready')}
{/* Remove button */} @@ -242,7 +242,7 @@ export default function UploadPage() { onClick={() => handleRemoveFile(index)} disabled={uploadMutation.isPending} className="p-2 rounded-lg text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - title="移除檔案" + title={t('upload.fileList.removeFile')} > @@ -255,7 +255,7 @@ export default function UploadPage() { {/* Action Bar */}
- 請確認檔案無誤後點擊上傳按鈕開始處理 + {t('upload.fileList.confirmPrompt')}