-
+
+
{t.language.label}
{t.language.description}
{isSavingLocale &&
{t.language.saving}
}
-
{LOCALE_META[locale].name}
-
-
- {locales.map(code => {
- const active = locale === code
-
- return (
-
- )
- })}
+
diff --git a/apps/desktop/src/app/settings/config-settings.tsx b/apps/desktop/src/app/settings/config-settings.tsx
index 8645162b780..2d550560764 100644
--- a/apps/desktop/src/app/settings/config-settings.tsx
+++ b/apps/desktop/src/app/settings/config-settings.tsx
@@ -19,6 +19,7 @@ import { notify, notifyError } from '@/store/notifications'
import type { ConfigFieldSchema, HermesConfigRecord } from '@/types/hermes'
import { CONTROL_TEXT, EMPTY_SELECT_VALUE, FIELD_DESCRIPTIONS, FIELD_LABELS, SECTIONS } from './constants'
+import { fieldCopyForSchemaKey } from './field-copy'
import { enumOptionsFor, getNested, prettyName, setNested } from './helpers'
import { ModelSettings } from './model-settings'
import { EmptyState, ListRow, LoadingState, SettingsContent } from './primitives'
@@ -39,15 +40,18 @@ 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)
+ fieldCopyForSchemaKey(t.settings.fieldLabels, schemaKey) ??
+ fieldCopyForSchemaKey(FIELD_LABELS, schemaKey) ??
+ prettyName(schemaKey.split('.').pop() ?? schemaKey)
const normalize = (v: string) => v.toLowerCase().replace(/[^a-z0-9]+/g, '')
const rawDescription = (
- t.settings.fieldDescriptions[schemaKey] ??
- FIELD_DESCRIPTIONS[schemaKey] ??
+ fieldCopyForSchemaKey(t.settings.fieldDescriptions, schemaKey) ??
+ fieldCopyForSchemaKey(FIELD_DESCRIPTIONS, schemaKey) ??
schema.description ??
''
).trim()
@@ -88,8 +92,8 @@ function ConfigField({
{option
? (optionLabels?.[option] ?? prettyName(option))
: schemaKey === 'display.personality'
- ? 'None'
- : '(none)'}
+ ? c.none
+ : c.noneParen}
))}
@@ -109,7 +113,7 @@ function ConfigField({
onChange(n)
}
}}
- placeholder="Not set"
+ placeholder={c.notSet}
type="number"
value={value === undefined || value === null ? '' : String(value)}
/>
@@ -128,7 +132,7 @@ function ConfigField({
.filter(Boolean)
)
}
- placeholder="comma-separated values"
+ placeholder={c.commaSeparated}
value={Array.isArray(value) ? value.join(', ') : String(value ?? '')}
/>
)
@@ -145,7 +149,7 @@ function ConfigField({
/* keep last valid */
}
}}
- placeholder="Not set"
+ placeholder={c.notSet}
spellCheck={false}
value={JSON.stringify(value, null, 2)}
/>,
@@ -160,14 +164,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 99efb342589..4d0e11b2822 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',
+ modelContextLength: 'Context Window',
+ fallbackProviders: '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',
- 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',
- 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'
-}
+ display: {
+ personality: 'Personality',
+ showReasoning: 'Reasoning Blocks'
+ },
+ agent: {
+ maxTurns: 'Max Agent Steps',
+ imageInputMode: 'Image Attachments',
+ apiMaxRetries: 'API Retries',
+ serviceTier: 'Service Tier',
+ toolUseEnforcement: 'Tool-Use Enforcement'
+ },
+ terminal: {
+ cwd: 'Working Directory',
+ backend: 'Execution Backend',
+ timeout: 'Command Timeout',
+ persistentShell: 'Persistent Shell',
+ envPassthrough: 'Environment Passthrough'
+ },
+ fileReadMaxChars: 'File Read Limit',
+ toolOutput: {
+ maxBytes: 'Terminal Output Limit',
+ maxLines: 'File Page Limit',
+ maxLineLength: 'Line Length Limit'
+ },
+ codeExecution: {
+ mode: 'Code Execution Mode'
+ },
+ approvals: {
+ mode: 'Approval Mode',
+ timeout: 'Approval Timeout',
+ mcpReloadConfirm: 'Confirm MCP Reloads'
+ },
+ commandAllowlist: 'Command Allowlist',
+ security: {
+ redactSecrets: 'Redact Secrets',
+ allowPrivateUrls: 'Allow Private URLs'
+ },
+ browser: {
+ allowPrivateUrls: 'Browser Private URLs',
+ autoLocalForPrivateUrls: 'Local Browser For Private URLs'
+ },
+ checkpoints: {
+ enabled: 'File Checkpoints',
+ maxSnapshots: 'Checkpoint Limit'
+ },
+ voice: {
+ recordKey: 'Voice Shortcut',
+ maxRecordingSeconds: 'Max Recording Length',
+ autoTts: 'Read Responses Aloud'
+ },
+ stt: {
+ enabled: 'Speech To Text',
+ provider: 'Speech-To-Text Provider',
+ local: {
+ model: 'Local Transcription Model',
+ language: 'Transcription Language'
+ },
+ elevenlabs: {
+ modelId: 'ElevenLabs STT Model',
+ languageCode: 'ElevenLabs Language',
+ tagAudioEvents: '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: {
+ voiceId: 'ElevenLabs Voice',
+ modelId: 'ElevenLabs Model'
+ }
+ },
+ memory: {
+ memoryEnabled: 'Persistent Memory',
+ userProfileEnabled: 'User Profile',
+ memoryCharLimit: 'Memory Budget',
+ userCharLimit: 'Profile Budget',
+ provider: 'Memory Provider'
+ },
+ context: {
+ engine: 'Context Engine'
+ },
+ compression: {
+ enabled: 'Auto-Compression',
+ threshold: 'Compression Threshold',
+ targetRatio: 'Compression Target',
+ protectLastN: 'Protected Recent Messages'
+ },
+ delegation: {
+ model: 'Subagent Model',
+ provider: 'Subagent Provider',
+ maxIterations: 'Subagent Turn Limit',
+ maxConcurrentChildren: 'Parallel Subagents',
+ childTimeoutSeconds: 'Subagent Timeout',
+ reasoningEffort: 'Subagent Reasoning Effort'
+ },
+ updates: {
+ nonInteractiveLocalChanges: '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.',
+ modelContextLength: "Leave at 0 to use the selected model's detected context window.",
+ fallbackProviders: 'Backup provider:model entries to try if the default model fails.',
+ display: {
+ personality: 'Default assistant style for new sessions.',
+ showReasoning: '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.',
- 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.'
-}
+ agent: {
+ imageInputMode: 'Controls how image attachments are sent to the model.',
+ maxTurns: 'Upper bound for tool-calling turns before Hermes stops a run.'
+ },
+ terminal: {
+ cwd: 'Default project folder for tool and terminal work.',
+ persistentShell: 'Keep shell state between commands when the backend supports it.',
+ envPassthrough: 'Environment variables to pass into tool execution.'
+ },
+ codeExecution: {
+ mode: 'How strictly code execution is scoped to the current project.'
+ },
+ fileReadMaxChars: 'Maximum characters Hermes can read from one file request.',
+ approvals: {
+ mode: 'How Hermes handles commands that need explicit approval.',
+ timeout: 'How long approval prompts wait before timing out.'
+ },
+ security: {
+ redactSecrets: 'Hide detected secrets from model-visible content when possible.'
+ },
+ checkpoints: {
+ enabled: 'Create rollback snapshots before file edits.'
+ },
+ memory: {
+ memoryEnabled: 'Save durable memories that can help future sessions.',
+ userProfileEnabled: '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: {
+ autoTts: 'Automatically speak assistant responses.'
+ },
+ stt: {
+ enabled: 'Enable local or provider-backed speech transcription.',
+ elevenlabs: {
+ languageCode: 'Optional ISO-639-3 language code. Blank lets ElevenLabs auto-detect.'
+ }
+ },
+ updates: {
+ nonInteractiveLocalChanges:
+ '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 8003b348759..614fdcf34ea 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}
)}