Refactor desktop settings i18n keys to camelCase
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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> = {}
|
||||
|
||||
|
||||
@@ -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('.')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user