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')}
navigate('/data-sources')}
className="px-3 py-1 rounded text-xs font-medium cursor-pointer"
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)', border: 'none' }}
>
- Connect
+ {t('common.connect')}
setBannerDismissed(true)}
@@ -106,10 +108,10 @@ export function ChatArea() {
- {getGreeting()}
+ {t(getGreetingKey())}
- Ask anything. Your AI runs locally — private, fast, and always available.
+ {t('chat.askAnything')}
{/* Quick action hints */}
@@ -126,7 +128,7 @@ export function ChatArea() {
onMouseLeave={(e) => (e.currentTarget.style.borderColor = 'var(--color-border)')}
>
- Connect Data Sources
+ {t('chat.connectDataSources')}
{ navigate('/data-sources'); setTimeout(() => window.dispatchEvent(new CustomEvent('switch-tab', { detail: 'messaging' })), 100); }}
@@ -140,7 +142,7 @@ export function ChatArea() {
onMouseLeave={(e) => (e.currentTarget.style.borderColor = 'var(--color-border)')}
>
- Set Up Messaging Channels
+ {t('chat.setUpMessaging')}
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() {
- Enter to send ·{' '}
- Shift+Enter for new line
+ {t('chat.inputHelp')}
diff --git a/frontend/src/components/Chat/MessageBubble.tsx b/frontend/src/components/Chat/MessageBubble.tsx
index c221050b2..2240008b5 100644
--- a/frontend/src/components/Chat/MessageBubble.tsx
+++ b/frontend/src/components/Chat/MessageBubble.tsx
@@ -12,6 +12,7 @@ import { ResearchTimeline } from './ResearchTimeline';
import { rehypeCitations } from '../../lib/rehype-citations';
import { XRayFooter } from './XRayFooter';
import type { ChatMessage } from '../../types';
+import { useI18n } from '../../lib/i18n';
function stripThinkTags(text: string): string {
let cleaned = text.replace(/[\s\S]*?<\/think>\s*/gi, '');
@@ -39,6 +40,7 @@ function getTextContent(node: any): string {
function CodeBlockPre({ children, ...props }: any) {
const [copied, setCopied] = useState(false);
+ const { t } = useI18n();
const codeElement = Array.isArray(children) ? children[0] : children;
const className = codeElement?.props?.className || '';
const match = /language-([\w-]+)/.exec(className);
@@ -69,7 +71,7 @@ function CodeBlockPre({ children, ...props }: any) {
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--color-text-tertiary)')}
>
{copied ? : }
- {copied ? 'Copied' : 'Copy'}
+ {copied ? t('common.copied') : t('common.copy')}
@@ -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')}
- {optInEnabled ? 'Sharing Savings' : 'Share Your Savings'}
+ {optInEnabled ? t('system.sharingSavings') : t('system.shareSavings')}
- {optInEnabled ? 'ON' : 'OFF'}
+ {optInEnabled ? t('common.on') : t('common.off')}
@@ -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')}
agentManagerAvailable && setShowWizard(true)}
@@ -3932,11 +3934,11 @@ export function AgentsPage() {
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
}}
>
- New Agent
+ {t('agents.newAgent')}
- 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')}
agentManagerAvailable && setShowWizard(true)}
disabled={agentManagerAvailable === false}
@@ -3997,7 +3999,7 @@ export function AgentsPage() {
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
}}
>
- Launch your first agent
+ {t('agents.firstAgent')}
)}
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() {
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')}
navigate('/')}
@@ -223,7 +224,7 @@ function HostedView() {
onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')}
>
- Start Chatting
+ {t('getStarted.startChatting')}
@@ -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 })}
- Copy All
+ {t('logs.copyAll')}
- Clear
+ {t('common.clear')}
- 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)}
);
})}
-
+
+ { updateSettings({ language: e.target.value as Locale }); showSaved(); }}
+ className="text-sm px-3 py-1.5 rounded-lg outline-none cursor-pointer"
+ style={{
+ background: 'var(--color-bg-secondary)',
+ color: 'var(--color-text)',
+ border: '1px solid var(--color-border)',
+ }}
+ >
+ {languageOptions.map((opt) => (
+ {opt.label}
+ ))}
+
+
+
{ updateSettings({ fontSize: e.target.value as any }); showSaved(); }}
@@ -311,27 +336,27 @@ export function SettingsPage() {
border: '1px solid var(--color-border)',
}}
>
- Small
- Default
- Large
+ {t('settings.fontSmall')}
+ {t('settings.fontDefault')}
+ {t('settings.fontLarge')}
{/* Connection */}
-