From 42850c446010db10c386920b5251706bc1f0ce04 Mon Sep 17 00:00:00 2001 From: liuyuanchuang Date: Sat, 24 Jan 2026 13:53:50 +0800 Subject: [PATCH] feat: add translate --- src/App.tsx | 33 +++++- src/components/AuthModal.tsx | 14 ++- src/components/ExportSidebar.tsx | 14 ++- src/components/FilePreview.tsx | 12 +- src/components/LeftSidebar.tsx | 40 ++++--- src/components/Navbar.tsx | 78 +++++++++++-- src/components/ResultPanel.tsx | 21 ++-- src/components/UploadModal.tsx | 15 ++- src/components/UserGuide.tsx | 174 ++++++++++++++++++++++++++++ src/contexts/LanguageContext.tsx | 39 +++++++ src/lib/translations.ts | 193 +++++++++++++++++++++++++++++++ src/main.tsx | 5 +- 12 files changed, 572 insertions(+), 66 deletions(-) create mode 100644 src/components/UserGuide.tsx create mode 100644 src/contexts/LanguageContext.tsx create mode 100644 src/lib/translations.ts diff --git a/src/App.tsx b/src/App.tsx index 5e04a90..d7ab87c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'; import { useAuth } from './contexts/AuthContext'; +import { useLanguage } from './contexts/LanguageContext'; import { uploadService } from './lib/uploadService'; import { FileRecord, RecognitionResult } from './types'; import { TaskStatus, TaskHistoryItem } from './types/api'; @@ -8,15 +9,18 @@ import Navbar from './components/Navbar'; import FilePreview from './components/FilePreview'; import ResultPanel from './components/ResultPanel'; import UploadModal from './components/UploadModal'; +import UserGuide from './components/UserGuide'; const PAGE_SIZE = 6; function App() { const { user, initializing } = useAuth(); + const { t } = useLanguage(); const [files, setFiles] = useState([]); const [selectedFileId, setSelectedFileId] = useState(null); const [selectedResult, setSelectedResult] = useState(null); const [showUploadModal, setShowUploadModal] = useState(false); + const [showUserGuide, setShowUserGuide] = useState(false); const [loading, setLoading] = useState(false); // Pagination state @@ -41,6 +45,20 @@ function App() { const selectedFile = files.find((f) => f.id === selectedFileId) || null; + useEffect(() => { + const handleStartGuide = () => setShowUserGuide(true); + window.addEventListener('start-user-guide', handleStartGuide); + + // Check for first-time user + const hasSeenGuide = localStorage.getItem('hasSeenGuide'); + if (!hasSeenGuide) { + setTimeout(() => setShowUserGuide(true), 1500); + localStorage.setItem('hasSeenGuide', 'true'); + } + + return () => window.removeEventListener('start-user-guide', handleStartGuide); + }, []); + useEffect(() => { if (!initializing && user && !hasLoadedFiles.current) { hasLoadedFiles.current = true; @@ -352,7 +370,7 @@ function App() { return f; })); - alert('Task timeout: Recognition took too long.'); + alert(t.alerts.taskTimeout); } } catch (error) { @@ -366,7 +384,7 @@ function App() { if (f.id === fileId) return { ...f, status: 'failed' }; return f; })); - alert('Task timeout or network error.'); + alert(t.alerts.networkError); } } }, 2000); // Poll every 2 seconds @@ -415,7 +433,7 @@ function App() { } } catch (error) { console.error('Error uploading files:', error); - alert('Upload failed: ' + (error instanceof Error ? error.message : 'Unknown error')); + alert(`${t.alerts.uploadFailed}: ` + (error instanceof Error ? error.message : 'Unknown error')); } finally { setLoading(false); } @@ -426,7 +444,7 @@ function App() {
-

Loading...

+

{t.common.loading}

); @@ -484,11 +502,16 @@ function App() { /> )} + setShowUserGuide(false)} + /> + {loading && (
-

Processing...

+

{t.common.processing}

)} diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index aa63b55..c0665e1 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { X } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; +import { useLanguage } from '../contexts/LanguageContext'; interface AuthModalProps { onClose: () => void; @@ -8,6 +9,7 @@ interface AuthModalProps { export default function AuthModal({ onClose }: AuthModalProps) { const { signIn, signUp } = useAuth(); + const { t } = useLanguage(); const [isSignUp, setIsSignUp] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -41,7 +43,7 @@ export default function AuthModal({ onClose }: AuthModalProps) {

- {isSignUp ? '注册账号' : '登录账号'} + {isSignUp ? t.auth.signUpTitle : t.auth.signInTitle}

@@ -101,7 +103,7 @@ export default function AuthModal({ onClose }: AuthModalProps) { onClick={() => setIsSignUp(!isSignUp)} className="text-sm text-blue-600 hover:text-blue-700" > - {isSignUp ? '已有账号?去登录' : '没有账号?去注册'} + {isSignUp ? t.auth.hasAccount : t.auth.noAccount}
diff --git a/src/components/ExportSidebar.tsx b/src/components/ExportSidebar.tsx index c9c7084..50a8adf 100644 --- a/src/components/ExportSidebar.tsx +++ b/src/components/ExportSidebar.tsx @@ -5,6 +5,7 @@ import { convertMathmlToOmml, wrapOmmlForClipboard } from '../lib/ommlConverter' import { generateImageFromElement, copyImageToClipboard, downloadImage } from '../lib/imageGenerator'; import { API_BASE_URL } from '../config/env'; import { tokenManager } from '../lib/api'; +import { useLanguage } from '../contexts/LanguageContext'; interface ExportSidebarProps { isOpen: boolean; @@ -24,6 +25,7 @@ interface ExportOption { } export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebarProps) { + const { t } = useLanguage(); const [copiedId, setCopiedId] = useState(null); const [exportingId, setExportingId] = useState(null); @@ -127,7 +129,7 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar }, 1000); } catch (err) { console.error('Export failed:', err); - alert('导出失败,请重试'); + alert(t.export.failed); } finally { setExportingId(null); } @@ -160,7 +162,7 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar }, 1000); } catch (err) { console.error('Failed to generate image:', err); - alert(`生成图片失败: ${err}`); + alert(`${t.export.imageFailed}: ${err}`); } finally { setExportingId(null); } @@ -228,9 +230,9 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar }; const categories: { id: ExportCategory; icon: LucideIcon; label: string }[] = [ - { id: 'Code', icon: Code2, label: 'Code' }, - { id: 'Image', icon: ImageIcon, label: 'Image' }, - { id: 'File', icon: FileText, label: 'File' }, + { id: 'Code', icon: Code2, label: t.export.categories.code }, + { id: 'Image', icon: ImageIcon, label: t.export.categories.image }, + { id: 'File', icon: FileText, label: t.export.categories.file }, ]; return ( @@ -251,7 +253,7 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar `} >
-

Export

+

{t.export.title}

diff --git a/src/components/FilePreview.tsx b/src/components/FilePreview.tsx index c2247df..1d7a579 100644 --- a/src/components/FilePreview.tsx +++ b/src/components/FilePreview.tsx @@ -1,12 +1,14 @@ import { useState } from 'react'; import { ChevronLeft, ChevronRight, File as FileIcon, MinusCircle, PlusCircle } from 'lucide-react'; import { FileRecord } from '../types'; +import { useLanguage } from '../contexts/LanguageContext'; interface FilePreviewProps { file: FileRecord | null; } export default function FilePreview({ file }: FilePreviewProps) { + const { t } = useLanguage(); const [zoom, setZoom] = useState(100); const [page, setPage] = useState(1); const totalPages = 1; @@ -16,13 +18,13 @@ export default function FilePreview({ file }: FilePreviewProps) { if (!file) { return ( -
+
-

Upload file

+

{t.common.upload}

- Click, Drop, or Paste a file to start parsing + {t.sidebar.uploadInstruction}

); @@ -68,7 +70,7 @@ export default function FilePreview({ file }: FilePreviewProps) { @@ -78,7 +80,7 @@ export default function FilePreview({ file }: FilePreviewProps) { diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index 569496a..b447cdd 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { Upload, LogIn, LogOut, FileText, Clock, ChevronLeft, ChevronRight, Settings, History, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; +import { useLanguage } from '../contexts/LanguageContext'; import { FileRecord } from '../types'; import AuthModal from './AuthModal'; @@ -30,11 +31,13 @@ export default function LeftSidebar({ onLoadMore, }: LeftSidebarProps) { const { user, signOut } = useAuth(); + const { t } = useLanguage(); const [showAuthModal, setShowAuthModal] = useState(false); const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); const listRef = useRef(null); + // ... (rest of the logic remains the same) // Handle scroll to load more const handleScroll = useCallback(() => { if (!listRef.current || loadingMore || !hasMore) return; @@ -116,13 +119,13 @@ export default function LeftSidebar({
-
@@ -130,7 +133,7 @@ export default function LeftSidebar({ @@ -145,8 +148,8 @@ export default function LeftSidebar({
-

Formula Recognize

-

Support handwriting and printed formulas

+

{t.sidebar.title}

+

{t.sidebar.subtitle}

-
+ {/* Middle Area: History */} -
+ diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 1368f62..d32c28b 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,11 +1,15 @@ import { useState, useRef, useEffect } from 'react'; -import { Mail, Users, MessageCircle, ChevronDown, Check, Heart, X } from 'lucide-react'; +import { Mail, Users, MessageCircle, ChevronDown, Check, Heart, X, Languages, HelpCircle } from 'lucide-react'; +import { useLanguage } from '../contexts/LanguageContext'; export default function Navbar() { + const { language, setLanguage, t } = useLanguage(); const [showContact, setShowContact] = useState(false); const [showReward, setShowReward] = useState(false); + const [showLangMenu, setShowLangMenu] = useState(false); const [copied, setCopied] = useState(false); const dropdownRef = useRef(null); + const langMenuRef = useRef(null); const handleCopyQQ = async () => { await navigator.clipboard.writeText('1018282100'); @@ -19,6 +23,9 @@ export default function Navbar() { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setShowContact(false); } + if (langMenuRef.current && !langMenuRef.current.contains(event.target as Node)) { + setShowLangMenu(false); + } }; document.addEventListener('mousedown', handleClickOutside); @@ -37,8 +44,59 @@ export default function Navbar() {
- {/* Right: Reward & Contact Buttons */} + {/* Right: Actions */}
+ {/* Language Switcher */} +
+ + + {showLangMenu && ( +
+ + +
+ )} +
+ + {/* User Guide Button */} + + {/* Reward Button */}
{/* Reward Modal */} @@ -60,7 +118,7 @@ export default function Navbar() { onClick={e => e.stopPropagation()} >
- 微信赞赏码 + {t.navbar.rewardTitle}
@@ -91,7 +149,7 @@ export default function Navbar() { className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-700 text-sm font-medium transition-colors" > - Contact Us + {t.common.contactUs}
-
Email
+
{t.common.email}
yogecoder@gmail.com
@@ -126,7 +184,7 @@ export default function Navbar() {
- {copied ? 'Copied!' : 'QQ Group (Click to Copy)'} + {copied ? t.common.copied : t.common.qqGroup}
1018282100
diff --git a/src/components/ResultPanel.tsx b/src/components/ResultPanel.tsx index 807d238..50c091f 100644 --- a/src/components/ResultPanel.tsx +++ b/src/components/ResultPanel.tsx @@ -7,6 +7,7 @@ import rehypeKatex from 'rehype-katex'; import 'katex/dist/katex.min.css'; import { RecognitionResult } from '../types'; import ExportSidebar from './ExportSidebar'; +import { useLanguage } from '../contexts/LanguageContext'; interface ResultPanelProps { result: RecognitionResult | null; @@ -67,6 +68,7 @@ function preprocessLatex(content: string): string { } export default function ResultPanel({ result, fileStatus }: ResultPanelProps) { + const { t } = useLanguage(); const [isExportSidebarOpen, setIsExportSidebarOpen] = useState(false); if (!result) { @@ -75,44 +77,45 @@ export default function ResultPanel({ result, fileStatus }: ResultPanelProps) {

- {fileStatus === 'pending' ? 'Waiting in queue...' : 'Analyzing...'} + {fileStatus === 'pending' ? t.resultPanel.waitingQueue : t.resultPanel.analyzing}

{fileStatus === 'pending' - ? 'Your file is in the queue, please wait.' - : 'Texpixel is processing your file, this may take a moment.'} + ? t.resultPanel.queueSubtitle + : t.resultPanel.processingSubtitle}

); } return ( -
+
-

Waiting for recognition result

+

{t.resultPanel.waitingTitle}

- After uploading the file, Texpixel will automatically recognize and display the result here + {t.resultPanel.waitingSubtitle}

); } return ( -
+
{/* Top Header */}
-

Markdown

+

{t.resultPanel.markdown}

diff --git a/src/components/UploadModal.tsx b/src/components/UploadModal.tsx index 11934d7..e0f3a08 100644 --- a/src/components/UploadModal.tsx +++ b/src/components/UploadModal.tsx @@ -1,5 +1,6 @@ import { useCallback, useState, useEffect, useRef } from 'react'; import { Upload, X, MousePointerClick, FileUp, ClipboardPaste } from 'lucide-react'; +import { useLanguage } from '../contexts/LanguageContext'; interface UploadModalProps { onClose: () => void; @@ -7,6 +8,7 @@ interface UploadModalProps { } export default function UploadModal({ onClose, onUpload }: UploadModalProps) { + const { t } = useLanguage(); const [dragActive, setDragActive] = useState(false); const fileInputRef = useRef(null); @@ -75,7 +77,7 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
-

上传文件

+

{t.uploadModal.title}

+

{t.sidebar.uploadInstruction}

-

- Support JPG, PNG format +

+ {t.uploadModal.supportFormats}

- Click + {t.common.click}
- Drop + {t.common.drop}
- Paste + {t.common.paste}
diff --git a/src/components/UserGuide.tsx b/src/components/UserGuide.tsx new file mode 100644 index 0000000..8e45703 --- /dev/null +++ b/src/components/UserGuide.tsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { X, ChevronRight, ChevronLeft } from 'lucide-react'; +import { useLanguage } from '../contexts/LanguageContext'; + +interface Step { + id: string; + title: string; + content: string; + position: 'top' | 'bottom' | 'left' | 'right'; +} + +export default function UserGuide({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { + const { t } = useLanguage(); + const [currentStep, setCurrentStep] = useState(0); + const [highlightStyle, setHighlightStyle] = useState({}); + + const steps: Step[] = [ + { + id: 'sidebar-upload-area', + title: t.guide.step1Title, + content: t.guide.step1Content, + position: 'right', + }, + { + id: 'sidebar-history', + title: t.guide.step2Title, + content: t.guide.step2Content, + position: 'right', + }, + { + id: 'file-preview-empty', + title: t.guide.step3Title, + content: t.guide.step3Content, + position: 'right', + }, + { + id: 'result-empty-state', + title: t.guide.step4Title, + content: t.guide.step4Content, + position: 'left', + }, + { + id: 'export-button', + title: t.guide.stepExportTitle, + content: t.guide.stepExportContent, + position: 'left', + }, + ]; + + const updateHighlight = useCallback(() => { + if (!isOpen || steps.length === 0) return; + + const element = document.getElementById(steps[currentStep].id); + if (element) { + const rect = element.getBoundingClientRect(); + setHighlightStyle({ + top: rect.top - 8, + left: rect.left - 8, + width: rect.width + 16, + height: rect.height + 16, + opacity: 1, + }); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else { + setHighlightStyle({ opacity: 0 }); + } + }, [currentStep, isOpen, steps, t.guide]); + + useEffect(() => { + if (isOpen) { + updateHighlight(); + window.addEventListener('resize', updateHighlight); + } + return () => window.removeEventListener('resize', updateHighlight); + }, [isOpen, updateHighlight]); + + if (!isOpen) return null; + + const handleNext = () => { + if (currentStep < steps.length - 1) { + setCurrentStep(currentStep + 1); + } else { + onClose(); + setCurrentStep(0); + } + }; + + const handlePrev = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + return ( +
+ {/* Backdrop with hole */} +
+ + {/* Highlight border */} +
+ + {/* Tooltip */} +
+ + +
+ + Step {currentStep + 1} of {steps.length} + +

{steps[currentStep].title}

+

+ {steps[currentStep].content} +

+
+ +
+ + + +
+
+
+ ); +} diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx new file mode 100644 index 0000000..5aed3ff --- /dev/null +++ b/src/contexts/LanguageContext.tsx @@ -0,0 +1,39 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { translations, Language, TranslationKey } from '../lib/translations'; + +interface LanguageContextType { + language: Language; + setLanguage: (lang: Language) => void; + t: TranslationKey; +} + +const LanguageContext = createContext(undefined); + +export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [language, setLanguageState] = useState(() => { + const saved = localStorage.getItem('language'); + if (saved === 'en' || saved === 'zh') return saved; + return navigator.language.startsWith('zh') ? 'zh' : 'en'; + }); + + const setLanguage = (lang: Language) => { + setLanguageState(lang); + localStorage.setItem('language', lang); + }; + + const t = translations[language]; + + return ( + + {children} + + ); +}; + +export const useLanguage = () => { + const context = useContext(LanguageContext); + if (context === undefined) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +}; diff --git a/src/lib/translations.ts b/src/lib/translations.ts new file mode 100644 index 0000000..b5564fd --- /dev/null +++ b/src/lib/translations.ts @@ -0,0 +1,193 @@ +export const translations = { + en: { + common: { + upload: 'Upload', + history: 'History', + login: 'Login / Register', + logout: 'Logout', + loading: 'Loading...', + processing: 'Processing...', + cancel: 'Cancel', + copy: 'Copy', + copied: 'Copied!', + download: 'Download', + export: 'Export', + preview: 'Preview', + email: 'Email', + contactUs: 'Contact Us', + reward: 'Reward', + qqGroup: 'QQ Group (Click to Copy)', + guide: 'User Guide', + click: 'Click', + drop: 'Drop', + paste: 'Paste', + }, + navbar: { + rewardTitle: 'WeChat Reward', + rewardThanks: 'Thank you for your support and encouragement ❤️', + rewardSubtitle: 'Your support is our motivation for continuous updates', + }, + sidebar: { + title: 'Formula Recognize', + subtitle: 'Support handwriting and printed formulas', + uploadInstruction: 'Click, Drop, or Paste a file to start parsing', + pleaseLogin: 'Please login to view history', + noHistory: 'No history records', + noMore: 'No more records', + historyHeader: 'History', + }, + uploadModal: { + title: 'Upload File', + supportFormats: 'Support JPG, PNG, PDF format', + }, + resultPanel: { + waitingTitle: 'Waiting for recognition result', + waitingSubtitle: 'After uploading the file, Texpixel will automatically recognize and display the result here', + analyzing: 'Analyzing...', + waitingQueue: 'Waiting in queue...', + queueSubtitle: 'Your file is in the queue, please wait.', + processingSubtitle: 'Texpixel is processing your file, this may take a moment.', + markdown: 'Markdown', + }, + auth: { + signIn: 'Login', + signUp: 'Register', + signInTitle: 'Login Account', + signUpTitle: 'Register Account', + email: 'Email', + password: 'Password', + error: 'Error', + genericError: 'An error occurred, please try again', + hasAccount: 'Already have an account? Login', + noAccount: 'No account? Register', + }, + export: { + title: 'Export', + categories: { + code: 'Code', + image: 'Image', + file: 'File', + }, + failed: 'Export failed, please try again', + imageFailed: 'Failed to generate image', + }, + guide: { + next: 'Next', + prev: 'Back', + finish: 'Finish', + skip: 'Skip', + step1Title: 'Upload Area', + step1Content: 'Click or drag and drop your formula images/PDFs here to start recognition.', + step2Title: 'File History', + step2Content: 'Your recognized files will appear here. Login to sync across devices.', + step3Title: 'Preview Area', + step3Content: 'The original file you uploaded will be displayed here for comparison.', + step4Title: 'Recognition Result', + step4Content: 'The recognition results (Markdown/LaTeX) will be shown here.', + stepExportTitle: 'Export Result', + stepExportContent: 'You can export the recognition results to various formats such as Markdown, LaTeX, Word, or Image.', + }, + alerts: { + taskTimeout: 'Task timeout: Recognition took too long.', + networkError: 'Task timeout or network error.', + uploadFailed: 'Upload failed', + } + }, + zh: { + common: { + upload: '上传', + history: '历史记录', + login: '登录 / 注册', + logout: '退出登录', + loading: '加载中...', + processing: '处理中...', + cancel: '取消', + copy: '复制', + copied: '已复制!', + download: '下载', + export: '导出', + preview: '预览', + email: '邮箱', + contactUs: '联系我们', + reward: '赞赏', + qqGroup: 'QQ 群 (点击复制)', + guide: '使用引导', + click: '点击', + drop: '拖拽', + paste: '粘贴', + }, + navbar: { + rewardTitle: '微信赞赏码', + rewardThanks: '感谢您的支持与鼓励 ❤️', + rewardSubtitle: '您的支持是我们持续更新的动力', + }, + sidebar: { + title: '文档识别', + subtitle: '支持手写和印刷体文档识别', + uploadInstruction: '点击、拖拽或粘贴文件开始解析', + pleaseLogin: '请登录后查看历史记录', + noHistory: '暂无历史记录', + noMore: '没有更多记录了', + historyHeader: '历史记录', + }, + uploadModal: { + title: '上传文件', + supportFormats: '支持 JPG, PNG 格式', + }, + resultPanel: { + waitingTitle: '等待识别结果', + waitingSubtitle: '上传文件后,TexPixel 将自动识别并在此显示结果', + analyzing: '解析中...', + waitingQueue: '排队中...', + queueSubtitle: '您的文件正在排队,请稍候。', + processingSubtitle: 'TexPixel 正在处理您的文件,请稍候。', + markdown: 'Markdown', + }, + auth: { + signIn: '登录', + signUp: '注册', + signInTitle: '登录账号', + signUpTitle: '注册账号', + email: '邮箱', + password: '密码', + error: '错误', + genericError: '发生错误,请重试', + hasAccount: '已有账号?去登录', + noAccount: '没有账号?去注册', + }, + export: { + title: '导出', + categories: { + code: '代码', + image: '图片', + file: '文件', + }, + failed: '导出失败,请重试', + imageFailed: '生成图片失败', + }, + guide: { + next: '下一步', + prev: '上一步', + finish: '完成', + skip: '跳过', + step1Title: '上传区域', + step1Content: '点击此处或将公式/文档图片 粘贴或者拖拽到这里开始识别。', + step2Title: '历史记录', + step2Content: '识别过的文件会显示在这里。登录后可以跨设备同步。', + step3Title: '预览区域', + step3Content: '这里会显示您上传的原始文件,方便对比。', + step4Title: '识别结果', + step4Content: '这里会显示识别出的 Markdown/LaTeX 结果。', + stepExportTitle: '导出结果', + stepExportContent: '您可以将识别结果导出为多种格式,如 Markdown、LaTeX、Word 或图片。', + }, + alerts: { + taskTimeout: '任务超时:识别时间过长。', + networkError: '任务超时或网络错误。', + uploadFailed: '上传失败', + } + } +}; + +export type Language = 'en' | 'zh'; +export type TranslationKey = typeof translations.en; diff --git a/src/main.tsx b/src/main.tsx index 9d82f18..708a9c5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import './index.css'; import { AuthProvider } from './contexts/AuthContext'; +import { LanguageProvider } from './contexts/LanguageContext'; createRoot(document.getElementById('root')!).render( - + + + );