From cdf3621cbf3662975ccb719a79af45a326051907 Mon Sep 17 00:00:00 2001 From: zxstar7789 <191852478+zxstar7789@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:46:28 +0800 Subject: [PATCH] feat(frontend): add Chinese UI language toggle --- frontend/src/components/Chat/ChatArea.tsx | 24 +- frontend/src/components/Chat/InputArea.tsx | 42 +- .../src/components/Chat/MessageBubble.tsx | 7 +- frontend/src/components/Chat/SystemPanel.tsx | 34 +- frontend/src/components/Sidebar/Sidebar.tsx | 32 +- frontend/src/lib/i18n.ts | 429 ++++++++++++++++++ frontend/src/lib/store.ts | 3 + frontend/src/pages/AgentsPage.tsx | 36 +- frontend/src/pages/DashboardPage.tsx | 6 +- frontend/src/pages/DataSourcesPage.tsx | 12 +- frontend/src/pages/GetStartedPage.tsx | 65 ++- frontend/src/pages/LogsPage.tsx | 14 +- frontend/src/pages/SettingsPage.tsx | 143 +++--- 13 files changed, 659 insertions(+), 188 deletions(-) create mode 100644 frontend/src/lib/i18n.ts diff --git a/frontend/src/components/Chat/ChatArea.tsx b/frontend/src/components/Chat/ChatArea.tsx index 5df8b08c6..5e758e75a 100644 --- a/frontend/src/components/Chat/ChatArea.tsx +++ b/frontend/src/components/Chat/ChatArea.tsx @@ -6,12 +6,13 @@ import { StreamingDots } from './StreamingDots'; import { useAppStore } from '../../lib/store'; import { Sparkles, PanelRightOpen, PanelRightClose, Database, MessageSquare, X } from 'lucide-react'; import { listConnectors } from '../../lib/connectors-api'; +import { useI18n, type TranslationKey } from '../../lib/i18n'; -function getGreeting(): string { +function getGreetingKey(): TranslationKey { const hour = new Date().getHours(); - if (hour < 12) return 'Good morning'; - if (hour < 18) return 'Good afternoon'; - return 'Good evening'; + if (hour < 12) return 'chat.greetingMorning'; + if (hour < 18) return 'chat.greetingAfternoon'; + return 'chat.greetingEvening'; } export function ChatArea() { @@ -20,6 +21,7 @@ export function ChatArea() { const systemPanelOpen = useAppStore((s) => s.systemPanelOpen); const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel); const navigate = useNavigate(); + const { t } = useI18n(); const listRef = useRef(null); const shouldAutoScroll = useRef(true); @@ -57,7 +59,7 @@ export function ChatArea() { onClick={toggleSystemPanel} className="p-1.5 rounded-md transition-colors cursor-pointer" style={{ color: 'var(--color-text-tertiary)' }} - title={`${systemPanelOpen ? 'Hide' : 'Show'} system panel (${navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+I)`} + title={`${systemPanelOpen ? t('chat.hideSystemPanel') : t('chat.showSystemPanel')} (${navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+I)`} > @@ -74,14 +76,14 @@ export function ChatArea() { > - Connect your data sources (Gmail, iMessage, Slack, etc.) to get personalized answers. + {t('chat.dataSourcesBanner')} diff --git a/frontend/src/components/Chat/InputArea.tsx b/frontend/src/components/Chat/InputArea.tsx index 31fa20f7f..544fd3a2a 100644 --- a/frontend/src/components/Chat/InputArea.tsx +++ b/frontend/src/components/Chat/InputArea.tsx @@ -7,6 +7,7 @@ import { fetchSavings, getBase } from '../../lib/api'; import { listConnectors, getSyncStatus } from '../../lib/connectors-api'; import { MicButton } from './MicButton'; import { useSpeech } from '../../hooks/useSpeech'; +import { useI18n } from '../../lib/i18n'; import type { ChatMessage, MessageTelemetry, @@ -79,6 +80,7 @@ export function InputArea() { const textareaRef = useRef(null); const abortRef = useRef(null); const timerRef = useRef | null>(null); + const { t } = useI18n(); const activeId = useAppStore((s) => s.activeId); const selectedModel = useAppStore((s) => s.selectedModel); @@ -157,7 +159,7 @@ export function InputArea() { const content = input.trim(); if (!content || streamState.isStreaming) return; if (!selectedModel) { - toast.error('Pick a model first (⌘K)'); + toast.error(t('chat.modelRequiredToast')); return; } @@ -215,7 +217,7 @@ export function InputArea() { setStreamState({ isStreaming: true, - phase: deepResearch ? 'Researching...' : 'Generating...', + phase: deepResearch ? t('chat.researching') : t('chat.generating'), elapsedMs: 0, activeToolCalls: [], content: '', @@ -241,7 +243,7 @@ export function InputArea() { status: 'pending', }; researchTraces.push(trace); - setStreamState({ phase: `Searching: ${trace.query}` }); + setStreamState({ phase: t('chat.searching', { query: trace.query }) }); updateLastAssistant( convId, accumulatedContent, @@ -313,10 +315,10 @@ export function InputArea() { // missing, KnowledgeStore locked, etc.). Without surfacing the // message, the user sees only the generic "No response was // generated" fallback and has no way to self-diagnose. - const msg = ev.message || 'Research failed (no detail provided)'; + const msg = ev.message || `${t('chat.researchFailed')} (no detail provided)`; accumulatedContent = accumulatedContent - ? `${accumulatedContent}\n\n**Research stopped:** ${msg}` - : `**Research failed:** ${msg}`; + ? `${accumulatedContent}\n\n**${t('chat.researchStopped')}:** ${msg}` + : `**${t('chat.researchFailed')}:** ${msg}`; setStreamState({ content: accumulatedContent, phase: '' }); useAppStore.getState().addLogEntry({ timestamp: Date.now(), @@ -358,9 +360,9 @@ export function InputArea() { const eventName = sseEvent.event; if (eventName === 'agent_turn_start') { - setStreamState({ phase: 'Agent thinking...' }); + setStreamState({ phase: t('chat.agentThinking') }); } else if (eventName === 'inference_start') { - setStreamState({ phase: 'Generating...' }); + setStreamState({ phase: t('chat.generating') }); useAppStore.getState().addLogEntry({ timestamp: Date.now(), level: 'info', category: 'chat', message: `Generating with ${selectedModel}...`, @@ -397,7 +399,7 @@ export function InputArea() { tc.result = data.result; } setStreamState({ - phase: 'Generating...', + phase: t('chat.generating'), activeToolCalls: [...toolCalls], }); updateLastAssistant(convId, accumulatedContent, [...toolCalls]); @@ -446,7 +448,7 @@ export function InputArea() { useAppStore.getState().setLiveEnergy(null); } finally { if (!accumulatedContent) { - accumulatedContent = 'No response was generated. Please try again.'; + accumulatedContent = t('chat.errorNoResponse'); } const totalMs = Date.now() - startTime; const _CLOUD_PREFIXES = ['gpt-', 'o1-', 'o3-', 'o4-', 'claude-', 'gemini-', 'openrouter/', 'MiniMax-', 'chatgpt-']; @@ -521,6 +523,7 @@ export function InputArea() { deepResearch, temperature, maxTokens, + t, ]); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -545,10 +548,10 @@ export function InputArea() { border: `1px solid ${deepResearch ? 'var(--color-accent)' : 'var(--color-border)'}`, color: deepResearch ? 'var(--color-accent)' : 'var(--color-text-tertiary)', }} - title={deepResearch ? 'Deep Research: on' : 'Deep Research: off'} + title={deepResearch ? t('chat.deepResearchOn') : t('chat.deepResearchOff')} > - Deep Research + {t('sidebar.deepResearch')} {deepResearch && corpusSync.syncing && corpusSync.itemsSynced > 0 && ( @@ -556,11 +559,7 @@ export function InputArea() { className="text-[11px] leading-snug" style={{ color: 'var(--color-text-tertiary)' }} > - Searching over{' '} - - {corpusSync.itemsSynced.toLocaleString()} - {' '} - items — sync in progress, results will improve as more data is indexed. + {t('chat.searchingCorpus', { count: corpusSync.itemsSynced.toLocaleString() })} )} @@ -577,7 +576,7 @@ export function InputArea() { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} - placeholder={selectedModel ? 'Message OpenJarvis...' : 'Pick a model first (⌘K)...'} + placeholder={selectedModel ? t('chat.messagePlaceholder') : t('chat.pickModelPlaceholder')} rows={1} className="flex-1 bg-transparent outline-none resize-none text-sm leading-relaxed" style={{ color: 'var(--color-text)', maxHeight: '200px' }} @@ -588,7 +587,7 @@ export function InputArea() { onClick={stopStreaming} className="p-2 rounded-xl transition-colors shrink-0 cursor-pointer" style={{ background: 'var(--color-error)', color: 'var(--color-on-accent)' }} - title="Stop generating" + title={t('chat.stopGenerating')} > @@ -603,7 +602,7 @@ export function InputArea() {
@@ -81,6 +83,7 @@ function CodeBlockPre({ children, ...props }: any) {
 
 function CopyMessageButton({ content }: { content: string }) {
   const [copied, setCopied] = useState(false);
+  const { t } = useI18n();
 
   const handleCopy = () => {
     navigator.clipboard.writeText(content);
@@ -93,7 +96,7 @@ function CopyMessageButton({ content }: { content: string }) {
       onClick={handleCopy}
       className="p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
       style={{ color: 'var(--color-text-tertiary)' }}
-      title="Copy message"
+      title={t('message.copyMessage')}
     >
       {copied ?  : }
     
diff --git a/frontend/src/components/Chat/SystemPanel.tsx b/frontend/src/components/Chat/SystemPanel.tsx
index 0fc2a8662..184e4c138 100644
--- a/frontend/src/components/Chat/SystemPanel.tsx
+++ b/frontend/src/components/Chat/SystemPanel.tsx
@@ -14,6 +14,7 @@ import {
 } from 'lucide-react';
 import { useAppStore } from '../../lib/store';
 import { getBase } from '../../lib/api';
+import { useI18n } from '../../lib/i18n';
 
 interface EnergyData {
   total_energy_j?: number;
@@ -35,6 +36,7 @@ const CLOUD_PRICING = [
 ];
 
 export function SystemPanel() {
+  const { t } = useI18n();
   const savings = useAppStore((s) => s.savings);
   const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel);
   const optInEnabled = useAppStore((s) => s.optInEnabled);
@@ -91,13 +93,13 @@ export function SystemPanel() {
         style={{ borderBottom: '1px solid var(--color-border)' }}
       >
         
-          System
+          {t('system.system')}
         
         
@@ -107,35 +109,35 @@ export function SystemPanel() {
         {/* Session Stats */}
         

- Session + {t('system.session')}

- - + +
{/* Device */}

- Device + {t('system.device')}

{energy?.cpu_temp_c != null && ( - + )} {energy?.gpu_temp_c != null && ( - + )}

- Cost Comparison + {t('system.costComparison')}

{/* Local */} @@ -158,7 +160,7 @@ export function SystemPanel() { >
-
Local
+
{t('system.local')}
${(savings?.local_cost ?? 0).toFixed(4)} @@ -216,7 +218,7 @@ export function SystemPanel() { className="text-[11px] font-medium uppercase tracking-wide mb-2" style={{ color: 'var(--color-text-tertiary)' }} > - Leaderboard + {t('system.leaderboard')} @@ -266,7 +268,7 @@ export function SystemPanel() { onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--color-text-tertiary)')} > - View Leaderboard + {t('system.viewLeaderboard')}
diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index a4a5b6708..842f08b07 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -20,11 +20,13 @@ import { } from 'lucide-react'; import { ConversationList } from './ConversationList'; import { useAppStore } from '../../lib/store'; +import { useI18n, type TranslationKey } from '../../lib/i18n'; export function Sidebar() { const navigate = useNavigate(); const location = useLocation(); const [searchQuery, setSearchQuery] = useState(''); + const { t } = useI18n(); const sidebarOpen = useAppStore((s) => s.sidebarOpen); const toggleSidebar = useAppStore((s) => s.toggleSidebar); @@ -52,14 +54,14 @@ export function Sidebar() { navigate('/'); }; - const navItems = [ - { path: '/', icon: MessageSquare, label: 'Chat' }, - { path: '/dashboard', icon: BarChart3, label: 'Dashboard' }, - { path: '/data-sources', icon: Database, label: 'Data Sources' }, - { path: '/agents', icon: Bot, label: 'Agents' }, - { path: '/logs', icon: ScrollText, label: 'Logs' }, - { path: '/settings', icon: Settings, label: 'Settings' }, - { path: '/get-started', icon: Rocket, label: 'Get Started' }, + const navItems: Array<{ path: string; icon: typeof MessageSquare; labelKey: TranslationKey }> = [ + { path: '/', icon: MessageSquare, labelKey: 'nav.chat' }, + { path: '/dashboard', icon: BarChart3, labelKey: 'nav.dashboard' }, + { path: '/data-sources', icon: Database, labelKey: 'nav.dataSources' }, + { path: '/agents', icon: Bot, labelKey: 'nav.agents' }, + { path: '/logs', icon: ScrollText, labelKey: 'nav.logs' }, + { path: '/settings', icon: Settings, labelKey: 'nav.settings' }, + { path: '/get-started', icon: Rocket, labelKey: 'nav.getStarted' }, ]; return ( @@ -109,7 +111,7 @@ export function Sidebar() { style={{ color: 'var(--color-text-secondary)' }} onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-bg-tertiary)')} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} - title={`Theme: ${settings.theme} (click for ${nextTheme})`} + title={t('sidebar.themeTitle', { theme: settings.theme, nextTheme })} > @@ -119,7 +121,7 @@ export function Sidebar() { style={{ color: 'var(--color-text-secondary)' }} onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-bg-tertiary)')} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} - title="New chat" + title={t('sidebar.newChat')} > @@ -149,12 +151,12 @@ export function Sidebar() { style={{ color: deepResearch ? 'var(--color-accent)' : 'var(--color-text)' }} > {deepResearch - ? 'Deep Research' - : selectedModel || serverInfo?.model || 'Select model'} + ? t('sidebar.deepResearch') + : selectedModel || serverInfo?.model || t('sidebar.selectModel')} {modelLoading && ( - Loading model... + {t('sidebar.loadingModel')} )} @@ -177,7 +179,7 @@ export function Sidebar() { setSearchQuery(e.target.value)} className="flex-1 bg-transparent outline-none text-sm" @@ -223,7 +225,7 @@ export function Sidebar() { /> )} - {item.label} + {t(item.labelKey)} ); })} diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts new file mode 100644 index 000000000..893e3ef04 --- /dev/null +++ b/frontend/src/lib/i18n.ts @@ -0,0 +1,429 @@ +import { useAppStore } from './store'; + +export type Locale = 'en-US' | 'zh-CN'; + +type Vars = Record; + +const enUS = { + 'common.available': 'Available', + 'common.cancel': 'Cancel', + 'common.checking': 'Checking...', + 'common.clear': 'Clear', + 'common.close': 'Close', + 'common.connected': 'Connected', + 'common.connect': 'Connect', + 'common.copy': 'Copy', + 'common.copied': 'Copied', + 'common.disconnected': 'Disconnected', + 'common.export': 'Export', + 'common.import': 'Import', + 'common.notConfigured': 'Not configured', + 'common.save': 'Save', + 'common.saved': 'Saved', + 'common.unavailable': 'Unavailable', + 'common.off': 'OFF', + 'common.on': 'ON', + + 'nav.agents': 'Agents', + 'nav.chat': 'Chat', + 'nav.dashboard': 'Dashboard', + 'nav.dataSources': 'Data Sources', + 'nav.getStarted': 'Get Started', + 'nav.logs': 'Logs', + 'nav.settings': 'Settings', + + 'sidebar.deepResearch': 'Deep Research', + 'sidebar.loadingModel': 'Loading model...', + 'sidebar.newChat': 'New chat', + 'sidebar.searchChats': 'Search chats...', + 'sidebar.selectModel': 'Select model', + 'sidebar.themeTitle': 'Theme: {theme} (click for {nextTheme})', + + 'chat.agentThinking': 'Agent thinking...', + 'chat.askAnything': 'Ask anything. Your AI runs locally — private, fast, and always available.', + 'chat.connectDataSources': 'Connect Data Sources', + 'chat.dataSourcesBanner': 'Connect your data sources (Gmail, iMessage, Slack, etc.) to get personalized answers.', + 'chat.deepResearchOff': 'Deep Research: off', + 'chat.deepResearchOn': 'Deep Research: on', + 'chat.errorNoResponse': 'No response was generated. Please try again.', + 'chat.generating': 'Generating...', + 'chat.greetingAfternoon': 'Good afternoon', + 'chat.greetingEvening': 'Good evening', + 'chat.greetingMorning': 'Good morning', + 'chat.inputHelp': 'Enter to send · Shift+Enter for new line', + 'chat.messagePlaceholder': 'Message OpenJarvis...', + 'chat.modelRequiredToast': 'Pick a model first (⌘K)', + 'chat.pickModelPlaceholder': 'Pick a model first (⌘K)...', + 'chat.researchFailed': 'Research failed', + 'chat.researching': 'Researching...', + 'chat.researchStopped': 'Research stopped', + 'chat.searching': 'Searching: {query}', + 'chat.searchingCorpus': 'Searching over {count} items — sync in progress, results will improve as more data is indexed.', + 'chat.sendMessage': 'Send message', + 'chat.setUpMessaging': 'Set Up Messaging Channels', + 'chat.showSystemPanel': 'Show system panel', + 'chat.hideSystemPanel': 'Hide system panel', + 'chat.stopGenerating': 'Stop generating', + + 'message.copyMessage': 'Copy message', + + 'system.closePanel': 'Close panel', + 'system.costComparison': 'Cost Comparison', + 'system.cpuTemp': 'CPU Temp', + 'system.device': 'Device', + 'system.energy': 'Energy', + 'system.gpuTemp': 'GPU Temp', + 'system.leaderboard': 'Leaderboard', + 'system.local': 'Local', + 'system.outputTokens': 'Output Tokens', + 'system.power': 'Power', + 'system.requests': 'Requests', + 'system.session': 'Session', + 'system.shareSavings': 'Share Your Savings', + 'system.sharingSavings': 'Sharing Savings', + 'system.system': 'System', + 'system.viewLeaderboard': 'View Leaderboard', + + 'settings.apiKey': 'API key', + 'settings.apiKeyDescription': 'Required only if the server was started with an API key', + 'settings.apiKeys': 'API Keys', + 'settings.apiUrl': 'API URL', + 'settings.apiUrlDescription': 'Set if backend runs on a different port or host', + 'settings.appearance': 'Appearance', + 'settings.autoUpdate': 'Auto-update', + 'settings.autoUpdateDescription': 'Check for new desktop builds automatically every 30 minutes', + 'settings.backendStatus': 'Backend status', + 'settings.backendStatusDescription': 'Requires Whisper, Deepgram, or another speech backend', + 'settings.clearAllData': 'Clear all data', + 'settings.clearAllDataDescription': 'Permanently delete all conversations', + 'settings.clickAgain': 'Click again to confirm', + 'settings.cloudProviders': 'Cloud providers', + 'settings.cloudProvidersDescription': 'Green dot means API key is configured', + 'settings.connection': 'Connection', + 'settings.conversations': 'Conversations', + 'settings.conversationsStored': '{count} stored locally', + 'settings.data': 'Data', + 'settings.description': 'App preferences — appearance, model defaults, keyboard shortcuts, and data management.', + 'settings.fontSize': 'Font size', + 'settings.fontLarge': 'Large', + 'settings.fontDefault': 'Default', + 'settings.fontSmall': 'Small', + 'settings.inferenceSource': 'Inference source', + 'settings.language': 'Language', + 'settings.languageDescription': 'Choose the app interface language', + 'settings.localModels': 'Local models (Ollama)', + 'settings.localModelsDescription': 'Models available for local inference', + 'settings.maxContextTokens': 'Max context tokens', + 'settings.maxTokens': 'Max tokens', + 'settings.memory': 'Memory', + 'settings.memoryBackend': 'Memory backend', + 'settings.memoryBackendDescription': 'Which retrieval engine to use', + 'settings.memoryStatus': 'Memory status', + 'settings.memoryUnavailable': 'Unable to reach memory service', + 'settings.minRelevanceScore': 'Min relevance score', + 'settings.modelDefaults': 'Model Defaults', + 'settings.models': 'Models', + 'settings.noModelsLoaded': 'No models loaded', + 'settings.pageTitle': 'Settings', + 'settings.resultsToInject': 'Results to inject', + 'settings.runOllamaPull': 'Run {command} in your terminal to add more models', + 'settings.serverStatus': 'Server status', + 'settings.source': 'Source', + 'settings.sourceDescription': 'Where the app runs models. Applies after restart.', + 'settings.sourceOllama': 'Bundled Ollama (default)', + 'settings.sourceCustom': 'Custom OpenAI-compatible server', + 'settings.speech': 'Speech', + 'settings.speechSetup': 'Set up a speech backend to use voice input. See the documentation for details.', + 'settings.speechToText': 'Speech-to-Text', + 'settings.speechToTextDescription': 'Enable microphone input for voice dictation', + 'settings.temperature': 'Temperature', + 'settings.theme': 'Theme', + 'settings.themeDescription': 'Choose how OpenJarvis looks', + 'settings.themeDark': 'Dark', + 'settings.themeLight': 'Light', + 'settings.themeSystem': 'System', + 'settings.tools': 'Tools', + 'settings.updates': 'Updates', + 'settings.useMemoryContext': 'Use memory context', + 'settings.useMemoryContextDescription': 'Automatically inject relevant memories into conversations', + 'settings.webSearch': 'Web Search', + 'settings.webSearchDescription': 'SerpAPI or Tavily key for web search tool', + + 'dashboard.description': 'Live telemetry for the on-device inference engine — power draw, token throughput, and cost savings versus cloud APIs.', + 'dashboard.title': 'System Overview', + + 'logs.copyAll': 'Copy All', + 'logs.description': 'Recent activity — chat events, model switches, tool calls, and system messages from this session.', + 'logs.entries': '{count} entries', + 'logs.empty': 'No log entries yet. Logs appear as you chat, switch models, and interact with the app.', + 'logs.title': 'Logs', + + 'getStarted.allSystemsRunning': 'All systems running', + 'getStarted.browserApp': 'Browser App (Self-Hosted)', + 'getStarted.browserAppDescription': 'Launch the API server to get the full UI in your browser:', + 'getStarted.browserAppLocal': 'The chat, dashboard, energy profiling, and cost comparison all run locally on your machine.', + 'getStarted.checkingServer': 'Checking server...', + 'getStarted.cli': 'Command Line (macOS / Linux)', + 'getStarted.cliInstall': 'Clone and install (Python 3.10+ required):', + 'getStarted.desktopApp': 'Desktop App', + 'getStarted.desktopAppDescription': 'One-click install. Bundles Ollama and the server — no setup required.', + 'getStarted.desktopReady': 'Your local AI is ready. Everything runs on your device — no data leaves your machine.', + 'getStarted.docker': 'Docker (Cloud / VPS Deploy)', + 'getStarted.dockerDescription': 'Deploy with Docker Compose for a zero-setup hosted instance:', + 'getStarted.dockerServes': 'This starts both the API server and Ollama. The web UI is bundled and served automatically at port 8000.', + 'getStarted.downloadFor': 'Download for {platform}', + 'getStarted.heroDescription': 'Private AI that runs on your hardware. Chat, tools, agents, and energy profiling — no cloud required.', + 'getStarted.keyboardShortcuts': 'Keyboard Shortcuts', + 'getStarted.modelPicker': 'Model picker', + 'getStarted.noPrerequisites': 'No prerequisites — everything is bundled', + 'getStarted.or': 'Or', + 'getStarted.serverNotResponding': 'Server is not responding. The backend may be starting up.', + 'getStarted.serverRunning': 'Server is running', + 'getStarted.startChatting': 'Start Chatting', + 'getStarted.systemPanel': 'System panel', + 'getStarted.systemRequirements': 'System Requirements', + 'getStarted.thenGetStarted': 'Then get started:', + + 'agents.backToAgents': 'Back to agents', + 'agents.description': 'Long-running autonomous agents that can monitor sources, run tasks on a schedule, and message you through connected channels.', + 'agents.emptyDescription': 'Create your first agent to get started with autonomous task management.', + 'agents.emptyTitle': 'No agents yet', + 'agents.firstAgent': 'Launch your first agent', + 'agents.interact': 'Interact', + 'agents.learning': 'Learning', + 'agents.loading': 'Loading agents...', + 'agents.managerDisabled': 'Agent manager is not enabled. Set agent_manager.enabled = true in your config.', + 'agents.newAgent': 'New Agent', + 'agents.overview': 'Overview', + 'agents.tasks': 'Tasks', + 'agents.title': 'Agents', + + 'dataSources.description': 'Connect personal data so the assistant can search across everything, and set up messaging channels to chat from your phone.', + 'dataSources.memory': 'Memory', + 'dataSources.messaging': 'Messaging Channels', + 'dataSources.sources': 'Data Sources', + 'dataSources.title': 'Data Sources, Channels & Memory', +} as const; + +const zhCN: Record = { + 'common.available': '可用', + 'common.cancel': '取消', + 'common.checking': '检查中...', + 'common.clear': '清除', + 'common.close': '关闭', + 'common.connected': '已连接', + 'common.connect': '连接', + 'common.copy': '复制', + 'common.copied': '已复制', + 'common.disconnected': '未连接', + 'common.export': '导出', + 'common.import': '导入', + 'common.notConfigured': '未配置', + 'common.save': '保存', + 'common.saved': '已保存', + 'common.unavailable': '不可用', + 'common.off': '关', + 'common.on': '开', + + 'nav.agents': '智能体', + 'nav.chat': '聊天', + 'nav.dashboard': '仪表盘', + 'nav.dataSources': '数据源', + 'nav.getStarted': '开始使用', + 'nav.logs': '日志', + 'nav.settings': '设置', + + 'sidebar.deepResearch': '深度研究', + 'sidebar.loadingModel': '正在加载模型...', + 'sidebar.newChat': '新聊天', + 'sidebar.searchChats': '搜索聊天...', + 'sidebar.selectModel': '选择模型', + 'sidebar.themeTitle': '主题:{theme}(点击切换到 {nextTheme})', + + 'chat.agentThinking': '智能体思考中...', + 'chat.askAnything': '尽管提问。你的 AI 在本机运行,私密、快速、随时可用。', + 'chat.connectDataSources': '连接数据源', + 'chat.dataSourcesBanner': '连接你的数据源(Gmail、iMessage、Slack 等),获得更个性化的回答。', + 'chat.deepResearchOff': '深度研究:关闭', + 'chat.deepResearchOn': '深度研究:开启', + 'chat.errorNoResponse': '没有生成回复,请重试。', + 'chat.generating': '正在生成...', + 'chat.greetingAfternoon': '下午好', + 'chat.greetingEvening': '晚上好', + 'chat.greetingMorning': '早上好', + 'chat.inputHelp': 'Enter 发送 · Shift+Enter 换行', + 'chat.messagePlaceholder': '向 OpenJarvis 发送消息...', + 'chat.modelRequiredToast': '请先选择模型(⌘K)', + 'chat.pickModelPlaceholder': '请先选择模型(⌘K)...', + 'chat.researchFailed': '研究失败', + 'chat.researching': '正在研究...', + 'chat.researchStopped': '研究已停止', + 'chat.searching': '正在搜索:{query}', + 'chat.searchingCorpus': '正在检索 {count} 个条目。同步仍在进行,索引更多数据后结果会继续改善。', + 'chat.sendMessage': '发送消息', + 'chat.setUpMessaging': '设置消息渠道', + 'chat.showSystemPanel': '显示系统面板', + 'chat.hideSystemPanel': '隐藏系统面板', + 'chat.stopGenerating': '停止生成', + + 'message.copyMessage': '复制消息', + + 'system.closePanel': '关闭面板', + 'system.costComparison': '成本对比', + 'system.cpuTemp': 'CPU 温度', + 'system.device': '设备', + 'system.energy': '能耗', + 'system.gpuTemp': 'GPU 温度', + 'system.leaderboard': '排行榜', + 'system.local': '本地', + 'system.outputTokens': '输出 Tokens', + 'system.power': '功率', + 'system.requests': '请求数', + 'system.session': '会话', + 'system.shareSavings': '分享节省数据', + 'system.sharingSavings': '正在分享节省数据', + 'system.system': '系统', + 'system.viewLeaderboard': '查看排行榜', + + 'settings.apiKey': 'API 密钥', + 'settings.apiKeyDescription': '仅在服务端使用 API key 启动时需要', + 'settings.apiKeys': 'API 密钥', + 'settings.apiUrl': 'API 地址', + 'settings.apiUrlDescription': '后端运行在其他端口或主机时设置', + 'settings.appearance': '外观', + 'settings.autoUpdate': '自动更新', + 'settings.autoUpdateDescription': '每 30 分钟自动检查新的桌面版本', + 'settings.backendStatus': '后端状态', + 'settings.backendStatusDescription': '需要 Whisper、Deepgram 或其他语音后端', + 'settings.clearAllData': '清除所有数据', + 'settings.clearAllDataDescription': '永久删除所有会话', + 'settings.clickAgain': '再次点击确认', + 'settings.cloudProviders': '云服务提供商', + 'settings.cloudProvidersDescription': '绿点表示已配置 API key', + 'settings.connection': '连接', + 'settings.conversations': '会话', + 'settings.conversationsStored': '本地保存 {count} 个', + 'settings.data': '数据', + 'settings.description': '应用偏好设置:外观、模型默认值、快捷键和数据管理。', + 'settings.fontSize': '字体大小', + 'settings.fontLarge': '大', + 'settings.fontDefault': '默认', + 'settings.fontSmall': '小', + 'settings.inferenceSource': '推理来源', + 'settings.language': '语言', + 'settings.languageDescription': '选择应用界面语言', + 'settings.localModels': '本地模型(Ollama)', + 'settings.localModelsDescription': '可用于本地推理的模型', + 'settings.maxContextTokens': '最大上下文 Tokens', + 'settings.maxTokens': '最大 Tokens', + 'settings.memory': '记忆', + 'settings.memoryBackend': '记忆后端', + 'settings.memoryBackendDescription': '要使用的检索引擎', + 'settings.memoryStatus': '记忆状态', + 'settings.memoryUnavailable': '无法连接记忆服务', + 'settings.minRelevanceScore': '最低相关性分数', + 'settings.modelDefaults': '模型默认值', + 'settings.models': '模型', + 'settings.noModelsLoaded': '未加载模型', + 'settings.pageTitle': '设置', + 'settings.resultsToInject': '注入结果数量', + 'settings.runOllamaPull': '在终端运行 {command} 添加更多模型', + 'settings.serverStatus': '服务状态', + 'settings.source': '来源', + 'settings.sourceDescription': '应用运行模型的位置,重启后生效。', + 'settings.sourceOllama': '内置 Ollama(默认)', + 'settings.sourceCustom': '自定义 OpenAI 兼容服务', + 'settings.speech': '语音', + 'settings.speechSetup': '设置语音后端后即可使用语音输入。详情见文档。', + 'settings.speechToText': '语音转文本', + 'settings.speechToTextDescription': '启用麦克风输入进行语音听写', + 'settings.temperature': '温度', + 'settings.theme': '主题', + 'settings.themeDescription': '选择 OpenJarvis 的外观', + 'settings.themeDark': '深色', + 'settings.themeLight': '浅色', + 'settings.themeSystem': '跟随系统', + 'settings.tools': '工具', + 'settings.updates': '更新', + 'settings.useMemoryContext': '使用记忆上下文', + 'settings.useMemoryContextDescription': '自动把相关记忆注入对话', + 'settings.webSearch': '网页搜索', + 'settings.webSearchDescription': '用于网页搜索工具的 SerpAPI 或 Tavily key', + + 'dashboard.description': '本机推理引擎的实时遥测:功耗、token 吞吐量,以及相对云 API 的成本节省。', + 'dashboard.title': '系统概览', + + 'logs.copyAll': '全部复制', + 'logs.description': '当前会话的最近活动:聊天事件、模型切换、工具调用和系统消息。', + 'logs.entries': '{count} 条', + 'logs.empty': '还没有日志。聊天、切换模型或使用应用后会显示日志。', + 'logs.title': '日志', + + 'getStarted.allSystemsRunning': '所有系统正在运行', + 'getStarted.browserApp': '浏览器应用(自托管)', + 'getStarted.browserAppDescription': '启动 API 服务,即可在浏览器中使用完整界面:', + 'getStarted.browserAppLocal': '聊天、仪表盘、能耗分析和成本对比都会在你的本机运行。', + 'getStarted.checkingServer': '正在检查服务...', + 'getStarted.cli': '命令行(macOS / Linux)', + 'getStarted.cliInstall': '克隆并安装(需要 Python 3.10+):', + 'getStarted.desktopApp': '桌面应用', + 'getStarted.desktopAppDescription': '一键安装,内置 Ollama 和服务端,无需额外配置。', + 'getStarted.desktopReady': '你的本地 AI 已就绪。所有内容都在设备上运行,数据不会离开本机。', + 'getStarted.docker': 'Docker(云端 / VPS 部署)', + 'getStarted.dockerDescription': '使用 Docker Compose 部署零配置托管实例:', + 'getStarted.dockerServes': '这会同时启动 API 服务和 Ollama。Web 界面已内置,并会自动在 8000 端口提供服务。', + 'getStarted.downloadFor': '下载 {platform} 版本', + 'getStarted.heroDescription': '在你的硬件上运行的私有 AI。聊天、工具、智能体和能耗分析,无需云端。', + 'getStarted.keyboardShortcuts': '键盘快捷键', + 'getStarted.modelPicker': '模型选择器', + 'getStarted.noPrerequisites': '无需前置条件,所有内容都已内置', + 'getStarted.or': '或', + 'getStarted.serverNotResponding': '服务没有响应,后端可能仍在启动。', + 'getStarted.serverRunning': '服务正在运行', + 'getStarted.startChatting': '开始聊天', + 'getStarted.systemPanel': '系统面板', + 'getStarted.systemRequirements': '系统要求', + 'getStarted.thenGetStarted': '然后开始使用:', + + 'agents.backToAgents': '返回智能体列表', + 'agents.description': '长期运行的自主智能体,可以监控数据源、按计划执行任务,并通过已连接的渠道给你发送消息。', + 'agents.emptyDescription': '创建第一个智能体,开始使用自主任务管理。', + 'agents.emptyTitle': '还没有智能体', + 'agents.firstAgent': '启动第一个智能体', + 'agents.interact': '交互', + 'agents.learning': '学习', + 'agents.loading': '正在加载智能体...', + 'agents.managerDisabled': '智能体管理器未启用。请在配置中设置 agent_manager.enabled = true。', + 'agents.newAgent': '新建智能体', + 'agents.overview': '概览', + 'agents.tasks': '任务', + 'agents.title': '智能体', + + 'dataSources.description': '连接个人数据,让助手能搜索你的全部资料;也可以设置消息渠道,用手机与助手聊天。', + 'dataSources.memory': '记忆', + 'dataSources.messaging': '消息渠道', + 'dataSources.sources': '数据源', + 'dataSources.title': '数据源、渠道与记忆', +}; + +const dictionaries: Record> = { + 'en-US': enUS, + 'zh-CN': zhCN, +}; + +export type TranslationKey = keyof typeof enUS; + +export function translate(locale: Locale, key: TranslationKey, vars: Vars = {}): string { + const template = dictionaries[locale]?.[key] ?? enUS[key] ?? key; + return template.replace(/\{(\w+)\}/g, (_, name: string) => + Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : `{${name}}`, + ); +} + +export function useI18n() { + const language = useAppStore((s) => s.settings.language); + return { + language, + t: (key: TranslationKey, vars?: Vars) => translate(language, key, vars), + }; +} diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index d1a0775eb..748206bea 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -15,6 +15,7 @@ import type { TokenUsage, } from '../types'; import type { ManagedAgent } from './api'; +import type { Locale } from './i18n'; export interface CachedConnector { connector_id: string; @@ -69,6 +70,7 @@ export type ThemeMode = 'light' | 'dark' | 'system'; interface Settings { theme: ThemeMode; + language: Locale; apiUrl: string; // Local server API key (OPENJARVIS_API_KEY). Sent as a Bearer token on // /v1 + /api requests so a key-protected `jarvis serve` doesn't 401 the @@ -85,6 +87,7 @@ interface Settings { function loadSettings(): Settings { const defaults: Settings = { theme: 'system', + language: 'en-US', apiUrl: '', apiKey: '', fontSize: 'default', diff --git a/frontend/src/pages/AgentsPage.tsx b/frontend/src/pages/AgentsPage.tsx index c25036c74..2344ca3b4 100644 --- a/frontend/src/pages/AgentsPage.tsx +++ b/frontend/src/pages/AgentsPage.tsx @@ -68,6 +68,7 @@ import type { ConnectRequest } from '../types/connectors'; import { listConnectors, connectSource } from '../lib/connectors-api'; import type { ToolCallInfo } from '../types'; import { ToolCallCard } from '../components/Chat/ToolCallCard'; +import { useI18n } from '../lib/i18n'; // --------------------------------------------------------------------------- // Status helpers @@ -3418,6 +3419,7 @@ function LogsTab({ agentId }: { agentId: string }) { // --------------------------------------------------------------------------- export function AgentsPage() { + const { t } = useI18n(); const managedAgents = useAppStore((s) => s.managedAgents); const setManagedAgents = useAppStore((s) => s.setManagedAgents); const selectedAgentId = useAppStore((s) => s.selectedAgentId); @@ -3548,7 +3550,7 @@ export function AgentsPage() { if (loading) { return (
- Loading agents... + {t('agents.loading')}
); } @@ -3562,14 +3564,14 @@ export function AgentsPage() { : null; const DETAIL_TABS = [ - { id: 'interact', label: 'Interact', icon: MessageSquare }, - { id: 'overview', label: 'Overview', icon: Activity }, - { id: 'channels', label: 'Data Sources', icon: Database }, - { id: 'messaging', label: 'Messaging Channels', icon: Wifi }, - { id: 'tasks', label: 'Tasks', icon: ListTodo }, - { id: 'memory', label: 'Memory', icon: Brain }, - { id: 'learning', label: 'Learning', icon: Settings }, - { id: 'logs', label: 'Logs', icon: FileText }, + { id: 'interact', label: t('agents.interact'), icon: MessageSquare }, + { id: 'overview', label: t('agents.overview'), icon: Activity }, + { id: 'channels', label: t('dataSources.sources'), icon: Database }, + { id: 'messaging', label: t('dataSources.messaging'), icon: Wifi }, + { id: 'tasks', label: t('agents.tasks'), icon: ListTodo }, + { id: 'memory', label: t('dataSources.memory'), icon: Brain }, + { id: 'learning', label: t('agents.learning'), icon: Settings }, + { id: 'logs', label: t('logs.title'), icon: FileText }, ] as const; return ( @@ -3581,7 +3583,7 @@ export function AgentsPage() { className="flex items-center gap-1 mb-4 text-sm cursor-pointer" style={{ color: 'var(--color-text-secondary)' }} > - Back to agents + {t('agents.backToAgents')} {/* Header */} @@ -3921,7 +3923,7 @@ export function AgentsPage() {

- Agents + {t('agents.title')}

- Long-running autonomous agents that can monitor sources, run tasks on a schedule, and message you through connected channels. + {t('agents.description')}

@@ -3950,7 +3952,7 @@ export function AgentsPage() { }} > - Agent manager is not enabled. Set agent_manager.enabled = true in your config. + {t('agents.managerDisabled')} agent_manager.enabled = true )} @@ -3985,9 +3987,9 @@ export function AgentsPage() {

- No agents yet + {t('agents.emptyTitle')}

-

Create your first agent to get started with autonomous task management.

+

{t('agents.emptyDescription')}

)} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index a3d12993b..1ff1412af 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -1,8 +1,10 @@ import { EnergyDashboard } from '../components/Dashboard/EnergyDashboard'; import { CostComparison } from '../components/Dashboard/CostComparison'; import { TraceDebugger } from '../components/Dashboard/TraceDebugger'; +import { useI18n } from '../lib/i18n'; export function DashboardPage() { + const { t } = useI18n(); const now = new Date(); const stamp = now.toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; @@ -12,14 +14,14 @@ export function DashboardPage() {

- System Overview + {t('dashboard.title')}

{stamp}

- Live telemetry for the on-device inference engine — power draw, token throughput, and cost savings versus cloud APIs. + {t('dashboard.description')}

diff --git a/frontend/src/pages/DataSourcesPage.tsx b/frontend/src/pages/DataSourcesPage.tsx index 795acf021..f6c34a0c6 100644 --- a/frontend/src/pages/DataSourcesPage.tsx +++ b/frontend/src/pages/DataSourcesPage.tsx @@ -26,6 +26,7 @@ import { SOURCE_CATALOG } from '../types/connectors'; import type { ConnectRequest } from '../types/connectors'; import { listConnectors, connectSource, disconnectSource, getSyncStatus, triggerSync, startServerOAuth } from '../lib/connectors-api'; import type { SyncStatus } from '../types/connectors'; +import { useI18n } from '../lib/i18n'; // --------------------------------------------------------------------------- // Inline connect form (reused from AgentsPage pattern) @@ -1962,6 +1963,7 @@ function MemorySection() { // --------------------------------------------------------------------------- export function DataSourcesPage() { + const { t } = useI18n(); const [agents, setAgents] = useState([]); const [activeTab, setActiveTab] = useState<'sources' | 'messaging' | 'memory'>('sources'); const [creatingAgent, setCreatingAgent] = useState(false); @@ -2001,9 +2003,9 @@ export function DataSourcesPage() { }, [activeTab, firstAgent, creatingAgent, ensureAgent]); const tabs = [ - { id: 'sources' as const, label: 'Data Sources', icon: Database }, - { id: 'messaging' as const, label: 'Messaging Channels', icon: MessageSquare }, - { id: 'memory' as const, label: 'Memory', icon: Brain }, + { id: 'sources' as const, label: t('dataSources.sources'), icon: Database }, + { id: 'messaging' as const, label: t('dataSources.messaging'), icon: MessageSquare }, + { id: 'memory' as const, label: t('dataSources.memory'), icon: Brain }, ]; return ( @@ -2011,10 +2013,10 @@ export function DataSourcesPage() {

- Data Sources, Channels & Memory + {t('dataSources.title')}

- Connect personal data so the assistant can search across everything, and set up messaging channels to chat from your phone. + {t('dataSources.description')}

diff --git a/frontend/src/pages/GetStartedPage.tsx b/frontend/src/pages/GetStartedPage.tsx index 2468847a7..25598e2d6 100644 --- a/frontend/src/pages/GetStartedPage.tsx +++ b/frontend/src/pages/GetStartedPage.tsx @@ -17,6 +17,7 @@ import { ArrowRight, } from 'lucide-react'; import { isTauri, checkHealth } from '../lib/api'; +import { useI18n } from '../lib/i18n'; const GITHUB_BASE = 'https://github.com/open-jarvis/OpenJarvis/releases/latest/download'; @@ -184,6 +185,7 @@ function Section({ // --------------------------------------------------------------------------- function HostedView() { const navigate = useNavigate(); + const { t } = useI18n(); const [healthy, setHealthy] = useState(null); useEffect(() => { @@ -205,15 +207,14 @@ function HostedView() { className="text-sm mb-6 leading-relaxed max-w-md mx-auto" style={{ color: 'var(--color-text-secondary)' }} > - Private AI that runs on your hardware. Chat, tools, agents, and - energy profiling — no cloud required. + {t('getStarted.heroDescription')}

{healthy === true && (
- Server is running + {t('getStarted.serverRunning')}
@@ -234,13 +235,13 @@ function HostedView() { className="mt-4 inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm" style={{ background: 'color-mix(in srgb, var(--color-error) 10%, transparent)', color: 'var(--color-error)' }} > - Server is not responding. The backend may be starting up. + {t('getStarted.serverNotResponding')}
)} {healthy === null && (
- Checking server... + {t('getStarted.checkingServer')}
)} @@ -252,6 +253,7 @@ function HostedView() { // --------------------------------------------------------------------------- function DesktopView() { const navigate = useNavigate(); + const { t } = useI18n(); return ( <> @@ -269,8 +271,7 @@ function DesktopView() { className="text-sm mb-4 leading-relaxed max-w-md mx-auto" style={{ color: 'var(--color-text-secondary)' }} > - Your local AI is ready. Everything runs on your device — no - data leaves your machine. + {t('getStarted.desktopReady')}

- All systems running + {t('getStarted.allSystemsRunning')}

Ollama inference engine, API server, and AI model are active. @@ -299,17 +300,17 @@ function DesktopView() { onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')} > - Start Chatting + {t('getStarted.startChatting')}

-
+
-
Cmd+K Model picker
-
Cmd+I System panel
-
Cmd+N New chat
+
Cmd+K {t('getStarted.modelPicker')}
+
Cmd+I {t('getStarted.systemPanel')}
+
Cmd+N {t('sidebar.newChat')}
@@ -321,6 +322,7 @@ function DesktopView() { // Self-hosted view: running on localhost (manual setup) // --------------------------------------------------------------------------- function SelfHostedView() { + const { t } = useI18n(); const detectedId = useMemo(() => detectPlatform(), []); const primary = PLATFORMS.find((p) => p.id === detectedId) || PLATFORMS[0]; const others = PLATFORMS.filter((p) => p.id !== primary.id); @@ -342,8 +344,7 @@ function SelfHostedView() { className="text-sm mb-4 leading-relaxed max-w-md mx-auto" style={{ color: 'var(--color-text-secondary)' }} > - Private AI that runs on your hardware. Chat, tools, agents, and - energy profiling — no cloud required. + {t('getStarted.heroDescription')}

- Desktop App + {t('getStarted.desktopApp')}

- One-click install. Bundles Ollama and the server — no setup required. + {t('getStarted.desktopAppDescription')}

(e.currentTarget.style.opacity = '1')} > - Download for {primary.label} + {t('getStarted.downloadFor', { platform: primary.label })} @@ -444,13 +443,13 @@ function SelfHostedView() {

- System Requirements + {t('getStarted.systemRequirements')}

Desktop App
- No prerequisites — everything is bundled + {t('getStarted.noPrerequisites')}
CLI / Self-Hosted
diff --git a/frontend/src/pages/LogsPage.tsx b/frontend/src/pages/LogsPage.tsx index 29b730830..3cfd43372 100644 --- a/frontend/src/pages/LogsPage.tsx +++ b/frontend/src/pages/LogsPage.tsx @@ -1,6 +1,7 @@ import { useRef, useEffect } from 'react'; import { Copy, Trash2 } from 'lucide-react'; import { useAppStore } from '../lib/store'; +import { useI18n } from '../lib/i18n'; const LEVEL_COLORS: Record = { info: 'var(--color-text)', @@ -14,6 +15,7 @@ function formatTime(ts: number): string { } export function LogsPage() { + const { t } = useI18n(); const logEntries = useAppStore((s) => s.logEntries); const clearLogs = useAppStore((s) => s.clearLogs); const bottomRef = useRef(null); @@ -35,30 +37,30 @@ export function LogsPage() {

- Logs + {t('logs.title')}

- {logEntries.length} entries + {t('logs.entries', { count: logEntries.length })}

- Recent activity — chat events, model switches, tool calls, and system messages from this session. + {t('logs.description')}

@@ -69,7 +71,7 @@ export function LogsPage() { > {logEntries.length === 0 ? (
- No log entries yet. Logs appear as you chat, switch models, and interact with the app. + {t('logs.empty')}
) : ( logEntries.map((entry, i) => ( diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 951937cdb..898c85224 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -21,8 +21,10 @@ import { import { useAppStore, type ThemeMode } from '../lib/store'; import { checkHealth, fetchSpeechHealth, getMemoryStats, getInferenceSource, setInferenceSource, type InferenceSource } from '../lib/api'; import { isAutoUpdateDisabled, setAutoUpdateDisabled } from '../components/Desktop/UpdateChecker'; +import { useI18n, type Locale, type TranslationKey } from '../lib/i18n'; function OllamaModelList() { + const { t } = useI18n(); const [models, setModels] = useState>([]); useEffect(() => { fetch('http://localhost:11434/api/tags') @@ -30,7 +32,7 @@ function OllamaModelList() { .then(data => setModels((data.models || []).map((m: any) => ({ name: m.name, size: m.size })))) .catch(() => setModels([])); }, []); - if (models.length === 0) return No models loaded; + if (models.length === 0) return {t('settings.noModelsLoaded')}; return (
{models.map(m => ( @@ -45,6 +47,7 @@ function OllamaModelList() { } function ApiKeyInput({ storageKey, placeholder }: { storageKey: string; placeholder: string }) { + const { t } = useI18n(); const [value, setValue] = useState(() => { try { return localStorage.getItem(storageKey) || ''; } catch { return ''; } }); @@ -60,7 +63,7 @@ function ApiKeyInput({ storageKey, placeholder }: { storageKey: string; placehol save(e.target.value)} placeholder={placeholder} className="w-48 px-2 py-1 rounded text-xs" style={{ background: 'var(--color-bg)', border: '1px solid var(--color-border)', color: 'var(--color-text)' }} /> - {saved && Saved} + {saved && {t('common.saved')}}
); } @@ -109,13 +112,19 @@ function SettingRow({ label, description, children }: { label: string; descripti ); } -const themeOptions: { value: ThemeMode; label: string; icon: typeof Sun }[] = [ - { value: 'light', label: 'Light', icon: Sun }, - { value: 'dark', label: 'Dark', icon: Moon }, - { value: 'system', label: 'System', icon: Monitor }, +const themeOptions: { value: ThemeMode; labelKey: TranslationKey; icon: typeof Sun }[] = [ + { value: 'light', labelKey: 'settings.themeLight', icon: Sun }, + { value: 'dark', labelKey: 'settings.themeDark', icon: Moon }, + { value: 'system', labelKey: 'settings.themeSystem', icon: Monitor }, +]; + +const languageOptions: { value: Locale; label: string }[] = [ + { value: 'en-US', label: 'English' }, + { value: 'zh-CN', label: '简体中文' }, ]; export function SettingsPage() { + const { t } = useI18n(); const settings = useAppStore((s) => s.settings); const updateSettings = useAppStore((s) => s.updateSettings); const conversations = useAppStore((s) => s.conversations); @@ -259,26 +268,26 @@ export function SettingsPage() {

- Settings + {t('settings.pageTitle')}

{saved && ( - Saved + {t('common.saved')} )}

- App preferences — appearance, model defaults, keyboard shortcuts, and data management. + {t('settings.description')}

{/* Appearance */} -
- +
+
{themeOptions.map((opt) => { const isActive = settings.theme === opt.value; @@ -294,13 +303,29 @@ export function SettingsPage() { }} > - {opt.label} + {t(opt.labelKey)} ); })}
- + + + +
{/* Connection */} -
- +
+
- {healthy === true ? 'Connected' : healthy === false ? 'Disconnected' : 'Checking...'} + {healthy === true ? t('common.connected') : healthy === false ? t('common.disconnected') : t('common.checking')}
- + - + {/* Inference source */} -
- +
+ {srcKind === 'custom' && ( @@ -415,14 +440,14 @@ export function SettingsPage() {
{/* Models */} -
- +
+
- Run ollama pull <model-name> in your terminal to add more models + {t('settings.runOllamaPull', { command: 'ollama pull ' })}
- +
@@ -433,7 +458,7 @@ export function SettingsPage() {
{/* API Keys */} -
+
@@ -449,23 +474,23 @@ export function SettingsPage() {
{/* Tools */} -
- +
+
{/* Memory */} -
- +
+
- {memoryStats ? `${memoryStats.entries} entries` : 'Unavailable'} + {memoryStats ? `${memoryStats.entries} entries` : t('common.unavailable')}
- + - + - + - + - + {/* Model defaults */} -
- +
+ - + {/* Speech */} -
- +
+ - +
- {speechBackendAvailable === null ? 'Checking...' - : speechBackendAvailable ? 'Available' - : 'Not configured'} + {speechBackendAvailable === null ? t('common.checking') + : speechBackendAvailable ? t('common.available') + : t('common.notConfigured')}
{!speechBackendAvailable && speechBackendAvailable !== null && (
- Set up a speech backend to use voice input. - See the documentation for details. + {t('settings.speechSetup')}{' '} + documentation
)}
{/* Data */} -
- +
+
- +
{/* Updates */} -
- +
+