diff --git a/desktop/src/components/chat/AskUserQuestion.tsx b/desktop/src/components/chat/AskUserQuestion.tsx index c1ce4238..48c211b2 100644 --- a/desktop/src/components/chat/AskUserQuestion.tsx +++ b/desktop/src/components/chat/AskUserQuestion.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { useChatStore } from '../../stores/chatStore' +import { useTranslation } from '../../i18n' import { Button } from '../shared/Button' type QuestionOption = { @@ -46,6 +47,7 @@ function parseInput(input: unknown): Question[] { export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) { const { sendMessage } = useChatStore() + const t = useTranslation() const questions = parseInput(input) const [activeTab, setActiveTab] = useState(0) const [selections, setSelections] = useState>({}) @@ -109,11 +111,11 @@ export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) {
- Claude needs your input + {t('question.needsInput')} {submitted && ( - Answered + {t('question.answered')} )}
@@ -209,7 +211,7 @@ export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) { {!submitted && (
{ if (e.key === 'Enter' && allAnswered) handleSubmit() }} - placeholder="Type your answer..." + placeholder={t('question.typePlaceholder')} className="w-full px-3 py-2 text-sm bg-[var(--color-surface)] border border-[var(--color-outline-variant)]/40 rounded-[var(--radius-md)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:border-[var(--color-secondary)] focus:ring-1 focus:ring-[var(--color-secondary)]/30" />
@@ -232,7 +234,7 @@ export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) {
check_circle - Answered: {freeText.trim() || Object.values(selections).join(', ')} + {t('question.answeredPrefix')}{freeText.trim() || Object.values(selections).join(', ')}
)} @@ -250,7 +252,7 @@ export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) { send } > - Submit + {t('question.submit')} )} diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 1f3cbb3d..903cf802 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect, useCallback, useMemo } from 'react' +import { useTranslation } from '../../i18n' import { useChatStore } from '../../stores/chatStore' import { useSessionStore } from '../../stores/sessionStore' import { sessionsApi } from '../../api/sessions' @@ -26,6 +27,7 @@ type Attachment = { } export function ChatInput() { + const t = useTranslation() const [input, setInput] = useState('') const [attachments, setAttachments] = useState([]) const [plusMenuOpen, setPlusMenuOpen] = useState(false) @@ -418,11 +420,11 @@ export function ChatInput() {
Up/Down - navigate + {t('chat.navigate')} Enter - select + {t('chat.select')} Esc - dismiss + {t('chat.dismiss')}
)} @@ -441,8 +443,8 @@ export function ChatInput() { onPaste={handlePaste} placeholder={ isWorkspaceMissing - ? 'This session points to a missing workspace. Create a new session or pick another project.' - : 'Ask Claude to edit, debug or explain...' + ? t('chat.placeholderMissing') + : t('chat.placeholder') } disabled={isWorkspaceMissing} rows={1} @@ -470,14 +472,14 @@ export function ChatInput() { className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-[#F8F7F4]" > attach_file - Add files or photos + {t('chat.addFiles')} )} @@ -491,7 +493,7 @@ export function ChatInput() { diff --git a/desktop/src/components/chat/FileSearchMenu.tsx b/desktop/src/components/chat/FileSearchMenu.tsx index 958bc4c1..1a6d92b1 100644 --- a/desktop/src/components/chat/FileSearchMenu.tsx +++ b/desktop/src/components/chat/FileSearchMenu.tsx @@ -1,5 +1,6 @@ import { forwardRef, useState, useEffect, useRef, useCallback, useImperativeHandle } from 'react' import { filesystemApi } from '../../api/filesystem' +import { useTranslation } from '../../i18n' type DirEntry = { name: string @@ -18,6 +19,7 @@ type Props = { } export const FileSearchMenu = forwardRef(({ cwd, filter = '', onSelect }, ref) => { + const t = useTranslation() const [entries, setEntries] = useState([]) const [currentPath, setCurrentPath] = useState(cwd) const [loading, setLoading] = useState(false) @@ -133,10 +135,10 @@ export const FileSearchMenu = forwardRef(({ cwd, fi {/* File list */}
{loading && entries.length === 0 ? ( -
Searching...
+
{t('fileSearch.searching')}
) : entries.length === 0 ? (
- {filter ? 'No files match' : 'No files in this directory'} + {filter ? t('fileSearch.noMatch') : t('fileSearch.noFiles')}
) : ( <> @@ -183,11 +185,11 @@ export const FileSearchMenu = forwardRef(({ cwd, fi {/* Footer hint */}
↑↓ - navigate + {t('fileSearch.navigate')} Enter - attach + {t('fileSearch.attach')} Esc - close + {t('fileSearch.close')}
) diff --git a/desktop/src/components/chat/InlineTaskSummary.tsx b/desktop/src/components/chat/InlineTaskSummary.tsx index b7a743ba..9a77d2cb 100644 --- a/desktop/src/components/chat/InlineTaskSummary.tsx +++ b/desktop/src/components/chat/InlineTaskSummary.tsx @@ -1,4 +1,5 @@ import type { TaskSummaryItem } from '../../types/chat' +import { useTranslation } from '../../i18n' const statusIcon: Record = { pending: 'radio_button_unchecked', @@ -13,7 +14,8 @@ const statusColor: Record = { } export function InlineTaskSummary({ tasks }: { tasks: TaskSummaryItem[] }) { - const completed = tasks.filter((t) => t.status === 'completed').length + const t = useTranslation() + const completed = tasks.filter((tk) => tk.status === 'completed').length const total = tasks.length return ( @@ -25,7 +27,7 @@ export function InlineTaskSummary({ tasks }: { tasks: TaskSummaryItem[] }) { - Tasks completed + {t('tasks.completed')} {completed}/{total} diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 53ed8444..df3fa901 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1,5 +1,7 @@ import { useRef, useEffect } from 'react' import { useChatStore } from '../../stores/chatStore' +import { useTranslation } from '../../i18n' +import type { TranslationKey } from '../../i18n/locales/en' import { UserMessage } from './UserMessage' import { AssistantMessage } from './AssistantMessage' import { ThinkingBlock } from './ThinkingBlock' @@ -141,6 +143,8 @@ function MessageBlock({ activeThinkingId: string | null toolResult?: { content: unknown; isError: boolean } | null }) { + const t = useTranslation() + switch (message.type) { case 'user_text': return @@ -181,12 +185,16 @@ function MessageBlock({ description={message.description} /> ) - case 'error': + case 'error': { + const errorKey = message.code ? `error.${message.code}` as TranslationKey : null + const errorText = errorKey ? t(errorKey) : null + const displayMessage = (errorText && errorText !== errorKey) ? errorText : message.message return (
- Error: {message.message} + Error: {displayMessage}
) + } case 'task_summary': return case 'system': diff --git a/desktop/src/components/chat/PermissionDialog.tsx b/desktop/src/components/chat/PermissionDialog.tsx index 2097a6ef..34f0abff 100644 --- a/desktop/src/components/chat/PermissionDialog.tsx +++ b/desktop/src/components/chat/PermissionDialog.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { useChatStore } from '../../stores/chatStore' +import { useTranslation } from '../../i18n' import { Button } from '../shared/Button' import { DiffViewer } from './DiffViewer' @@ -31,7 +32,7 @@ const TOOL_META: Record /** * Extract human-readable detail lines from tool input. */ -function extractToolDetails(toolName: string, input: unknown): { primary: string; secondary?: string } { +function extractToolDetails(toolName: string, input: unknown, t: (key: any, params?: any) => string): { primary: string; secondary?: string } { const obj = (input && typeof input === 'object') ? input as Record : {} switch (toolName) { @@ -42,7 +43,7 @@ function extractToolDetails(toolName: string, input: unknown): { primary: string } case 'Edit': { const filePath = typeof obj.file_path === 'string' ? obj.file_path : '' - return { primary: filePath, secondary: obj.old_string ? 'Replacing content in file' : undefined } + return { primary: filePath, secondary: obj.old_string ? t('permission.replacingContent') : undefined } } case 'Write': { const filePath = typeof obj.file_path === 'string' ? obj.file_path : '' @@ -67,7 +68,7 @@ function extractToolDetails(toolName: string, input: unknown): { primary: string } } -function getPermissionTitle(toolName: string, input: unknown) { +function getPermissionTitle(toolName: string, input: unknown, t: (key: any, params?: any) => string) { const obj = (input && typeof input === 'object') ? input as Record : {} const filePath = typeof obj.file_path === 'string' ? obj.file_path : '' const fileName = filePath ? filePath.split('/').pop() || filePath : '' @@ -75,11 +76,11 @@ function getPermissionTitle(toolName: string, input: unknown) { switch (toolName) { case 'Edit': case 'Write': - return fileName ? `Allow Claude to ${toolName} ${fileName}?` : `Allow Claude to ${toolName.toLowerCase()} this file?` + return fileName ? t('permission.allowEditFile', { toolName, fileName }) : t('permission.allowEditFileGeneric', { toolName: toolName.toLowerCase() }) case 'Bash': - return 'Allow Claude to run this command?' + return t('permission.allowBash') default: - return `Allow Claude to use ${toolName}?` + return t('permission.allowTool', { toolName }) } } @@ -110,14 +111,15 @@ function renderPermissionPreview(toolName: string, input: unknown) { export function PermissionDialog({ requestId, toolName, input, description }: Props) { const { respondToPermission, pendingPermission } = useChatStore() + const t = useTranslation() const isPending = pendingPermission?.requestId === requestId const [showRaw, setShowRaw] = useState(false) const meta = TOOL_META[toolName] || { icon: 'shield', label: toolName, color: '#87736D' } - const details = extractToolDetails(toolName, input) + const details = extractToolDetails(toolName, input, t) const rawInput = typeof input === 'string' ? input : JSON.stringify(input, null, 2) const preview = renderPermissionPreview(toolName, input) - const title = getPermissionTitle(toolName, input) + const title = getPermissionTitle(toolName, input, t) const allowRawToggle = !preview return ( @@ -151,12 +153,12 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr {isPending && ( - Awaiting approval + {t('permission.awaitingApproval')} )} {!isPending && ( - Responded + {t('permission.responded')} )} @@ -204,7 +206,7 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr {showRaw ? 'expand_less' : 'expand_more'} - {showRaw ? 'Hide details' : 'Show full input'} + {showRaw ? t('permission.hideDetails') : t('permission.showFullInput')} )} @@ -226,7 +228,7 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr check } > - Allow + {t('permission.allow')}
)} diff --git a/desktop/src/components/chat/SessionTaskBar.tsx b/desktop/src/components/chat/SessionTaskBar.tsx index 2702c9e1..62778b08 100644 --- a/desktop/src/components/chat/SessionTaskBar.tsx +++ b/desktop/src/components/chat/SessionTaskBar.tsx @@ -1,4 +1,5 @@ import { useCLITaskStore } from '../../stores/cliTaskStore' +import { useTranslation } from '../../i18n' import type { CLITask } from '../../types/cliTask' const statusConfig = { @@ -21,14 +22,15 @@ const statusConfig = { export function SessionTaskBar() { const { tasks, expanded, toggleExpanded, completedAndDismissed } = useCLITaskStore() + const t = useTranslation() if (tasks.length === 0) return null // Don't show sticky bar if tasks were completed and the user already continued chatting - const allCompleted = tasks.every((t) => t.status === 'completed') + const allCompleted = tasks.every((tk) => tk.status === 'completed') if (allCompleted && completedAndDismissed) return null - const completedCount = tasks.filter((t) => t.status === 'completed').length + const completedCount = tasks.filter((tk) => tk.status === 'completed').length const totalCount = tasks.length const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0 @@ -49,7 +51,7 @@ export function SessionTaskBar() { - Tasks + {t('tasks.title')} {/* Progress bar */} diff --git a/desktop/src/components/chat/StreamingIndicator.tsx b/desktop/src/components/chat/StreamingIndicator.tsx index a7bb219f..5c3cc933 100644 --- a/desktop/src/components/chat/StreamingIndicator.tsx +++ b/desktop/src/components/chat/StreamingIndicator.tsx @@ -1,4 +1,6 @@ import { useChatStore } from '../../stores/chatStore' +import { useTranslation } from '../../i18n' +import type { TranslationKey } from '../../i18n/locales/en' function formatElapsed(seconds: number): string { if (seconds < 60) return `${seconds}s` @@ -9,9 +11,17 @@ function formatElapsed(seconds: number): string { export function StreamingIndicator() { const { chatState, statusVerb, elapsedSeconds, tokenUsage } = useChatStore() + const t = useTranslation() - const verb = statusVerb - || (chatState === 'thinking' ? 'Thinking' : chatState === 'tool_executing' ? 'Running' : 'Working') + // Translate known server-sent verbs (e.g. "Thinking", "Task started") + let verb: string + if (statusVerb) { + const serverKey = `serverVerb.${statusVerb}` as TranslationKey + const translated = t(serverKey) + verb = translated !== serverKey ? translated : statusVerb + } else { + verb = chatState === 'thinking' ? t('streaming.thinking') : chatState === 'tool_executing' ? t('streaming.running') : t('streaming.working') + } return (
diff --git a/desktop/src/components/chat/ThinkingBlock.tsx b/desktop/src/components/chat/ThinkingBlock.tsx index 94481f02..dc97706e 100644 --- a/desktop/src/components/chat/ThinkingBlock.tsx +++ b/desktop/src/components/chat/ThinkingBlock.tsx @@ -1,6 +1,8 @@ import { useState, useEffect, useRef } from 'react' +import { useTranslation } from '../../i18n' export function ThinkingBlock({ content, isActive = false }: { content: string; isActive?: boolean }) { + const t = useTranslation() const [expanded, setExpanded] = useState(false) const contentRef = useRef(null) @@ -26,7 +28,7 @@ export function ThinkingBlock({ content, isActive = false }: { content: string; {expanded ? '\u25BE' : '\u25B8'} - Thinking + {t('thinking.label')} {isActive && } {!expanded && preview && ( diff --git a/desktop/src/components/chat/ToolCallBlock.tsx b/desktop/src/components/chat/ToolCallBlock.tsx index 0db2fe50..3b89c9b7 100644 --- a/desktop/src/components/chat/ToolCallBlock.tsx +++ b/desktop/src/components/chat/ToolCallBlock.tsx @@ -3,6 +3,7 @@ import { CodeViewer } from './CodeViewer' import { DiffViewer } from './DiffViewer' import { TerminalChrome } from './TerminalChrome' import { CopyButton } from '../shared/CopyButton' +import { useTranslation } from '../../i18n' type Props = { toolName: string @@ -27,14 +28,15 @@ const TOOL_ICONS: Record = { export function ToolCallBlock({ toolName, input, result, compact = false }: Props) { const [expanded, setExpanded] = useState(false) + const t = useTranslation() const obj = input && typeof input === 'object' ? (input as Record) : {} const icon = TOOL_ICONS[toolName] || 'build' const filePath = typeof obj.file_path === 'string' ? obj.file_path : '' - const summary = getToolSummary(toolName, obj) - const outputSummary = getToolResultSummary(toolName, result?.content) + const summary = getToolSummary(toolName, obj, t) + const outputSummary = getToolResultSummary(toolName, result?.content, t) - const preview = useMemo(() => renderPreview(toolName, obj, result), [obj, result, toolName]) - const details = useMemo(() => renderDetails(toolName, obj), [obj, toolName]) + const preview = useMemo(() => renderPreview(toolName, obj, result, t), [obj, result, toolName, t]) + const details = useMemo(() => renderDetails(toolName, obj, t), [obj, toolName, t]) const expandable = toolName === 'Edit' || toolName === 'Write' return ( @@ -94,6 +96,7 @@ function renderPreview( toolName: string, obj: Record, result?: { content: unknown; isError: boolean } | null, + t?: (key: any, params?: any) => string, ) { const filePath = typeof obj.file_path === 'string' ? obj.file_path : 'file' @@ -129,7 +132,7 @@ function renderPreview( : 'border-[var(--color-border)] bg-[var(--color-surface)]' }`}>
- {result.isError ? 'Error Output' : 'Tool Output'} + {result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'} ) { +function renderDetails(toolName: string, obj: Record, t?: (key: any, params?: any) => string) { if (toolName === 'Edit' || toolName === 'Write') { return null } @@ -153,7 +156,7 @@ function renderDetails(toolName: string, obj: Record) { return (
- Tool Input + {t?.('tool.toolInput') ?? 'Tool Input'} ) { ) } -function getToolResultSummary(toolName: string, content: unknown): string { +function getToolResultSummary(toolName: string, content: unknown, t?: (key: any, params?: any) => string): string { if (toolName === 'Bash') return '' const text = extractTextContent(content) @@ -172,7 +175,7 @@ function getToolResultSummary(toolName: string, content: unknown): string { const lineCount = text.split('\n').length if (lineCount > 1) { - return `${lineCount} lines output` + return t?.('tool.linesOutput', { count: lineCount }) ?? `${lineCount} lines output` } const compact = text.replace(/\s+/g, ' ').trim() @@ -181,18 +184,20 @@ function getToolResultSummary(toolName: string, content: unknown): string { return `${compact.slice(0, 36)}…` } -function getToolSummary(toolName: string, obj: Record): string { +function getToolSummary(toolName: string, obj: Record, t?: (key: any, params?: any) => string): string { switch (toolName) { case 'Bash': return typeof obj.command === 'string' ? obj.command : '' case 'Read': - return typeof obj.limit === 'number' ? `Read file contents` : 'Read file contents' + return t?.('tool.readFileContents') ?? 'Read file contents' case 'Write': - return typeof obj.content === 'string' ? `${obj.content.split('\n').length} lines created` : 'Create file' + return typeof obj.content === 'string' + ? (t?.('tool.linesCreated', { count: obj.content.split('\n').length }) ?? `${obj.content.split('\n').length} lines created`) + : (t?.('tool.createFile') ?? 'Create file') case 'Edit': return typeof obj.old_string === 'string' && typeof obj.new_string === 'string' - ? changedLineSummary(obj.old_string, obj.new_string) - : 'Update file contents' + ? changedLineSummary(obj.old_string, obj.new_string, t) + : (t?.('tool.updateFileContents') ?? 'Update file contents') case 'Glob': return typeof obj.pattern === 'string' ? obj.pattern : '' case 'Grep': @@ -218,7 +223,7 @@ function extractTextContent(content: unknown): string | null { return null } -function changedLineSummary(oldString: string, newString: string): string { +function changedLineSummary(oldString: string, newString: string, t?: (key: any, params?: any) => string): string { const oldLines = oldString.split('\n') const newLines = newString.split('\n') let changed = 0 @@ -230,5 +235,5 @@ function changedLineSummary(oldString: string, newString: string): string { } } - return `${changed} lines changed` + return t?.('tool.linesChanged', { count: changed }) ?? `${changed} lines changed` } diff --git a/desktop/src/components/chat/ToolCallGroup.tsx b/desktop/src/components/chat/ToolCallGroup.tsx index f942b48d..0cf8c17b 100644 --- a/desktop/src/components/chat/ToolCallGroup.tsx +++ b/desktop/src/components/chat/ToolCallGroup.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { ToolCallBlock } from './ToolCallBlock' +import { useTranslation } from '../../i18n' import type { UIMessage } from '../../types/chat' type ToolCall = Extract @@ -12,19 +13,19 @@ type Props = { isStreaming?: boolean } -const TOOL_VERBS: Record string> = { - Read: (n) => `Read ${n} file${n > 1 ? 's' : ''}`, - Write: (n) => `created ${n > 1 ? `${n} files` : 'a file'}`, - Edit: (n) => `edited ${n > 1 ? `${n} files` : 'a file'}`, - Bash: (n) => `ran ${n > 1 ? `${n} commands` : 'a command'}`, - Glob: (_n) => `found files`, - Grep: (n) => `searched ${n > 1 ? `${n} patterns` : 'code'}`, - Agent: (n) => `dispatched ${n > 1 ? `${n} agents` : 'an agent'}`, - WebSearch: (_n) => `searched the web`, - WebFetch: (n) => `fetched ${n > 1 ? `${n} pages` : 'a page'}`, +const TOOL_VERBS: Record string) => string> = { + Read: (n, t) => n === 1 ? t('toolGroup.readOne') : t('toolGroup.readMany', { count: n }), + Write: (n, t) => n === 1 ? t('toolGroup.createdOne') : t('toolGroup.createdMany', { count: n }), + Edit: (n, t) => n === 1 ? t('toolGroup.editedOne') : t('toolGroup.editedMany', { count: n }), + Bash: (n, t) => n === 1 ? t('toolGroup.ranOne') : t('toolGroup.ranMany', { count: n }), + Glob: (_n, t) => t('toolGroup.foundFiles'), + Grep: (n, t) => n === 1 ? t('toolGroup.searchedOne') : t('toolGroup.searchedMany', { count: n }), + Agent: (n, t) => n === 1 ? t('toolGroup.agentOne') : t('toolGroup.agentMany', { count: n }), + WebSearch: (_n, t) => t('toolGroup.searchedWeb'), + WebFetch: (n, t) => n === 1 ? t('toolGroup.fetchedOne') : t('toolGroup.fetchedMany', { count: n }), } -function generateSummary(toolCalls: ToolCall[]): string { +function generateSummary(toolCalls: ToolCall[], t: (key: any, params?: any) => string): string { const counts = new Map() for (const tc of toolCalls) { counts.set(tc.toolName, (counts.get(tc.toolName) ?? 0) + 1) @@ -33,7 +34,7 @@ function generateSummary(toolCalls: ToolCall[]): string { const parts: string[] = [] for (const [name, count] of counts) { const verbFn = TOOL_VERBS[name] - parts.push(verbFn ? verbFn(count) : `${name} (${count})`) + parts.push(verbFn ? verbFn(count, t) : `${name} (${count})`) } return parts.join(', ') @@ -66,7 +67,8 @@ export function ToolCallGroup({ toolCalls, resultMap, isStreaming }: Props) { /** Separated so the useState hook is never called conditionally. */ function ToolCallGroupMulti({ toolCalls, resultMap, isStreaming }: Props) { const [expanded, setExpanded] = useState(false) - const summary = generateSummary(toolCalls) + const t = useTranslation() + const summary = generateSummary(toolCalls, t) const errorPresent = groupHasErrors(toolCalls, resultMap) const allComplete = toolCalls.every((tc) => resultMap.has(tc.toolUseId)) diff --git a/desktop/src/components/chat/ToolResultBlock.tsx b/desktop/src/components/chat/ToolResultBlock.tsx index 065d2323..0595831b 100644 --- a/desktop/src/components/chat/ToolResultBlock.tsx +++ b/desktop/src/components/chat/ToolResultBlock.tsx @@ -1,5 +1,6 @@ import { CodeViewer } from './CodeViewer' import { useState } from 'react' +import { useTranslation } from '../../i18n' type Props = { content: unknown @@ -15,6 +16,7 @@ type Props = { */ export function ToolResultBlock({ content, isError, toolName, standalone = true }: Props) { const [expanded, setExpanded] = useState(false) + const t = useTranslation() // Don't render standalone if this result is already rendered inline if (!standalone) return null @@ -43,14 +45,14 @@ export function ToolResultBlock({ content, isError, toolName, standalone = true {isError ? 'error' : 'check_circle'} - {toolName ? `${toolName} result` : 'Tool result'} + {toolName ? t('tool.result', { toolName }) : t('tool.resultGeneric')} - {isError ? 'ERROR' : 'SUCCESS'} + {isError ? t('tool.error') : t('tool.success')} @@ -79,7 +81,7 @@ export function ToolResultBlock({ content, isError, toolName, standalone = true onClick={() => setExpanded((value) => !value)} className="w-full py-1 text-[10px] font-medium text-[var(--color-text-accent)] hover:underline bg-[var(--color-surface-container-low)] border-t border-[var(--color-outline-variant)]/10" > - {expanded ? 'Show less' : `Show ${text.length - 200} more characters`} + {expanded ? t('tool.showLess') : t('tool.showMore', { count: text.length - 200 })} )}
diff --git a/desktop/src/components/controls/ModelSelector.tsx b/desktop/src/components/controls/ModelSelector.tsx index 755782fd..fad9c9e9 100644 --- a/desktop/src/components/controls/ModelSelector.tsx +++ b/desktop/src/components/controls/ModelSelector.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useEffect } from 'react' import { useSettingsStore } from '../../stores/settingsStore' +import { useTranslation } from '../../i18n' import type { EffortLevel } from '../../types/settings' const MODEL_ICONS = { @@ -8,13 +9,6 @@ const MODEL_ICONS = { haiku: 'bolt', } as const -const EFFORT_OPTIONS: { value: EffortLevel; label: string }[] = [ - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'max', label: 'Max' }, -] - type Props = { /** Controlled mode: model ID override */ value?: string @@ -23,10 +17,18 @@ type Props = { } export function ModelSelector({ value, onChange }: Props = {}) { + const t = useTranslation() const { currentModel: storeModel, availableModels, effortLevel, setModel, setEffort } = useSettingsStore() const [open, setOpen] = useState(false) const ref = useRef(null) + const EFFORT_OPTIONS: { value: EffortLevel; label: string }[] = [ + { value: 'low', label: t('settings.general.effort.low') }, + { value: 'medium', label: t('settings.general.effort.medium') }, + { value: 'high', label: t('settings.general.effort.high') }, + { value: 'max', label: t('settings.general.effort.max') }, + ] + const isControlled = value !== undefined const selectedModel = isControlled ? availableModels.find((m) => m.id === value) || null : storeModel @@ -61,7 +63,7 @@ export function ModelSelector({ value, onChange }: Props = {}) { className="flex items-center gap-1.5 px-2.5 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs font-medium text-[var(--color-text-secondary)] transition-colors" > auto_awesome - {selectedModel?.name ?? 'Select model'} + {selectedModel?.name ?? t('model.selectModel')} expand_more @@ -70,7 +72,7 @@ export function ModelSelector({ value, onChange }: Props = {}) { {/* Models */}
- Model Configuration + {t('model.configuration')}
{availableModels.map((model) => { @@ -124,7 +126,7 @@ export function ModelSelector({ value, onChange }: Props = {}) { {/* Effort — hidden in controlled mode (not relevant for task creation) */} {!isControlled &&
- Effort + {t('model.effort')}
{EFFORT_OPTIONS.map((opt) => { diff --git a/desktop/src/components/controls/PermissionModeSelector.tsx b/desktop/src/components/controls/PermissionModeSelector.tsx index 9f128c5c..dfb4712a 100644 --- a/desktop/src/components/controls/PermissionModeSelector.tsx +++ b/desktop/src/components/controls/PermissionModeSelector.tsx @@ -3,43 +3,9 @@ import { createPortal } from 'react-dom' import { useSettingsStore } from '../../stores/settingsStore' import { useChatStore } from '../../stores/chatStore' import { useSessionStore } from '../../stores/sessionStore' +import { useTranslation } from '../../i18n' import type { PermissionMode } from '../../types/settings' -const PERMISSION_ITEMS: Array<{ - value: PermissionMode - label: string - description: string - icon: string - color?: string -}> = [ - { - value: 'default', - label: 'Ask permissions', - description: 'Confirm file edits and higher-risk commands when CLI asks', - icon: 'verified_user', - }, - { - value: 'acceptEdits', - label: 'Auto accept edits', - description: 'Claude writes to disk without asking', - icon: 'bolt', - }, - { - value: 'plan', - label: 'Plan mode', - description: 'Architecture & reasoning only, no files', - icon: 'architecture', - color: 'text-[var(--color-text-tertiary)]', - }, - { - value: 'bypassPermissions', - label: 'Bypass permissions', - description: 'Full tool access for shell and file system', - icon: 'gavel', - color: 'text-[var(--color-error)]', - }, -] - const MODE_ICONS: Record = { default: 'verified_user', acceptEdits: 'bolt', @@ -48,14 +14,6 @@ const MODE_ICONS: Record = { dontAsk: 'gavel', } -const MODE_LABELS: Record = { - default: 'Ask permissions', - acceptEdits: 'Auto accept', - plan: 'Plan mode', - bypassPermissions: 'Bypass', - dontAsk: 'Don\'t ask', -} - type Props = { workDir?: string /** Controlled mode: override current value */ @@ -65,6 +23,7 @@ type Props = { } export function PermissionModeSelector({ workDir: workDirProp, value, onChange }: Props = {}) { + const t = useTranslation() const { permissionMode: storeMode, setPermissionMode } = useSettingsStore() const setSessionPermissionMode = useChatStore((s) => s.setSessionPermissionMode) const sessions = useSessionStore((s) => s.sessions) @@ -76,6 +35,49 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange } const isControlled = value !== undefined const currentMode = isControlled ? value : storeMode + const PERMISSION_ITEMS: Array<{ + value: PermissionMode + label: string + description: string + icon: string + color?: string + }> = [ + { + value: 'default', + label: t('permMode.askPermissions'), + description: t('permMode.askPermDesc'), + icon: 'verified_user', + }, + { + value: 'acceptEdits', + label: t('permMode.autoAccept'), + description: t('permMode.autoAcceptDesc'), + icon: 'bolt', + }, + { + value: 'plan', + label: t('permMode.planMode'), + description: t('permMode.planModeDesc'), + icon: 'architecture', + color: 'text-[var(--color-text-tertiary)]', + }, + { + value: 'bypassPermissions', + label: t('permMode.bypass'), + description: t('permMode.bypassDesc'), + icon: 'gavel', + color: 'text-[var(--color-error)]', + }, + ] + + const MODE_LABELS: Record = { + default: t('permMode.label.default'), + acceptEdits: t('permMode.label.acceptEdits'), + plan: t('permMode.label.plan'), + bypassPermissions: t('permMode.label.bypassPermissions'), + dontAsk: t('permMode.label.dontAsk'), + } + const activeSession = sessions.find((s) => s.id === activeSessionId) const workDir = workDirProp || activeSession?.workDir || '~' @@ -109,7 +111,7 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange } {open && (
- Execution Permissions + {t('permMode.executionPermissions')}
{PERMISSION_ITEMS.map((item) => (
-
Enable bypass permissions?
-
This grants full access to your system
+
{t('permMode.enableBypassTitle')}
+
{t('permMode.enableBypassSubtitle')}
{/* Body */}
-

- Claude will have unrestricted access to execute shell commands and modify files within: -

+

folder {workDir} @@ -181,15 +181,15 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange }
  • check - Read, write, and delete any files + {t('permMode.permReadWrite')}
  • check - Execute arbitrary shell commands + {t('permMode.permShell')}
  • check - Install or remove packages + {t('permMode.permPackages')}
@@ -200,7 +200,7 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange } onClick={() => setConfirmDialog(false)} className="px-4 py-2 text-xs font-semibold text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors" > - Cancel + {t('common.cancel')}
diff --git a/desktop/src/components/layout/ProjectFilter.tsx b/desktop/src/components/layout/ProjectFilter.tsx index 52ec8cb3..406524f3 100644 --- a/desktop/src/components/layout/ProjectFilter.tsx +++ b/desktop/src/components/layout/ProjectFilter.tsx @@ -1,7 +1,9 @@ import { useState, useRef, useEffect } from 'react' import { useSessionStore } from '../../stores/sessionStore' +import { useTranslation } from '../../i18n' export function ProjectFilter() { + const t = useTranslation() const { availableProjects, selectedProjects, setSelectedProjects } = useSessionStore() const [open, setOpen] = useState(false) const ref = useRef(null) @@ -19,9 +21,9 @@ export function ProjectFilter() { const isAllSelected = selectedProjects.length === 0 const label = isAllSelected - ? 'All projects' + ? t('sidebar.allProjects') : selectedProjects.length === 1 - ? getDisplayName(selectedProjects[0]!) + ? getDisplayName(selectedProjects[0]!, t('sidebar.other')) : `${selectedProjects.length} projects` const toggleProject = (path: string) => { @@ -62,7 +64,7 @@ export function ProjectFilter() { className="w-full flex items-center gap-2.5 px-3 py-1.5 text-sm text-left hover:bg-[var(--color-surface-hover)] transition-colors" > - All projects + {t('sidebar.allProjects')} {isAllSelected && } @@ -78,7 +80,7 @@ export function ProjectFilter() { className="w-full flex items-center gap-2.5 px-3 py-1.5 text-sm text-left hover:bg-[var(--color-surface-hover)] transition-colors" > - {getDisplayName(path)} + {getDisplayName(path, t('sidebar.other'))} {checked && } ) @@ -89,10 +91,10 @@ export function ProjectFilter() { ) } -function getDisplayName(sanitizedPath: string): string { - if (!sanitizedPath || sanitizedPath === '_unknown') return 'Other' +function getDisplayName(sanitizedPath: string, fallback: string = 'Other'): string { + if (!sanitizedPath || sanitizedPath === '_unknown') return fallback const segments = sanitizedPath.split('-').filter(Boolean) - return segments[segments.length - 1] || 'Other' + return segments[segments.length - 1] || fallback } function ChevronIcon({ open }: { open: boolean }) { diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx index baa59f63..7a4009ae 100644 --- a/desktop/src/components/layout/Sidebar.tsx +++ b/desktop/src/components/layout/Sidebar.tsx @@ -1,14 +1,15 @@ import { useEffect, useState, useCallback, useMemo } from 'react' import { useSessionStore } from '../../stores/sessionStore' import { useUIStore } from '../../stores/uiStore' +import { useTranslation } from '../../i18n' import { ProjectFilter } from './ProjectFilter' import type { SessionListItem } from '../../types/session' const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window) -type TimeGroup = 'Today' | 'Yesterday' | 'Last 7 days' | 'Last 30 days' | 'Older' +type TimeGroup = 'today' | 'yesterday' | 'last7days' | 'last30days' | 'older' -const TIME_GROUP_ORDER: TimeGroup[] = ['Today', 'Yesterday', 'Last 7 days', 'Last 30 days', 'Older'] +const TIME_GROUP_ORDER: TimeGroup[] = ['today', 'yesterday', 'last7days', 'last30days', 'older'] export function Sidebar() { const { @@ -79,6 +80,16 @@ export function Sidebar() { setRenameValue('') }, [renamingId, renameValue, renameSession]) + const t = useTranslation() + + const TIME_GROUP_LABELS: Record = { + today: t('sidebar.timeGroup.today'), + yesterday: t('sidebar.timeGroup.yesterday'), + last7days: t('sidebar.timeGroup.last7days'), + last30days: t('sidebar.timeGroup.last30days'), + older: t('sidebar.timeGroup.older'), + } + return (
@@ -127,7 +138,7 @@ export function Sidebar() { setSearchQuery(e.target.value)} className="w-full h-7 px-2 text-xs rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none focus:border-[var(--color-border-focus)]" @@ -138,19 +149,19 @@ export function Sidebar() {
{error && (
-
Session list failed to load
+
{t('sidebar.sessionListFailed')}
{error}
)} {filteredSessions.length === 0 && (
- {searchQuery ? 'No matching sessions' : 'No sessions yet'} + {searchQuery ? t('sidebar.noMatching') : t('sidebar.noSessions')}
)} {TIME_GROUP_ORDER.map((group) => { @@ -159,7 +170,7 @@ export function Sidebar() { return (
- {group} + {TIME_GROUP_LABELS[group]}
{items.map((session) => (
@@ -195,9 +206,9 @@ export function Sidebar() { {!session.workDirExists && ( - missing dir + {t('sidebar.missingDir')} )} @@ -219,7 +230,7 @@ export function Sidebar() { onClick={() => setActiveView('settings')} icon={settings} > - Settings + {t('sidebar.settings')}
@@ -236,13 +247,13 @@ export function Sidebar() { }} className="w-full px-3 py-1.5 text-xs text-left text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors" > - Rename + {t('common.rename')}
)} @@ -261,11 +272,11 @@ function groupByTime(sessions: SessionListItem[]): Map= startOfToday) group = 'Today' - else if (ts >= startOfYesterday) group = 'Yesterday' - else if (ts >= sevenDaysAgo) group = 'Last 7 days' - else if (ts >= thirtyDaysAgo) group = 'Last 30 days' - else group = 'Older' + if (ts >= startOfToday) group = 'today' + else if (ts >= startOfYesterday) group = 'yesterday' + else if (ts >= sevenDaysAgo) group = 'last7days' + else if (ts >= thirtyDaysAgo) group = 'last30days' + else group = 'older' if (!groups.has(group)) groups.set(group, []) groups.get(group)!.push(session) diff --git a/desktop/src/components/layout/StatusBar.tsx b/desktop/src/components/layout/StatusBar.tsx index b5eeb973..0be7bca2 100644 --- a/desktop/src/components/layout/StatusBar.tsx +++ b/desktop/src/components/layout/StatusBar.tsx @@ -1,11 +1,13 @@ import { useSettingsStore } from '../../stores/settingsStore' import { useChatStore } from '../../stores/chatStore' import { useSessionStore } from '../../stores/sessionStore' +import { useTranslation } from '../../i18n' export function StatusBar() { const { currentModel } = useSettingsStore() const { connectionState } = useChatStore() const { sessions, activeSessionId } = useSessionStore() + const t = useTranslation() const activeSession = sessions.find((s) => s.id === activeSessionId) @@ -18,12 +20,12 @@ export function StatusBar() { const statusText = connectionState === 'connected' - ? 'Connected' + ? t('status.connected') : connectionState === 'connecting' - ? 'Connecting...' + ? t('status.connecting') : connectionState === 'reconnecting' - ? 'Reconnecting...' - : 'Disconnected' + ? t('status.reconnecting') + : t('status.disconnected') const projectName = activeSession?.projectPath ? activeSession.projectPath.split('-').filter(Boolean).pop() || '' diff --git a/desktop/src/components/layout/TitleBar.tsx b/desktop/src/components/layout/TitleBar.tsx index eb1a63b7..bf34f066 100644 --- a/desktop/src/components/layout/TitleBar.tsx +++ b/desktop/src/components/layout/TitleBar.tsx @@ -1,7 +1,9 @@ import { useUIStore } from '../../stores/uiStore' +import { useTranslation } from '../../i18n' export function TitleBar() { const { activeView, setActiveView } = useUIStore() + const t = useTranslation() return (
setActiveView('code')} icon="code" > - Code + {t('titlebar.code')} setActiveView('terminal')} icon="terminal" > - Terminal + {t('titlebar.terminal')} setActiveView('history')} icon="history" > - History + {t('titlebar.history')}
diff --git a/desktop/src/components/shared/DirectoryPicker.tsx b/desktop/src/components/shared/DirectoryPicker.tsx index cacfa273..56a84d3c 100644 --- a/desktop/src/components/shared/DirectoryPicker.tsx +++ b/desktop/src/components/shared/DirectoryPicker.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from 'react' import { sessionsApi, type RecentProject } from '../../api/sessions' import { filesystemApi } from '../../api/filesystem' +import { useTranslation } from '../../i18n' type Props = { value: string @@ -14,6 +15,7 @@ function isTauriRuntime() { } export function DirectoryPicker({ value, onChange }: Props) { + const t = useTranslation() const [isOpen, setIsOpen] = useState(false) const [mode, setMode] = useState<'recent' | 'browse'>('recent') const [projects, setProjects] = useState([]) @@ -69,7 +71,7 @@ export function DirectoryPicker({ value, onChange }: Props) { const selected = await open({ directory: true, multiple: false, - title: 'Choose project folder', + title: t('dirPicker.chooseProjectFolder'), }) if (selected) onChange(selected) } catch (err) { @@ -120,7 +122,7 @@ export function DirectoryPicker({ value, onChange }: Props) { className="flex items-center gap-2 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors" > folder_open - Select a project... + {t('dirPicker.selectProject')} )} @@ -130,13 +132,13 @@ export function DirectoryPicker({ value, onChange }: Props) { {mode === 'recent' ? ( <>
- Recent + {t('dirPicker.recent')}
{loading ? ( -
Loading...
+
{t('common.loading')}
) : projects.length === 0 ? ( -
No recent projects
+
{t('dirPicker.noRecent')}
) : ( projects.map((project) => { const isSelected = project.realPath === value @@ -182,7 +184,7 @@ export function DirectoryPicker({ value, onChange }: Props) { className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-[var(--color-surface-hover)] transition-colors" > create_new_folder - Choose a different folder + {t('dirPicker.chooseFolder')}
@@ -191,7 +193,7 @@ export function DirectoryPicker({ value, onChange }: Props) { <>
{browsePath.split('/').filter(Boolean).map((seg, i, arr) => ( @@ -207,7 +209,7 @@ export function DirectoryPicker({ value, onChange }: Props) {
{loading ? ( -
Loading...
+
{t('common.loading')}
) : ( <> {browseParent && browseParent !== browsePath && ( @@ -217,7 +219,7 @@ export function DirectoryPicker({ value, onChange }: Props) { )} {browseEntries.length === 0 ? ( -
No subdirectories
+
{t('dirPicker.noSubdirs')}
) : browseEntries.map((entry) => ( ))} @@ -238,7 +240,7 @@ export function DirectoryPicker({ value, onChange }: Props) {
{browsePath}
diff --git a/desktop/src/components/tasks/NewTaskModal.tsx b/desktop/src/components/tasks/NewTaskModal.tsx index 8bbe34d0..40658886 100644 --- a/desktop/src/components/tasks/NewTaskModal.tsx +++ b/desktop/src/components/tasks/NewTaskModal.tsx @@ -5,6 +5,7 @@ import { Modal } from '../shared/Modal' import { Input } from '../shared/Input' import { Button } from '../shared/Button' import { PromptEditor } from './PromptEditor' +import { useTranslation } from '../../i18n' import type { PermissionMode } from '../../types/settings' type Props = { @@ -14,14 +15,6 @@ type Props = { type FrequencyKey = 'hourly' | 'daily' | 'weekdays' | 'weekly' | 'monthly' -const FREQUENCY_OPTIONS: Array<{ value: FrequencyKey; label: string; showTime: boolean }> = [ - { value: 'hourly', label: 'Hourly', showTime: false }, - { value: 'daily', label: 'Daily', showTime: true }, - { value: 'weekdays', label: 'Weekdays', showTime: true }, - { value: 'weekly', label: 'Weekly', showTime: true }, - { value: 'monthly', label: 'Monthly', showTime: true }, -] - function buildCron(freq: FrequencyKey, time: string): string { const [hours, minutes] = time.split(':').map(Number) switch (freq) { @@ -34,12 +27,21 @@ function buildCron(freq: FrequencyKey, time: string): string { } export function NewTaskModal({ open, onClose }: Props) { + const t = useTranslation() const { createTask } = useTaskStore() const sessions = useSessionStore((s) => s.sessions) const activeSessionId = useSessionStore((s) => s.activeSessionId) const activeSession = sessions.find((s) => s.id === activeSessionId) const defaultWorkDir = activeSession?.workDir || '' + const FREQUENCY_OPTIONS: Array<{ value: FrequencyKey; label: string; showTime: boolean }> = [ + { value: 'hourly', label: t('newTask.hourly'), showTime: false }, + { value: 'daily', label: t('newTask.daily'), showTime: true }, + { value: 'weekdays', label: t('newTask.weekdays'), showTime: true }, + { value: 'weekly', label: t('newTask.weekly'), showTime: true }, + { value: 'monthly', label: t('newTask.monthly'), showTime: true }, + ] + const [name, setName] = useState('') const [description, setDescription] = useState('') const [prompt, setPrompt] = useState('') @@ -92,11 +94,11 @@ export function NewTaskModal({ open, onClose }: Props) { - - + + } > @@ -104,25 +106,25 @@ export function NewTaskModal({ open, onClose }: Props) {
info - Local tasks only run while your computer is awake. + {t('newTask.localWarning')}
setName(e.target.value)} - placeholder="daily-code-review" + placeholder={t('newTask.namePlaceholder')} /> setDescription(e.target.value)} - placeholder="Review yesterday's commits and flag anything concerning" + placeholder={t('newTask.descPlaceholder')} /> {/* Prompt editor with embedded controls */} @@ -142,7 +144,7 @@ export function NewTaskModal({ open, onClose }: Props) { {/* Frequency */}
- +
setName(e.target.value)} placeholder="Provider name" /> + setName(e.target.value)} placeholder={t('settings.providers.namePlaceholder')} /> - setNotes(e.target.value)} placeholder="Optional notes..." /> + setNotes(e.target.value)} placeholder={t('settings.providers.notesPlaceholder')} /> {/* Base URL */} {isCustom || mode === 'edit' ? ( - setBaseUrl(e.target.value)} placeholder="https://api.example.com/anthropic" /> + setBaseUrl(e.target.value)} placeholder={t('settings.providers.baseUrlPlaceholder')} /> ) : (
- +
{baseUrl}
@@ -407,7 +412,7 @@ function ProviderFormModal({ open, onClose, mode, provider }: ProviderFormProps) )} - +
- setModels({ ...models, main: e.target.value })} placeholder="Model ID" /> - setModels({ ...models, haiku: e.target.value })} placeholder="Same as main" /> - setModels({ ...models, sonnet: e.target.value })} placeholder="Same as main" /> - setModels({ ...models, opus: e.target.value })} placeholder="Same as main" /> + setModels({ ...models, main: e.target.value })} placeholder="Model ID" /> + setModels({ ...models, haiku: e.target.value })} placeholder={t('settings.providers.sameAsMain')} /> + setModels({ ...models, sonnet: e.target.value })} placeholder={t('settings.providers.sameAsMain')} /> + setModels({ ...models, opus: e.target.value })} placeholder={t('settings.providers.sameAsMain')} />
{/* Test connection */}
{testResult && ( - {testResult.success ? `Connected (${testResult.latencyMs}ms)` : `Failed: ${testResult.error}`} + {testResult.success ? t('settings.providers.connected', { latency: String(testResult.latencyMs) }) : t('settings.providers.failed', { error: testResult.error || '' })} )}
{/* Settings JSON — editable, shown for all presets including official */}
- +