Refactor desktop settings i18n keys to camelCase

This commit is contained in:
Jim Liu 宝玉
2026-06-06 07:51:44 -07:00
committed by Teknium
parent c24abf5b32
commit 1c2189839d
9 changed files with 312 additions and 255 deletions
@@ -51,6 +51,7 @@ import {
SKILLS_ROUTE
} from '../routes'
import { FIELD_LABELS, SECTIONS } from '../settings/constants'
import { fieldCopyForSchemaKey } from '../settings/field-copy'
import { prettyName } from '../settings/helpers'
interface PaletteItem {
@@ -198,7 +199,10 @@ export function CommandPalette() {
[t.settings.sections]
)
const configFieldLabel = useCallback(
(key: string) => t.settings.fieldLabels[key] ?? FIELD_LABELS[key] ?? prettyName(key.split('.').pop() ?? key),
(key: string) =>
fieldCopyForSchemaKey(t.settings.fieldLabels, key) ??
fieldCopyForSchemaKey(FIELD_LABELS, key) ??
prettyName(key.split('.').pop() ?? key),
[t.settings.fieldLabels]
)
@@ -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'
@@ -42,13 +43,15 @@ function ConfigField({
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()
+57 -57
View File
@@ -248,59 +248,59 @@ export const ENUM_OPTIONS: Record<string, string[]> = {
export const FIELD_LABELS: Record<string, string> = 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',
show_reasoning: 'Reasoning Blocks'
showReasoning: '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'
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',
persistent_shell: 'Persistent Shell',
env_passthrough: 'Environment Passthrough'
persistentShell: 'Persistent Shell',
envPassthrough: 'Environment Passthrough'
},
file_read_max_chars: 'File Read Limit',
tool_output: {
max_bytes: 'Terminal Output Limit',
max_lines: 'File Page Limit',
max_line_length: 'Line Length Limit'
fileReadMaxChars: 'File Read Limit',
toolOutput: {
maxBytes: 'Terminal Output Limit',
maxLines: 'File Page Limit',
maxLineLength: 'Line Length Limit'
},
code_execution: {
codeExecution: {
mode: 'Code Execution Mode'
},
approvals: {
mode: 'Approval Mode',
timeout: 'Approval Timeout',
mcp_reload_confirm: 'Confirm MCP Reloads'
mcpReloadConfirm: 'Confirm MCP Reloads'
},
command_allowlist: 'Command Allowlist',
commandAllowlist: 'Command Allowlist',
security: {
redact_secrets: 'Redact Secrets',
allow_private_urls: 'Allow Private URLs'
redactSecrets: 'Redact Secrets',
allowPrivateUrls: 'Allow Private URLs'
},
browser: {
allow_private_urls: 'Browser Private URLs',
auto_local_for_private_urls: 'Local Browser For Private URLs'
allowPrivateUrls: 'Browser Private URLs',
autoLocalForPrivateUrls: 'Local Browser For Private URLs'
},
checkpoints: {
enabled: 'File Checkpoints',
max_snapshots: 'Checkpoint Limit'
maxSnapshots: 'Checkpoint Limit'
},
voice: {
record_key: 'Voice Shortcut',
max_recording_seconds: 'Max Recording Length',
auto_tts: 'Read Responses Aloud'
recordKey: 'Voice Shortcut',
maxRecordingSeconds: 'Max Recording Length',
autoTts: 'Read Responses Aloud'
},
stt: {
enabled: 'Speech To Text',
@@ -310,9 +310,9 @@ export const FIELD_LABELS: Record<string, string> = defineFieldCopy({
language: 'Transcription Language'
},
elevenlabs: {
model_id: 'ElevenLabs STT Model',
language_code: 'ElevenLabs Language',
tag_audio_events: 'Tag Audio Events',
modelId: 'ElevenLabs STT Model',
languageCode: 'ElevenLabs Language',
tagAudioEvents: 'Tag Audio Events',
diarize: 'Speaker Diarization'
}
},
@@ -326,15 +326,15 @@ export const FIELD_LABELS: Record<string, string> = defineFieldCopy({
voice: 'OpenAI Voice'
},
elevenlabs: {
voice_id: 'ElevenLabs Voice',
model_id: 'ElevenLabs Model'
voiceId: 'ElevenLabs Voice',
modelId: 'ElevenLabs Model'
}
},
memory: {
memory_enabled: 'Persistent Memory',
user_profile_enabled: 'User Profile',
memory_char_limit: 'Memory Budget',
user_char_limit: 'Profile Budget',
memoryEnabled: 'Persistent Memory',
userProfileEnabled: 'User Profile',
memoryCharLimit: 'Memory Budget',
userCharLimit: 'Profile Budget',
provider: 'Memory Provider'
},
context: {
@@ -343,57 +343,57 @@ export const FIELD_LABELS: Record<string, string> = defineFieldCopy({
compression: {
enabled: 'Auto-Compression',
threshold: 'Compression Threshold',
target_ratio: 'Compression Target',
protect_last_n: 'Protected Recent Messages'
targetRatio: 'Compression Target',
protectLastN: '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'
maxIterations: 'Subagent Turn Limit',
maxConcurrentChildren: 'Parallel Subagents',
childTimeoutSeconds: 'Subagent Timeout',
reasoningEffort: 'Subagent Reasoning Effort'
},
updates: {
non_interactive_local_changes: 'In-App Update Local Changes'
nonInteractiveLocalChanges: 'In-App Update Local Changes'
}
})
export const FIELD_DESCRIPTIONS: Record<string, string> = 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.',
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.',
show_reasoning: 'Show reasoning sections when the backend provides them.'
showReasoning: 'Show reasoning sections when the backend provides them.'
},
timezone: 'Used when Hermes needs local time context. Blank uses the system timezone.',
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.'
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.',
persistent_shell: 'Keep shell state between commands when the backend supports it.',
env_passthrough: 'Environment variables to pass into tool execution.'
persistentShell: 'Keep shell state between commands when the backend supports it.',
envPassthrough: 'Environment variables to pass into tool execution.'
},
code_execution: {
codeExecution: {
mode: 'How strictly code execution is scoped to the current project.'
},
file_read_max_chars: 'Maximum characters Hermes can read from one file request.',
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: {
redact_secrets: 'Hide detected secrets from model-visible content when possible.'
redactSecrets: '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.'
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.'
@@ -402,16 +402,16 @@ export const FIELD_DESCRIPTIONS: Record<string, string> = defineFieldCopy({
enabled: 'Summarize older context when conversations get large.'
},
voice: {
auto_tts: 'Automatically speak assistant responses.'
autoTts: '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.'
languageCode: 'Optional ISO-639-3 language code. Blank lets ElevenLabs auto-detect.'
}
},
updates: {
non_interactive_local_changes:
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.'
}
})
@@ -2,10 +2,22 @@ export interface FieldCopyTree {
[key: string]: string | FieldCopyTree
}
function schemaSegmentToFieldCopySegment(segment: string): string {
return segment.replace(/_([a-z0-9])/g, (_, char: string) => char.toUpperCase())
}
function isFieldCopyTree(value: unknown): value is FieldCopyTree {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
export function schemaKeyToFieldCopyKey(schemaKey: string): string {
return schemaKey.split('.').map(schemaSegmentToFieldCopySegment).join('.')
}
export function fieldCopyForSchemaKey(copy: Record<string, string>, schemaKey: string): string | undefined {
return copy[schemaKeyToFieldCopyKey(schemaKey)] ?? copy[schemaKey]
}
export function defineFieldCopy(copy: FieldCopyTree): Record<string, string> {
const result: Record<string, string> = {}
+28 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import type { HermesConfigRecord } from '@/types/hermes'
import { defineFieldCopy } from './field-copy'
import { defineFieldCopy, fieldCopyForSchemaKey, schemaKeyToFieldCopyKey } from './field-copy'
import { getNested, providerGroup, setNested, stripToolsetLabel, toolsetDisplayLabel } from './helpers'
describe('settings helpers', () => {
@@ -35,6 +35,33 @@ describe('settings helpers', () => {
})
})
it('maps schema keys to camelCase translation keys', () => {
expect(schemaKeyToFieldCopyKey('model_context_length')).toBe('modelContextLength')
expect(schemaKeyToFieldCopyKey('display.show_reasoning')).toBe('display.showReasoning')
expect(schemaKeyToFieldCopyKey('tool_output.max_line_length')).toBe('toolOutput.maxLineLength')
expect(schemaKeyToFieldCopyKey('updates.non_interactive_local_changes')).toBe(
'updates.nonInteractiveLocalChanges'
)
})
it('looks up camelCase field copy by schema key with legacy fallback', () => {
const copy = defineFieldCopy({
display: {
showReasoning: 'Reasoning Blocks'
},
file_read_max_chars: 'Legacy File Read Limit',
modelContextLength: 'Context Window',
toolOutput: {
maxLineLength: 'Line Length Limit'
}
})
expect(fieldCopyForSchemaKey(copy, 'model_context_length')).toBe('Context Window')
expect(fieldCopyForSchemaKey(copy, 'display.show_reasoning')).toBe('Reasoning Blocks')
expect(fieldCopyForSchemaKey(copy, 'tool_output.max_line_length')).toBe('Line Length Limit')
expect(fieldCopyForSchemaKey(copy, 'file_read_max_chars')).toBe('Legacy File Read Limit')
})
it('rejects duplicate flattened paths', () => {
const duplicateKey = ['display', 'personality'].join('.')