(
-
+
)}
@@ -353,8 +369,10 @@ function FileTreeBody({
}
function FileTreeLoadingState() {
+ const { t } = useI18n()
+
return (
-
+
{
// Pre-select the Terminal tab so the slot is ready to host us on return.
@@ -77,7 +79,7 @@ export function TerminalTab({ cwd, onAddSelectionToChat }: TerminalTabProps) {
type="button"
variant="secondary"
>
- Add to chat
+ {t.rightSidebar.addToChat}
{addSelectionShortcutLabel()}
diff --git a/apps/desktop/src/app/session/hooks/use-cwd-actions.ts b/apps/desktop/src/app/session/hooks/use-cwd-actions.ts
index ab301e5930..e10f34e929 100644
--- a/apps/desktop/src/app/session/hooks/use-cwd-actions.ts
+++ b/apps/desktop/src/app/session/hooks/use-cwd-actions.ts
@@ -1,5 +1,6 @@
import { type MutableRefObject, useCallback } from 'react'
+import { useI18n } from '@/i18n'
import { notify, notifyError } from '@/store/notifications'
import { $currentCwd, setCurrentBranch, setCurrentCwd } from '@/store/session'
import type { SessionRuntimeInfo } from '@/types/hermes'
@@ -17,6 +18,8 @@ export function useCwdActions({
onSessionRuntimeInfo,
requestGateway
}: CwdActionsOptions) {
+ const { t } = useI18n()
+ const copy = t.desktop
const refreshProjectBranch = useCallback(
async (cwd: string) => {
const target = cwd.trim()
@@ -85,7 +88,7 @@ export function useCwdActions({
const message = err instanceof Error ? err.message : String(err)
if (!message.includes('unknown method')) {
- notifyError(err, 'Working directory change failed')
+ notifyError(err, copy.cwdChangeFailed)
return
}
@@ -94,12 +97,12 @@ export function useCwdActions({
setCurrentBranch('')
notify({
kind: 'warning',
- title: 'Working directory staged',
- message: 'Restart the desktop backend to apply cwd changes to this active session.'
+ title: copy.cwdStagedTitle,
+ message: copy.cwdStagedMessage
})
}
},
- [activeSessionId, onSessionRuntimeInfo, requestGateway]
+ [activeSessionId, copy, onSessionRuntimeInfo, requestGateway]
)
return { changeSessionCwd, refreshProjectBranch }
diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.ts b/apps/desktop/src/app/session/hooks/use-model-controls.ts
index 3d1348434a..1a04b19da7 100644
--- a/apps/desktop/src/app/session/hooks/use-model-controls.ts
+++ b/apps/desktop/src/app/session/hooks/use-model-controls.ts
@@ -2,6 +2,7 @@ import { type QueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import { getGlobalModelInfo, setGlobalModel } from '@/hermes'
+import { useI18n } from '@/i18n'
import { notifyError } from '@/store/notifications'
import { $currentModel, $currentProvider, setCurrentModel, setCurrentProvider } from '@/store/session'
import type { ModelOptionsResponse } from '@/types/hermes'
@@ -19,6 +20,8 @@ interface ModelControlsOptions {
}
export function useModelControls({ activeSessionId, queryClient, requestGateway }: ModelControlsOptions) {
+ const { t } = useI18n()
+ const copy = t.desktop
const updateModelOptionsCache = useCallback(
(provider: string, model: string, includeGlobal: boolean) => {
const patch = (prev: ModelOptionsResponse | undefined) => ({ ...(prev ?? {}), provider, model })
@@ -91,12 +94,12 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway
setCurrentModel(prevModel)
setCurrentProvider(prevProvider)
updateModelOptionsCache(prevProvider, prevModel, includeGlobal)
- notifyError(err, 'Model switch failed')
+ notifyError(err, copy.modelSwitchFailed)
return false
}
},
- [activeSessionId, queryClient, refreshCurrentModel, requestGateway, updateModelOptionsCache]
+ [activeSessionId, copy.modelSwitchFailed, queryClient, refreshCurrentModel, requestGateway, updateModelOptionsCache]
)
return { refreshCurrentModel, selectModel, updateModelOptionsCache }
diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts
index b966841344..e31a0ce07e 100644
--- a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts
+++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts
@@ -2,7 +2,8 @@ import type { AppendMessage, ThreadMessage } from '@assistant-ui/react'
import { type MutableRefObject, useCallback } from 'react'
import { getProfiles, transcribeAudio } from '@/hermes'
-import { branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
+import { type Translations, translateNow, useI18n } from '@/i18n'
+import { appendTextPart, branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
import {
attachmentDisplayText,
parseCommandDispatch,
@@ -57,10 +58,10 @@ function blobToDataUrl(blob: Blob): Promise
{
if (typeof reader.result === 'string') {
resolve(reader.result)
} else {
- reject(new Error('Could not read recorded audio'))
+ reject(new Error(translateNow('desktop.audioReadFailed')))
}
})
- reader.addEventListener('error', () => reject(reader.error || new Error('Could not read recorded audio')))
+ reader.addEventListener('error', () => reject(reader.error || new Error(translateNow('desktop.audioReadFailed'))))
reader.readAsDataURL(blob)
})
}
@@ -101,12 +102,12 @@ interface SubmitTextOptions {
fromQueue?: boolean
}
-function renderCommandsCatalog(catalog: CommandsCatalogLike): string {
+function renderCommandsCatalog(catalog: CommandsCatalogLike, copy: Translations['desktop']): string {
const desktopCatalog = filterDesktopCommandsCatalog(catalog)
const sections = desktopCatalog.categories?.length
? desktopCatalog.categories
- : [{ name: 'Desktop commands', pairs: desktopCatalog.pairs ?? [] }]
+ : [{ name: copy.desktopCommands, pairs: desktopCatalog.pairs ?? [] }]
const body = sections
.filter(section => section.pairs.length > 0)
@@ -118,8 +119,8 @@ function renderCommandsCatalog(catalog: CommandsCatalogLike): string {
.join('\n\n')
const tail = [
- desktopCatalog.skill_count ? `${desktopCatalog.skill_count} skill commands available.` : '',
- desktopCatalog.warning ? `warning: ${desktopCatalog.warning}` : ''
+ desktopCatalog.skill_count ? copy.skillCommandsAvailable(desktopCatalog.skill_count) : '',
+ desktopCatalog.warning ? copy.warningLine(desktopCatalog.warning) : ''
]
.filter(Boolean)
.join('\n')
@@ -156,6 +157,8 @@ export function usePromptActions({
sttEnabled,
updateSessionState
}: PromptActionsOptions) {
+ const { t } = useI18n()
+ const copy = t.desktop
const appendSessionTextMessage = useCallback(
(sessionId: string, role: ChatMessage['role'], text: string) => {
const body = text.trim()
@@ -326,7 +329,7 @@ export function usePromptActions({
} catch (err) {
dropOptimistic(null)
releaseBusy()
- notifyError(err, 'Session unavailable')
+ notifyError(err, copy.sessionUnavailable)
return false
}
@@ -334,7 +337,7 @@ export function usePromptActions({
if (!sessionId) {
dropOptimistic(null)
releaseBusy()
- notify({ kind: 'error', title: 'Session unavailable', message: 'Could not create a new session' })
+ notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
return false
}
@@ -354,7 +357,7 @@ export function usePromptActions({
return true
} catch (err) {
- const message = inlineErrorMessage(err, 'Prompt failed')
+ const message = inlineErrorMessage(err, copy.promptFailed)
releaseBusy()
updateSessionState(sessionId, state => ({
@@ -365,7 +368,7 @@ export function usePromptActions({
id: `assistant-error-${Date.now()}`,
role: 'assistant',
parts: [],
- error: message || 'Prompt failed',
+ error: message || copy.promptFailed,
branchGroupId: state.pendingBranchGroup ?? undefined
}
],
@@ -376,12 +379,12 @@ export function usePromptActions({
}))
if (isProviderSetupError(err)) {
- requestDesktopOnboarding('Add a provider credential before sending your first message.')
+ requestDesktopOnboarding(copy.providerCredentialRequired)
return false
}
- notifyError(err, 'Prompt failed')
+ notifyError(err, copy.promptFailed)
return false
}
@@ -389,6 +392,7 @@ export function usePromptActions({
[
activeSessionId,
busyRef,
+ copy,
createBackendSessionForSend,
requestGateway,
selectedStoredSessionIdRef,
@@ -408,7 +412,7 @@ export function usePromptActions({
const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
if (sessionId) {
- appendSessionTextMessage(sessionId, 'system', 'empty slash command')
+ appendSessionTextMessage(sessionId, 'system', copy.emptySlashCommand)
}
return
@@ -435,16 +439,16 @@ export function usePromptActions({
if (!sid) {
setYoloActive(next)
- notify({ kind: 'success', message: next ? 'YOLO armed for this chat' : 'YOLO off' })
+ notify({ kind: 'success', message: next ? copy.yoloArmed : copy.yoloOff })
return
}
try {
const active = await setSessionYolo(requestGateway, sid, next)
- appendSessionTextMessage(sid, 'system', `YOLO ${active ? 'on' : 'off'} for this session`)
+ appendSessionTextMessage(sid, 'system', copy.yoloSystem(active))
} catch {
- notify({ kind: 'error', title: 'YOLO', message: 'Could not toggle YOLO' })
+ notify({ kind: 'error', title: copy.yoloTitle, message: copy.yoloToggleFailed })
}
return
@@ -467,7 +471,7 @@ export function usePromptActions({
if (!target) {
notify({
kind: 'success',
- message: `Profile: ${current}. Use /profile or the "New session" picker to start a chat in another profile.`
+ message: copy.profileStatus(current)
})
return
@@ -480,8 +484,8 @@ export function usePromptActions({
if (!match) {
notify({
kind: 'error',
- title: 'Unknown profile',
- message: `No profile named "${target}". Available: ${profiles.map(profile => profile.name).join(', ')}`
+ title: copy.unknownProfile,
+ message: copy.noProfileNamed(target, profiles.map(profile => profile.name).join(', '))
})
return
@@ -493,9 +497,9 @@ export function usePromptActions({
// Swap the live gateway now so an empty draft sends into this
// profile immediately; an existing thread keeps its own profile.
await ensureGatewayProfile(key)
- notify({ kind: 'success', message: `New chats will use profile ${match.name}.` })
+ notify({ kind: 'success', message: copy.newChatsProfile(match.name) })
} catch (err) {
- notifyError(err, 'Failed to set profile')
+ notifyError(err, copy.setProfileFailed)
}
return
@@ -506,8 +510,8 @@ export function usePromptActions({
if (!sessionId) {
notify({
kind: 'error',
- title: 'Session unavailable',
- message: 'Could not create a new session'
+ title: copy.sessionUnavailable,
+ message: copy.createSessionFailed
})
return
@@ -570,7 +574,7 @@ export function usePromptActions({
try {
const catalog = await requestGateway('commands.catalog', { session_id: sessionId })
- renderSlashOutput(renderCommandsCatalog(catalog))
+ renderSlashOutput(renderCommandsCatalog(catalog, copy))
} catch (err) {
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
}
@@ -658,6 +662,7 @@ export function usePromptActions({
appendSessionTextMessage,
branchCurrentSession,
busyRef,
+ copy,
createBackendSessionForSend,
handleSkinCommand,
refreshSessions,
@@ -687,7 +692,7 @@ export function usePromptActions({
const transcribeVoiceAudio = useCallback(
async (audio: Blob) => {
if (!sttEnabled) {
- throw new Error('Speech-to-text is disabled in settings.')
+ throw new Error(copy.sttDisabled)
}
const dataUrl = await blobToDataUrl(audio)
@@ -695,7 +700,7 @@ export function usePromptActions({
return result.transcript
},
- [sttEnabled]
+ [copy.sttDisabled, sttEnabled]
)
const cancelRun = useCallback(async () => {
@@ -745,9 +750,9 @@ export function usePromptActions({
} catch (err) {
setMutableRef(busyRef, false)
setBusy(false)
- notifyError(err, 'Stop failed')
+ notifyError(err, copy.stopFailed)
}
- }, [activeSessionId, activeSessionIdRef, busyRef, requestGateway, updateSessionState])
+ }, [activeSessionId, activeSessionIdRef, busyRef, copy.stopFailed, requestGateway, updateSessionState])
// Steer = nudge the live turn without interrupting: the gateway appends the
// text to the next tool result so the model reads it on its next iteration
@@ -853,10 +858,10 @@ export function usePromptActions({
busy: false,
awaitingResponse: false
}))
- notifyError(err, 'Regenerate failed')
+ notifyError(err, copy.regenerateFailed)
}
},
- [activeSessionId, requestGateway, updateSessionState]
+ [activeSessionId, copy.regenerateFailed, requestGateway, updateSessionState]
)
const editMessage = useCallback(
@@ -926,10 +931,10 @@ export function usePromptActions({
setBusy(false)
setAwaitingResponse(false)
updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
- notifyError(surfaced, 'Edit failed')
+ notifyError(surfaced, copy.editFailed)
}
},
- [activeSessionId, activeSessionIdRef, busyRef, requestGateway, updateSessionState]
+ [activeSessionId, activeSessionIdRef, busyRef, copy.editFailed, requestGateway, updateSessionState]
)
const handleThreadMessagesChange = useCallback(
diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.ts b/apps/desktop/src/app/session/hooks/use-session-actions.ts
index b0a4c7efc1..ca39d77853 100644
--- a/apps/desktop/src/app/session/hooks/use-session-actions.ts
+++ b/apps/desktop/src/app/session/hooks/use-session-actions.ts
@@ -3,6 +3,7 @@ import { useCallback, useRef } from 'react'
import type { NavigateFunction } from 'react-router-dom'
import { deleteSession, getSessionMessages, setSessionArchived } from '@/hermes'
+import { useI18n } from '@/i18n'
import { type ChatMessage, chatMessageText, preserveLocalAssistantErrors, toChatMessages } from '@/lib/chat-messages'
import { normalizePersonalityValue } from '@/lib/chat-runtime'
import { embeddedImageUrls, textWithoutEmbeddedImages } from '@/lib/embedded-images'
@@ -285,6 +286,8 @@ export function useSessionActions({
syncSessionStateToView,
updateSessionState
}: SessionActionsOptions) {
+ const { t } = useI18n()
+ const copy = t.desktop
const resumeRequestRef = useRef(0)
const startFreshSessionDraft = useCallback(
@@ -602,7 +605,7 @@ export function useSessionActions({
}
setMessages(preserveLocalAssistantErrors(toChatMessages(fallback.messages), $messages.get()))
- notifyError(err, 'Resume failed')
+ notifyError(err, copy.resumeFailed)
} finally {
if (isCurrentResume()) {
busyRef.current = false
@@ -614,6 +617,7 @@ export function useSessionActions({
[
activeSessionIdRef,
busyRef,
+ copy,
requestGateway,
runtimeIdByStoredSessionIdRef,
selectedStoredSessionIdRef,
@@ -630,8 +634,8 @@ export function useSessionActions({
if (!sourceSessionId) {
notify({
kind: 'warning',
- title: 'Nothing to branch',
- message: 'Start or resume a chat before branching.'
+ title: copy.nothingToBranch,
+ message: copy.branchNeedsChat
})
return false
@@ -640,8 +644,8 @@ export function useSessionActions({
if (busyRef.current) {
notify({
kind: 'warning',
- title: 'Session busy',
- message: 'Stop the current turn before branching this chat.'
+ title: copy.sessionBusy,
+ message: copy.branchStopCurrent
})
return false
@@ -671,8 +675,8 @@ export function useSessionActions({
if (!branchMessages.length) {
notify({
kind: 'warning',
- title: 'Nothing to branch',
- message: 'This message has no text to branch from.'
+ title: copy.nothingToBranch,
+ message: copy.branchNoText
})
return false
@@ -686,14 +690,14 @@ export function useSessionActions({
cols: 96,
...(cwd && { cwd }),
messages: branchMessages.map(({ content, role }) => ({ content, role })),
- title: 'Branch'
+ title: copy.branchTitle
})
const routedSessionId = branched.stored_session_id ?? branched.session_id
const preview = branchMessages.map(({ content }) => content).find(Boolean) ?? null
setFreshDraftReady(false)
- upsertOptimisticSession(branched, routedSessionId, 'Branch', preview)
+ upsertOptimisticSession(branched, routedSessionId, copy.branchTitle, preview)
ensureSessionState(branched.session_id, routedSessionId)
setActiveSessionId(branched.session_id)
activeSessionIdRef.current = branched.session_id
@@ -723,7 +727,7 @@ export function useSessionActions({
return true
} catch (err) {
- notifyError(err, 'Branch failed')
+ notifyError(err, copy.branchFailed)
return false
} finally {
@@ -735,6 +739,7 @@ export function useSessionActions({
[
activeSessionIdRef,
busyRef,
+ copy,
creatingSessionRef,
ensureSessionState,
navigate,
@@ -812,12 +817,13 @@ export function useSessionActions({
}
}
- notifyError(err, 'Delete failed')
+ notifyError(err, copy.deleteFailed)
}
},
[
activeSessionId,
activeSessionIdRef,
+ copy,
navigate,
requestGateway,
selectedStoredSessionId,
@@ -851,7 +857,7 @@ export function useSessionActions({
try {
await setSessionArchived(storedSessionId, true, archived?.profile)
- notify({ durationMs: 2_000, kind: 'success', message: 'Archived' })
+ notify({ durationMs: 2_000, kind: 'success', message: copy.archived })
} catch (err) {
if (archived) {
setSessions(prev => [archived, ...prev.filter(s => s.id !== storedSessionId)])
@@ -859,10 +865,10 @@ export function useSessionActions({
}
$pinnedSessionIds.set(previousPinned)
- notifyError(err, 'Archive failed')
+ notifyError(err, copy.archiveFailed)
}
},
- [selectedStoredSessionId, startFreshSessionDraft]
+ [copy, selectedStoredSessionId, startFreshSessionDraft]
)
return {
diff --git a/apps/desktop/src/app/settings/config-settings.tsx b/apps/desktop/src/app/settings/config-settings.tsx
index 8645162b78..0c12d55118 100644
--- a/apps/desktop/src/app/settings/config-settings.tsx
+++ b/apps/desktop/src/app/settings/config-settings.tsx
@@ -39,6 +39,7 @@ function ConfigField({
onChange: (value: unknown) => void
}) {
const { t } = useI18n()
+ const c = t.settings.config
const label =
t.settings.fieldLabels[schemaKey] ?? FIELD_LABELS[schemaKey] ?? prettyName(schemaKey.split('.').pop() ?? schemaKey)
@@ -88,8 +89,8 @@ function ConfigField({
{option
? (optionLabels?.[option] ?? prettyName(option))
: schemaKey === 'display.personality'
- ? 'None'
- : '(none)'}
+ ? c.none
+ : c.noneParen}
))}
@@ -109,7 +110,7 @@ function ConfigField({
onChange(n)
}
}}
- placeholder="Not set"
+ placeholder={c.notSet}
type="number"
value={value === undefined || value === null ? '' : String(value)}
/>
@@ -128,7 +129,7 @@ function ConfigField({
.filter(Boolean)
)
}
- placeholder="comma-separated values"
+ placeholder={c.commaSeparated}
value={Array.isArray(value) ? value.join(', ') : String(value ?? '')}
/>
)
@@ -145,7 +146,7 @@ function ConfigField({
/* keep last valid */
}
}}
- placeholder="Not set"
+ placeholder={c.notSet}
spellCheck={false}
value={JSON.stringify(value, null, 2)}
/>,
@@ -160,14 +161,14 @@ function ConfigField({
)}
{fields.length === 0 ? (
-
+
) : (
{fields.map(([key, field]) => (
diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts
index 99efb34258..6fb8ad9e97 100644
--- a/apps/desktop/src/app/settings/constants.ts
+++ b/apps/desktop/src/app/settings/constants.ts
@@ -14,6 +14,7 @@ import {
import type { ThemeMode } from '@/themes/context'
import type { DesktopConfigSection } from './types'
+import { defineFieldCopy } from './field-copy'
// Provider group definitions used to fold raw env-var names like
// ``XAI_API_KEY`` into a single "xAI" card with a friendly label, short
@@ -245,103 +246,175 @@ export const ENUM_OPTIONS: Record = {
'updates.non_interactive_local_changes': ['stash', 'discard']
}
-export const FIELD_LABELS: Record = {
+export const FIELD_LABELS: Record = defineFieldCopy({
model: 'Default Model',
model_context_length: 'Context Window',
fallback_providers: 'Fallback Models',
toolsets: 'Enabled Toolsets',
timezone: 'Timezone',
- 'display.personality': 'Personality',
- 'display.show_reasoning': 'Reasoning Blocks',
- 'agent.max_turns': 'Max Agent Steps',
- 'agent.image_input_mode': 'Image Attachments',
- 'terminal.cwd': 'Working Directory',
- 'terminal.backend': 'Execution Backend',
- 'terminal.timeout': 'Command Timeout',
- 'terminal.persistent_shell': 'Persistent Shell',
- 'terminal.env_passthrough': 'Environment Passthrough',
+ display: {
+ personality: 'Personality',
+ show_reasoning: 'Reasoning Blocks'
+ },
+ agent: {
+ max_turns: 'Max Agent Steps',
+ image_input_mode: 'Image Attachments',
+ api_max_retries: 'API Retries',
+ service_tier: 'Service Tier',
+ tool_use_enforcement: 'Tool-Use Enforcement'
+ },
+ terminal: {
+ cwd: 'Working Directory',
+ backend: 'Execution Backend',
+ timeout: 'Command Timeout',
+ persistent_shell: 'Persistent Shell',
+ env_passthrough: 'Environment Passthrough'
+ },
file_read_max_chars: 'File Read Limit',
- 'tool_output.max_bytes': 'Terminal Output Limit',
- 'tool_output.max_lines': 'File Page Limit',
- 'tool_output.max_line_length': 'Line Length Limit',
- 'code_execution.mode': 'Code Execution Mode',
- 'approvals.mode': 'Approval Mode',
- 'approvals.timeout': 'Approval Timeout',
- 'approvals.mcp_reload_confirm': 'Confirm MCP Reloads',
+ tool_output: {
+ max_bytes: 'Terminal Output Limit',
+ max_lines: 'File Page Limit',
+ max_line_length: 'Line Length Limit'
+ },
+ code_execution: {
+ mode: 'Code Execution Mode'
+ },
+ approvals: {
+ mode: 'Approval Mode',
+ timeout: 'Approval Timeout',
+ mcp_reload_confirm: 'Confirm MCP Reloads'
+ },
command_allowlist: 'Command Allowlist',
- 'security.redact_secrets': 'Redact Secrets',
- 'security.allow_private_urls': 'Allow Private URLs',
- 'browser.allow_private_urls': 'Browser Private URLs',
- 'browser.auto_local_for_private_urls': 'Local Browser For Private URLs',
- 'checkpoints.enabled': 'File Checkpoints',
- 'checkpoints.max_snapshots': 'Checkpoint Limit',
- 'voice.record_key': 'Voice Shortcut',
- 'voice.max_recording_seconds': 'Max Recording Length',
- 'voice.auto_tts': 'Read Responses Aloud',
- 'stt.enabled': 'Speech To Text',
- 'stt.provider': 'Speech-To-Text Provider',
- 'stt.local.model': 'Local Transcription Model',
- 'stt.local.language': 'Transcription Language',
- 'stt.elevenlabs.model_id': 'ElevenLabs STT Model',
- 'stt.elevenlabs.language_code': 'ElevenLabs Language',
- 'stt.elevenlabs.tag_audio_events': 'Tag Audio Events',
- 'stt.elevenlabs.diarize': 'Speaker Diarization',
- 'tts.provider': 'Text-To-Speech Provider',
- 'tts.edge.voice': 'Edge Voice',
- 'tts.openai.model': 'OpenAI TTS Model',
- 'tts.openai.voice': 'OpenAI Voice',
- 'tts.elevenlabs.voice_id': 'ElevenLabs Voice',
- 'tts.elevenlabs.model_id': 'ElevenLabs Model',
- 'memory.memory_enabled': 'Persistent Memory',
- 'memory.user_profile_enabled': 'User Profile',
- 'memory.memory_char_limit': 'Memory Budget',
- 'memory.user_char_limit': 'Profile Budget',
- 'memory.provider': 'Memory Provider',
- 'context.engine': 'Context Engine',
- 'compression.enabled': 'Auto-Compression',
- 'compression.threshold': 'Compression Threshold',
- 'compression.target_ratio': 'Compression Target',
- 'compression.protect_last_n': 'Protected Recent Messages',
- 'agent.api_max_retries': 'API Retries',
- 'agent.service_tier': 'Service Tier',
- 'agent.tool_use_enforcement': 'Tool-Use Enforcement',
- 'delegation.model': 'Subagent Model',
- 'delegation.provider': 'Subagent Provider',
- 'delegation.max_iterations': 'Subagent Turn Limit',
- 'delegation.max_concurrent_children': 'Parallel Subagents',
- 'delegation.child_timeout_seconds': 'Subagent Timeout',
- 'delegation.reasoning_effort': 'Subagent Reasoning Effort',
- 'updates.non_interactive_local_changes': 'In-App Update Local Changes'
-}
+ security: {
+ redact_secrets: 'Redact Secrets',
+ allow_private_urls: 'Allow Private URLs'
+ },
+ browser: {
+ allow_private_urls: 'Browser Private URLs',
+ auto_local_for_private_urls: 'Local Browser For Private URLs'
+ },
+ checkpoints: {
+ enabled: 'File Checkpoints',
+ max_snapshots: 'Checkpoint Limit'
+ },
+ voice: {
+ record_key: 'Voice Shortcut',
+ max_recording_seconds: 'Max Recording Length',
+ auto_tts: 'Read Responses Aloud'
+ },
+ stt: {
+ enabled: 'Speech To Text',
+ provider: 'Speech-To-Text Provider',
+ local: {
+ model: 'Local Transcription Model',
+ language: 'Transcription Language'
+ },
+ elevenlabs: {
+ model_id: 'ElevenLabs STT Model',
+ language_code: 'ElevenLabs Language',
+ tag_audio_events: 'Tag Audio Events',
+ diarize: 'Speaker Diarization'
+ }
+ },
+ tts: {
+ provider: 'Text-To-Speech Provider',
+ edge: {
+ voice: 'Edge Voice'
+ },
+ openai: {
+ model: 'OpenAI TTS Model',
+ voice: 'OpenAI Voice'
+ },
+ elevenlabs: {
+ voice_id: 'ElevenLabs Voice',
+ model_id: 'ElevenLabs Model'
+ }
+ },
+ memory: {
+ memory_enabled: 'Persistent Memory',
+ user_profile_enabled: 'User Profile',
+ memory_char_limit: 'Memory Budget',
+ user_char_limit: 'Profile Budget',
+ provider: 'Memory Provider'
+ },
+ context: {
+ engine: 'Context Engine'
+ },
+ compression: {
+ enabled: 'Auto-Compression',
+ threshold: 'Compression Threshold',
+ target_ratio: 'Compression Target',
+ protect_last_n: 'Protected Recent Messages'
+ },
+ delegation: {
+ model: 'Subagent Model',
+ provider: 'Subagent Provider',
+ max_iterations: 'Subagent Turn Limit',
+ max_concurrent_children: 'Parallel Subagents',
+ child_timeout_seconds: 'Subagent Timeout',
+ reasoning_effort: 'Subagent Reasoning Effort'
+ },
+ updates: {
+ non_interactive_local_changes: 'In-App Update Local Changes'
+ }
+})
-export const FIELD_DESCRIPTIONS: Record = {
+export const FIELD_DESCRIPTIONS: Record = defineFieldCopy({
model: 'Used for new chats unless you pick a different model in the composer.',
model_context_length: "Leave at 0 to use the selected model's detected context window.",
fallback_providers: 'Backup provider:model entries to try if the default model fails.',
- 'display.personality': 'Default assistant style for new sessions.',
+ display: {
+ personality: 'Default assistant style for new sessions.',
+ show_reasoning: 'Show reasoning sections when the backend provides them.'
+ },
timezone: 'Used when Hermes needs local time context. Blank uses the system timezone.',
- 'display.show_reasoning': 'Show reasoning sections when the backend provides them.',
- 'agent.image_input_mode': 'Controls how image attachments are sent to the model.',
- 'terminal.cwd': 'Default project folder for tool and terminal work.',
- 'code_execution.mode': 'How strictly code execution is scoped to the current project.',
- 'terminal.persistent_shell': 'Keep shell state between commands when the backend supports it.',
- 'terminal.env_passthrough': 'Environment variables to pass into tool execution.',
+ agent: {
+ image_input_mode: 'Controls how image attachments are sent to the model.',
+ max_turns: 'Upper bound for tool-calling turns before Hermes stops a run.'
+ },
+ terminal: {
+ cwd: 'Default project folder for tool and terminal work.',
+ persistent_shell: 'Keep shell state between commands when the backend supports it.',
+ env_passthrough: 'Environment variables to pass into tool execution.'
+ },
+ code_execution: {
+ mode: 'How strictly code execution is scoped to the current project.'
+ },
file_read_max_chars: 'Maximum characters Hermes can read from one file request.',
- 'approvals.mode': 'How Hermes handles commands that need explicit approval.',
- 'approvals.timeout': 'How long approval prompts wait before timing out.',
- 'security.redact_secrets': 'Hide detected secrets from model-visible content when possible.',
- 'checkpoints.enabled': 'Create rollback snapshots before file edits.',
- 'memory.memory_enabled': 'Save durable memories that can help future sessions.',
- 'memory.user_profile_enabled': 'Maintain a compact profile of user preferences.',
- 'context.engine': 'Strategy for managing long conversations near the context limit.',
- 'compression.enabled': 'Summarize older context when conversations get large.',
- 'voice.auto_tts': 'Automatically speak assistant responses.',
- 'stt.enabled': 'Enable local or provider-backed speech transcription.',
- 'stt.elevenlabs.language_code': 'Optional ISO-639-3 language code. Blank lets ElevenLabs auto-detect.',
- 'agent.max_turns': 'Upper bound for tool-calling turns before Hermes stops a run.',
- 'updates.non_interactive_local_changes':
- 'When Hermes updates itself from the app (no terminal prompt), keep local source edits (stash) or throw them away (discard). Terminal updates always ask.'
-}
+ approvals: {
+ mode: 'How Hermes handles commands that need explicit approval.',
+ timeout: 'How long approval prompts wait before timing out.'
+ },
+ security: {
+ redact_secrets: 'Hide detected secrets from model-visible content when possible.'
+ },
+ checkpoints: {
+ enabled: 'Create rollback snapshots before file edits.'
+ },
+ memory: {
+ memory_enabled: 'Save durable memories that can help future sessions.',
+ user_profile_enabled: 'Maintain a compact profile of user preferences.'
+ },
+ context: {
+ engine: 'Strategy for managing long conversations near the context limit.'
+ },
+ compression: {
+ enabled: 'Summarize older context when conversations get large.'
+ },
+ voice: {
+ auto_tts: 'Automatically speak assistant responses.'
+ },
+ stt: {
+ enabled: 'Enable local or provider-backed speech transcription.',
+ elevenlabs: {
+ language_code: 'Optional ISO-639-3 language code. Blank lets ElevenLabs auto-detect.'
+ }
+ },
+ updates: {
+ non_interactive_local_changes:
+ 'When Hermes updates itself from the app (no terminal prompt), keep local source edits (stash) or throw them away (discard). Terminal updates always ask.'
+ }
+})
// Curated desktop config surface: only fields a user might tune from the app.
export const SECTIONS: DesktopConfigSection[] = [
diff --git a/apps/desktop/src/app/settings/credential-key-ui.tsx b/apps/desktop/src/app/settings/credential-key-ui.tsx
index 8003b34875..614fdcf34e 100644
--- a/apps/desktop/src/app/settings/credential-key-ui.tsx
+++ b/apps/desktop/src/app/settings/credential-key-ui.tsx
@@ -2,6 +2,7 @@ import { type ChangeEvent, type KeyboardEvent } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
+import { translateNow, useI18n } from '@/i18n'
import { ChevronDown, ExternalLink, Loader2, Save } from '@/lib/icons'
import { cn } from '@/lib/utils'
import type { EnvVarInfo } from '@/types/hermes'
@@ -27,7 +28,11 @@ export const friendlyFieldLabel = (key: string, info: EnvVarInfo) =>
.replace(/\b\w/g, c => c.toUpperCase())
export const credentialPlaceholder = (key: string, info: EnvVarInfo, label: string): string =>
- isKeyVar(key, info) ? `Paste ${label} key` : /URL$/i.test(key) ? 'https://…' : 'Optional'
+ isKeyVar(key, info)
+ ? translateNow('settings.credentials.pasteLabelKey', label)
+ : /URL$/i.test(key)
+ ? 'https://…'
+ : translateNow('settings.credentials.optional')
// A single credential field: a set key shows as a filled read-only input
// (redacted value) that edits in place on click. Save appears once typed; a set
@@ -43,6 +48,7 @@ export function KeyField({
rowProps: KeyRowProps
varKey: string
}) {
+ const { t } = useI18n()
const { edits, onClear, onSave, saving, setEdits } = rowProps
const editing = edits[varKey] !== undefined
const draft = edits[varKey] ?? ''
@@ -84,14 +90,14 @@ export function KeyField({
className={cn(CREDENTIAL_CONTROL_CLASS, 'min-w-0 flex-1')}
onChange={update}
onKeyDown={keydown}
- placeholder={placeholder ?? 'Paste key'}
+ placeholder={placeholder ?? t.settings.credentials.pasteKey}
type={editType}
value={draft}
/>
{dirty && (
)}
@@ -106,12 +112,12 @@ export function KeyField({
type="button"
variant="text"
>
- Remove
+ {t.settings.credentials.remove}
- or
+ {t.settings.credentials.or}
>
)}
- esc to cancel
+ {t.settings.credentials.escToCancel}