diff --git a/apps/desktop/src/app/command-palette/index.tsx b/apps/desktop/src/app/command-palette/index.tsx index 236aac244bd..35a246ff330 100644 --- a/apps/desktop/src/app/command-palette/index.tsx +++ b/apps/desktop/src/app/command-palette/index.tsx @@ -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] ) diff --git a/apps/desktop/src/app/settings/config-settings.tsx b/apps/desktop/src/app/settings/config-settings.tsx index 0c12d551184..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' @@ -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() diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index 6fb8ad9e97d..4d0e11b2822 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -248,59 +248,59 @@ export const ENUM_OPTIONS: 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', - 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 = 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 = 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 = 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 = 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 = 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.' } }) diff --git a/apps/desktop/src/app/settings/field-copy.ts b/apps/desktop/src/app/settings/field-copy.ts index 9077f6d6852..e66c00de781 100644 --- a/apps/desktop/src/app/settings/field-copy.ts +++ b/apps/desktop/src/app/settings/field-copy.ts @@ -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, schemaKey: string): string | undefined { + return copy[schemaKeyToFieldCopyKey(schemaKey)] ?? copy[schemaKey] +} + export function defineFieldCopy(copy: FieldCopyTree): Record { const result: Record = {} diff --git a/apps/desktop/src/app/settings/helpers.test.ts b/apps/desktop/src/app/settings/helpers.test.ts index e607c165bcd..ee2377a24b1 100644 --- a/apps/desktop/src/app/settings/helpers.test.ts +++ b/apps/desktop/src/app/settings/helpers.test.ts @@ -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('.') diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index afd0008a904..3565add8fe3 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -219,59 +219,59 @@ export const ja = defineLocale({ }, fieldLabels: defineFieldCopy({ model: 'デフォルトモデル', - model_context_length: 'コンテキストウィンドウ', - fallback_providers: 'フォールバックモデル', + modelContextLength: 'コンテキストウィンドウ', + fallbackProviders: 'フォールバックモデル', toolsets: '有効なツールセット', timezone: 'タイムゾーン', display: { personality: '人格', - show_reasoning: '推論ブロック' + showReasoning: '推論ブロック' }, agent: { - max_turns: '最大エージェントステップ', - image_input_mode: '画像添付', - api_max_retries: 'API 再試行回数', - service_tier: 'サービス階層', - tool_use_enforcement: 'ツール使用の強制' + maxTurns: '最大エージェントステップ', + imageInputMode: '画像添付', + apiMaxRetries: 'API 再試行回数', + serviceTier: 'サービス階層', + toolUseEnforcement: 'ツール使用の強制' }, terminal: { cwd: '作業ディレクトリ', backend: '実行バックエンド', timeout: 'コマンドタイムアウト', - persistent_shell: '永続シェル', - env_passthrough: '環境変数の引き継ぎ' + persistentShell: '永続シェル', + envPassthrough: '環境変数の引き継ぎ' }, - file_read_max_chars: 'ファイル読み取り上限', - tool_output: { - max_bytes: 'ターミナル出力上限', - max_lines: 'ファイルページ上限', - max_line_length: '行長上限' + fileReadMaxChars: 'ファイル読み取り上限', + toolOutput: { + maxBytes: 'ターミナル出力上限', + maxLines: 'ファイルページ上限', + maxLineLength: '行長上限' }, - code_execution: { + codeExecution: { mode: 'コード実行モード' }, approvals: { mode: '承認モード', timeout: '承認タイムアウト', - mcp_reload_confirm: 'MCP 再読み込みの確認' + mcpReloadConfirm: 'MCP 再読み込みの確認' }, - command_allowlist: 'コマンド許可リスト', + commandAllowlist: 'コマンド許可リスト', security: { - redact_secrets: 'シークレットを伏せる', - allow_private_urls: 'プライベート URL を許可' + redactSecrets: 'シークレットを伏せる', + allowPrivateUrls: 'プライベート URL を許可' }, browser: { - allow_private_urls: 'ブラウザーのプライベート URL', - auto_local_for_private_urls: 'プライベート URL にはローカルブラウザーを使用' + allowPrivateUrls: 'ブラウザーのプライベート URL', + autoLocalForPrivateUrls: 'プライベート URL にはローカルブラウザーを使用' }, checkpoints: { enabled: 'ファイルチェックポイント', - max_snapshots: 'チェックポイント上限' + maxSnapshots: 'チェックポイント上限' }, voice: { - record_key: '音声ショートカット', - max_recording_seconds: '最大録音時間', - auto_tts: '応答を読み上げる' + recordKey: '音声ショートカット', + maxRecordingSeconds: '最大録音時間', + autoTts: '応答を読み上げる' }, stt: { enabled: '音声認識', @@ -281,9 +281,9 @@ export const ja = defineLocale({ language: '文字起こし言語' }, elevenlabs: { - model_id: 'ElevenLabs STT モデル', - language_code: 'ElevenLabs 言語', - tag_audio_events: '音声イベントをタグ付け', + modelId: 'ElevenLabs STT モデル', + languageCode: 'ElevenLabs 言語', + tagAudioEvents: '音声イベントをタグ付け', diarize: '話者分離' } }, @@ -297,15 +297,15 @@ export const ja = defineLocale({ voice: 'OpenAI 音声' }, elevenlabs: { - voice_id: 'ElevenLabs 音声', - model_id: 'ElevenLabs モデル' + voiceId: 'ElevenLabs 音声', + modelId: 'ElevenLabs モデル' } }, memory: { - memory_enabled: '永続メモリ', - user_profile_enabled: 'ユーザープロファイル', - memory_char_limit: 'メモリ予算', - user_char_limit: 'プロファイル予算', + memoryEnabled: '永続メモリ', + userProfileEnabled: 'ユーザープロファイル', + memoryCharLimit: 'メモリ予算', + userCharLimit: 'プロファイル予算', provider: 'メモリプロバイダー' }, context: { @@ -314,56 +314,56 @@ export const ja = defineLocale({ compression: { enabled: '自動圧縮', threshold: '圧縮しきい値', - target_ratio: '圧縮目標', - protect_last_n: '保護する直近メッセージ' + targetRatio: '圧縮目標', + protectLastN: '保護する直近メッセージ' }, delegation: { model: 'サブエージェントモデル', provider: 'サブエージェントプロバイダー', - max_iterations: 'サブエージェントターン上限', - max_concurrent_children: '並列サブエージェント', - child_timeout_seconds: 'サブエージェントタイムアウト', - reasoning_effort: 'サブエージェント推論強度' + maxIterations: 'サブエージェントターン上限', + maxConcurrentChildren: '並列サブエージェント', + childTimeoutSeconds: 'サブエージェントタイムアウト', + reasoningEffort: 'サブエージェント推論強度' }, updates: { - non_interactive_local_changes: 'アプリ内更新時のローカル変更' + nonInteractiveLocalChanges: 'アプリ内更新時のローカル変更' } }), fieldDescriptions: defineFieldCopy({ model: 'コンポーザーで別のモデルを選ばない限り、新しいチャットで使用されます。', - model_context_length: '0 のままにすると、選択したモデルから検出されたコンテキストウィンドウを使用します。', - fallback_providers: 'デフォルトモデルが失敗したときに試す provider:model 形式のバックアップです。', + modelContextLength: '0 のままにすると、選択したモデルから検出されたコンテキストウィンドウを使用します。', + fallbackProviders: 'デフォルトモデルが失敗したときに試す provider:model 形式のバックアップです。', display: { personality: '新しいセッションのデフォルトのアシスタントスタイルです。', - show_reasoning: 'バックエンドが推論内容を提供したときに表示します。' + showReasoning: 'バックエンドが推論内容を提供したときに表示します。' }, timezone: 'Hermes がローカル時刻のコンテキストを必要とするときに使用します。空欄ならシステムのタイムゾーンを使います。', agent: { - image_input_mode: '画像添付をモデルへ送る方法を制御します。', - max_turns: 'Hermes が 1 回の実行を停止するまでのツール呼び出しターン上限です。' + imageInputMode: '画像添付をモデルへ送る方法を制御します。', + maxTurns: 'Hermes が 1 回の実行を停止するまでのツール呼び出しターン上限です。' }, terminal: { cwd: 'ツールとターミナル作業のデフォルトプロジェクトフォルダーです。', - persistent_shell: 'バックエンドが対応している場合、コマンド間でシェル状態を保持します。', - env_passthrough: 'ツール実行へ渡す環境変数です。' + persistentShell: 'バックエンドが対応している場合、コマンド間でシェル状態を保持します。', + envPassthrough: 'ツール実行へ渡す環境変数です。' }, - code_execution: { + codeExecution: { mode: 'コード実行を現在のプロジェクトにどれだけ厳密に制限するかを設定します。' }, - file_read_max_chars: 'Hermes が 1 回のファイル読み取りで取得できる最大文字数です。', + fileReadMaxChars: 'Hermes が 1 回のファイル読み取りで取得できる最大文字数です。', approvals: { mode: '明示的な承認が必要なコマンドを Hermes がどう扱うかを設定します。', timeout: '承認プロンプトがタイムアウトするまで待つ時間です。' }, security: { - redact_secrets: '検出したシークレットを、可能な限りモデルから見える内容から隠します。' + redactSecrets: '検出したシークレットを、可能な限りモデルから見える内容から隠します。' }, checkpoints: { enabled: 'ファイル編集前にロールバック用スナップショットを作成します。' }, memory: { - memory_enabled: '将来のセッションに役立つ永続メモリを保存します。', - user_profile_enabled: 'ユーザーの好みをまとめた簡潔なプロファイルを維持します。' + memoryEnabled: '将来のセッションに役立つ永続メモリを保存します。', + userProfileEnabled: 'ユーザーの好みをまとめた簡潔なプロファイルを維持します。' }, context: { engine: '長い会話がコンテキスト上限に近づいたときの管理戦略です。' @@ -372,16 +372,16 @@ export const ja = defineLocale({ enabled: '会話が大きくなったとき、古いコンテキストを要約します。' }, voice: { - auto_tts: 'アシスタントの応答を自動で読み上げます。' + autoTts: 'アシスタントの応答を自動で読み上げます。' }, stt: { enabled: 'ローカルまたはプロバイダーによる音声文字起こしを有効にします。', elevenlabs: { - language_code: '任意の ISO-639-3 言語コードです。空欄なら ElevenLabs が自動検出します。' + languageCode: '任意の ISO-639-3 言語コードです。空欄なら ElevenLabs が自動検出します。' } }, updates: { - non_interactive_local_changes: + nonInteractiveLocalChanges: 'アプリから Hermes 自身を更新するとき、ローカルのソース変更を保持するか破棄するかを選びます。ターミナル更新では常に確認されます。' } }), diff --git a/apps/desktop/src/i18n/runtime.test.ts b/apps/desktop/src/i18n/runtime.test.ts index 0c2bc4870b3..499fc1de6c9 100644 --- a/apps/desktop/src/i18n/runtime.test.ts +++ b/apps/desktop/src/i18n/runtime.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { fieldCopyForSchemaKey } from '@/app/settings/field-copy' + +import { TRANSLATIONS } from './catalog' import { setRuntimeI18nLocale, translateNow } from './runtime' import { zh } from './zh' @@ -43,17 +46,25 @@ describe('desktop i18n runtime translator', () => { expect(translateNow('settings.nav.providerApiKeys')).toBe('API 金鑰') }) - it('keeps translated settings field copy addressable by schema keys', () => { - const field = ['display', 'personality'].join('.') + it('keeps translated settings field copy addressable from schema keys', () => { + const field = ['display', 'show_reasoning'].join('.') - expect(zh.settings.fieldLabels[field]).toBe('人格') - expect(zh.settings.fieldDescriptions[field]).toBe('新会话的默认助手风格。') + expect(fieldCopyForSchemaKey(zh.settings.fieldLabels, field)).toBe('推理过程块') + expect(fieldCopyForSchemaKey(zh.settings.fieldDescriptions, field)).toBe('当后端提供推理内容时予以显示。') }) - it('falls back to English for untranslated desktop-only keys in partial locales', () => { - setRuntimeI18nLocale('ja') + it('falls back to English when the active locale cannot resolve a key', () => { + const boot = TRANSLATIONS.ja.boot as { ready?: string } + const originalReady = boot.ready - expect(translateNow('boot.ready')).toBe('Hermes Desktop is ready') + try { + boot.ready = undefined + setRuntimeI18nLocale('ja') + + expect(translateNow('boot.ready')).toBe('Hermes Desktop is ready') + } finally { + boot.ready = originalReady + } }) it('returns the key when no locale can resolve a path', () => { diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index dac65ca5d3a..09ce699ea09 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -213,59 +213,59 @@ export const zhHant = defineLocale({ }, fieldLabels: defineFieldCopy({ model: '預設模型', - model_context_length: '上下文視窗', - fallback_providers: '備用模型', + modelContextLength: '上下文視窗', + fallbackProviders: '備用模型', toolsets: '已啟用工具集', timezone: '時區', display: { personality: '人格', - show_reasoning: '推理區塊' + showReasoning: '推理區塊' }, agent: { - max_turns: '最大代理步數', - image_input_mode: '圖片附件', - api_max_retries: 'API 重試次數', - service_tier: '服務層級', - tool_use_enforcement: '工具使用強制' + maxTurns: '最大代理步數', + imageInputMode: '圖片附件', + apiMaxRetries: 'API 重試次數', + serviceTier: '服務層級', + toolUseEnforcement: '工具使用強制' }, terminal: { cwd: '工作目錄', backend: '執行後端', timeout: '指令逾時', - persistent_shell: '持久化 Shell', - env_passthrough: '環境變數傳遞' + persistentShell: '持久化 Shell', + envPassthrough: '環境變數傳遞' }, - file_read_max_chars: '檔案讀取上限', - tool_output: { - max_bytes: '終端機輸出上限', - max_lines: '檔案頁面上限', - max_line_length: '行長上限' + fileReadMaxChars: '檔案讀取上限', + toolOutput: { + maxBytes: '終端機輸出上限', + maxLines: '檔案頁面上限', + maxLineLength: '行長上限' }, - code_execution: { + codeExecution: { mode: '程式碼執行模式' }, approvals: { mode: '批准模式', timeout: '批准逾時', - mcp_reload_confirm: '確認 MCP 重新載入' + mcpReloadConfirm: '確認 MCP 重新載入' }, - command_allowlist: '指令允許清單', + commandAllowlist: '指令允許清單', security: { - redact_secrets: '遮蔽密鑰', - allow_private_urls: '允許私有 URL' + redactSecrets: '遮蔽密鑰', + allowPrivateUrls: '允許私有 URL' }, browser: { - allow_private_urls: '瀏覽器私有 URL', - auto_local_for_private_urls: '私有 URL 使用本機瀏覽器' + allowPrivateUrls: '瀏覽器私有 URL', + autoLocalForPrivateUrls: '私有 URL 使用本機瀏覽器' }, checkpoints: { enabled: '檔案檢查點', - max_snapshots: '檢查點上限' + maxSnapshots: '檢查點上限' }, voice: { - record_key: '語音快捷鍵', - max_recording_seconds: '最長錄音時間', - auto_tts: '朗讀回覆' + recordKey: '語音快捷鍵', + maxRecordingSeconds: '最長錄音時間', + autoTts: '朗讀回覆' }, stt: { enabled: '語音轉文字', @@ -275,9 +275,9 @@ export const zhHant = defineLocale({ language: '轉寫語言' }, elevenlabs: { - model_id: 'ElevenLabs STT 模型', - language_code: 'ElevenLabs 語言', - tag_audio_events: '標記音訊事件', + modelId: 'ElevenLabs STT 模型', + languageCode: 'ElevenLabs 語言', + tagAudioEvents: '標記音訊事件', diarize: '說話者分離' } }, @@ -291,15 +291,15 @@ export const zhHant = defineLocale({ voice: 'OpenAI 語音' }, elevenlabs: { - voice_id: 'ElevenLabs 語音', - model_id: 'ElevenLabs 模型' + voiceId: 'ElevenLabs 語音', + modelId: 'ElevenLabs 模型' } }, memory: { - memory_enabled: '持久記憶', - user_profile_enabled: '使用者設定檔', - memory_char_limit: '記憶預算', - user_char_limit: '設定檔預算', + memoryEnabled: '持久記憶', + userProfileEnabled: '使用者設定檔', + memoryCharLimit: '記憶預算', + userCharLimit: '設定檔預算', provider: '記憶提供方' }, context: { @@ -308,56 +308,56 @@ export const zhHant = defineLocale({ compression: { enabled: '自動壓縮', threshold: '壓縮閾值', - target_ratio: '壓縮目標', - protect_last_n: '保護最近訊息' + targetRatio: '壓縮目標', + protectLastN: '保護最近訊息' }, delegation: { model: '子代理模型', provider: '子代理提供方', - max_iterations: '子代理輪次上限', - max_concurrent_children: '平行子代理', - child_timeout_seconds: '子代理逾時', - reasoning_effort: '子代理推理強度' + maxIterations: '子代理輪次上限', + maxConcurrentChildren: '平行子代理', + childTimeoutSeconds: '子代理逾時', + reasoningEffort: '子代理推理強度' }, updates: { - non_interactive_local_changes: '應用程式內更新的本機變更' + nonInteractiveLocalChanges: '應用程式內更新的本機變更' } }), fieldDescriptions: defineFieldCopy({ model: '除非你在輸入框選擇其他模型,否則新聊天會使用此模型。', - model_context_length: '保留 0 會使用所選模型偵測到的上下文視窗。', - fallback_providers: '預設模型失敗時要嘗試的備用 provider:model 項目。', + modelContextLength: '保留 0 會使用所選模型偵測到的上下文視窗。', + fallbackProviders: '預設模型失敗時要嘗試的備用 provider:model 項目。', display: { personality: '新工作階段的預設助手風格。', - show_reasoning: '後端提供推理內容時顯示該區塊。' + showReasoning: '後端提供推理內容時顯示該區塊。' }, timezone: 'Hermes 需要本機時間上下文時使用。留空則使用系統時區。', agent: { - image_input_mode: '控制圖片附件如何傳送給模型。', - max_turns: 'Hermes 停止一次執行前的工具呼叫輪次上限。' + imageInputMode: '控制圖片附件如何傳送給模型。', + maxTurns: 'Hermes 停止一次執行前的工具呼叫輪次上限。' }, terminal: { cwd: '工具與終端機操作的預設專案資料夾。', - persistent_shell: '後端支援時,在指令之間保留 Shell 狀態。', - env_passthrough: '傳入工具執行的環境變數。' + persistentShell: '後端支援時,在指令之間保留 Shell 狀態。', + envPassthrough: '傳入工具執行的環境變數。' }, - code_execution: { + codeExecution: { mode: '程式碼執行被限制在目前專案中的嚴格程度。' }, - file_read_max_chars: 'Hermes 單次檔案讀取可讀取的最大字元數。', + fileReadMaxChars: 'Hermes 單次檔案讀取可讀取的最大字元數。', approvals: { mode: 'Hermes 如何處理需要明確批准的指令。', timeout: '批准提示逾時前等待的時間。' }, security: { - redact_secrets: '盡可能從模型可見內容中隱藏偵測到的密鑰。' + redactSecrets: '盡可能從模型可見內容中隱藏偵測到的密鑰。' }, checkpoints: { enabled: '在檔案編輯前建立可回復的快照。' }, memory: { - memory_enabled: '儲存有助於未來工作階段的持久記憶。', - user_profile_enabled: '維護一份精簡的使用者偏好設定檔。' + memoryEnabled: '儲存有助於未來工作階段的持久記憶。', + userProfileEnabled: '維護一份精簡的使用者偏好設定檔。' }, context: { engine: '長對話接近上下文上限時的管理策略。' @@ -366,16 +366,16 @@ export const zhHant = defineLocale({ enabled: '對話變大時摘要較早的上下文。' }, voice: { - auto_tts: '自動朗讀助手回覆。' + autoTts: '自動朗讀助手回覆。' }, stt: { enabled: '啟用本機或提供方支援的語音轉寫。', elevenlabs: { - language_code: '可選的 ISO-639-3 語言代碼。留空讓 ElevenLabs 自動偵測。' + languageCode: '可選的 ISO-639-3 語言代碼。留空讓 ElevenLabs 自動偵測。' } }, updates: { - non_interactive_local_changes: + nonInteractiveLocalChanges: 'Hermes 從應用程式內更新自身時,保留本機原始碼變更(stash)或丟棄(discard)。終端機更新一律會詢問。' } }), diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 42f01a2d366..78b3c2fea25 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -213,59 +213,59 @@ export const zh: Translations = { }, fieldLabels: defineFieldCopy({ model: '默认模型', - model_context_length: '上下文窗口', - fallback_providers: '备用模型', + modelContextLength: '上下文窗口', + fallbackProviders: '备用模型', toolsets: '启用的工具集', timezone: '时区', display: { personality: '人格', - show_reasoning: '推理过程块' + showReasoning: '推理过程块' }, agent: { - max_turns: '最大智能体步数', - image_input_mode: '图片附件', - api_max_retries: 'API 重试次数', - service_tier: '服务等级', - tool_use_enforcement: '工具调用强制' + maxTurns: '最大智能体步数', + imageInputMode: '图片附件', + apiMaxRetries: 'API 重试次数', + serviceTier: '服务等级', + toolUseEnforcement: '工具调用强制' }, terminal: { cwd: '工作目录', backend: '执行后端', timeout: '命令超时', - persistent_shell: '持久化 Shell', - env_passthrough: '环境变量透传' + persistentShell: '持久化 Shell', + envPassthrough: '环境变量透传' }, - file_read_max_chars: '文件读取上限', - tool_output: { - max_bytes: '终端输出上限', - max_lines: '文件分页上限', - max_line_length: '行长度上限' + fileReadMaxChars: '文件读取上限', + toolOutput: { + maxBytes: '终端输出上限', + maxLines: '文件分页上限', + maxLineLength: '行长度上限' }, - code_execution: { + codeExecution: { mode: '代码执行模式' }, approvals: { mode: '审批模式', timeout: '审批超时', - mcp_reload_confirm: '确认 MCP 重载' + mcpReloadConfirm: '确认 MCP 重载' }, - command_allowlist: '命令白名单', + commandAllowlist: '命令白名单', security: { - redact_secrets: '隐去密钥', - allow_private_urls: '允许私有 URL' + redactSecrets: '隐去密钥', + allowPrivateUrls: '允许私有 URL' }, browser: { - allow_private_urls: '浏览器私有 URL', - auto_local_for_private_urls: '私有 URL 使用本地浏览器' + allowPrivateUrls: '浏览器私有 URL', + autoLocalForPrivateUrls: '私有 URL 使用本地浏览器' }, checkpoints: { enabled: '文件检查点', - max_snapshots: '检查点上限' + maxSnapshots: '检查点上限' }, voice: { - record_key: '语音快捷键', - max_recording_seconds: '最长录音时长', - auto_tts: '朗读回复' + recordKey: '语音快捷键', + maxRecordingSeconds: '最长录音时长', + autoTts: '朗读回复' }, stt: { enabled: '语音转文字', @@ -275,9 +275,9 @@ export const zh: Translations = { language: '转写语言' }, elevenlabs: { - model_id: 'ElevenLabs STT 模型', - language_code: 'ElevenLabs 语言', - tag_audio_events: '标记音频事件', + modelId: 'ElevenLabs STT 模型', + languageCode: 'ElevenLabs 语言', + tagAudioEvents: '标记音频事件', diarize: '说话人区分' } }, @@ -291,15 +291,15 @@ export const zh: Translations = { voice: 'OpenAI 语音' }, elevenlabs: { - voice_id: 'ElevenLabs 语音', - model_id: 'ElevenLabs 模型' + voiceId: 'ElevenLabs 语音', + modelId: 'ElevenLabs 模型' } }, memory: { - memory_enabled: '持久记忆', - user_profile_enabled: '用户画像', - memory_char_limit: '记忆预算', - user_char_limit: '画像预算', + memoryEnabled: '持久记忆', + userProfileEnabled: '用户画像', + memoryCharLimit: '记忆预算', + userCharLimit: '画像预算', provider: '记忆提供方' }, context: { @@ -308,56 +308,56 @@ export const zh: Translations = { compression: { enabled: '自动压缩', threshold: '压缩阈值', - target_ratio: '压缩目标', - protect_last_n: '保护最近消息' + targetRatio: '压缩目标', + protectLastN: '保护最近消息' }, delegation: { model: '子智能体模型', provider: '子智能体提供方', - max_iterations: '子智能体轮次上限', - max_concurrent_children: '并行子智能体', - child_timeout_seconds: '子智能体超时', - reasoning_effort: '子智能体推理强度' + maxIterations: '子智能体轮次上限', + maxConcurrentChildren: '并行子智能体', + childTimeoutSeconds: '子智能体超时', + reasoningEffort: '子智能体推理强度' }, updates: { - non_interactive_local_changes: '应用内更新本地更改' + nonInteractiveLocalChanges: '应用内更新本地更改' } }), fieldDescriptions: defineFieldCopy({ model: '用于新对话,除非你在输入框中选择其他模型。', - model_context_length: '保持为 0 则使用所选模型检测到的上下文窗口。', - fallback_providers: '默认模型失败时尝试的备用 provider:model 条目。', + modelContextLength: '保持为 0 则使用所选模型检测到的上下文窗口。', + fallbackProviders: '默认模型失败时尝试的备用 provider:model 条目。', display: { personality: '新会话的默认助手风格。', - show_reasoning: '当后端提供推理内容时予以显示。' + showReasoning: '当后端提供推理内容时予以显示。' }, timezone: '当 Hermes 需要本地时间上下文时使用。留空则使用系统时区。', agent: { - image_input_mode: '控制图片附件如何发送给模型。', - max_turns: 'Hermes 停止一次运行前工具调用轮次的上限。' + imageInputMode: '控制图片附件如何发送给模型。', + maxTurns: 'Hermes 停止一次运行前工具调用轮次的上限。' }, terminal: { cwd: '工具与终端操作的默认项目目录。', - persistent_shell: '当后端支持时,在命令之间保留 Shell 状态。', - env_passthrough: '传入工具执行的环境变量。' + persistentShell: '当后端支持时,在命令之间保留 Shell 状态。', + envPassthrough: '传入工具执行的环境变量。' }, - code_execution: { + codeExecution: { mode: '代码执行被限定到当前项目的严格程度。' }, - file_read_max_chars: 'Hermes 单次文件读取可读取的最大字符数。', + fileReadMaxChars: 'Hermes 单次文件读取可读取的最大字符数。', approvals: { mode: 'Hermes 如何处理需要显式审批的命令。', timeout: '审批提示在超时前等待的时长。' }, security: { - redact_secrets: '尽可能从模型可见内容中隐藏检测到的密钥。' + redactSecrets: '尽可能从模型可见内容中隐藏检测到的密钥。' }, checkpoints: { enabled: '在文件编辑前创建可回滚的快照。' }, memory: { - memory_enabled: '保存有助于未来会话的持久记忆。', - user_profile_enabled: '维护一份精简的用户偏好画像。' + memoryEnabled: '保存有助于未来会话的持久记忆。', + userProfileEnabled: '维护一份精简的用户偏好画像。' }, context: { engine: '在接近上下文上限时管理长对话的策略。' @@ -366,16 +366,16 @@ export const zh: Translations = { enabled: '当对话变大时对较早的上下文进行摘要。' }, voice: { - auto_tts: '自动朗读助手回复。' + autoTts: '自动朗读助手回复。' }, stt: { enabled: '启用本地或提供方支持的语音转写。', elevenlabs: { - language_code: '可选的 ISO-639-3 语言代码。留空让 ElevenLabs 自动检测。' + languageCode: '可选的 ISO-639-3 语言代码。留空让 ElevenLabs 自动检测。' } }, updates: { - non_interactive_local_changes: + nonInteractiveLocalChanges: 'Hermes 从应用内更新时(无终端提示),保留本地源码修改(暂存)或丢弃(放弃)。通过终端更新时始终会询问。' } }), @@ -1338,7 +1338,7 @@ export const zh: Translations = { '某个安装步骤失败。在 Windows 上,如果另一个 Hermes CLI 或桌面实例正在运行,可能会出现这种情况。请停止正在运行的 Hermes 实例后重试。可查看下面的详情或 desktop 日志中的完整记录。', activeDesc: '这是一次性设置。Hermes 安装器正在下载依赖并配置你的机器。之后启动会跳过此步骤。', progress: (completed, total) => `${completed}/${total} 个步骤已完成`, - currentStage: stage => ` -- 当前: ${stage}`, + currentStage: stage => ` -- 当前:${stage}`, fetchingManifest: '正在获取安装器 manifest...', error: '错误', hideOutput: '隐藏安装器输出', @@ -1374,7 +1374,7 @@ export const zh: Translations = { xai: { short: 'Grok 模型', description: '直接访问 xAI Grok 模型。' }, local: { short: '自托管', - description: '将 Hermes 指向本地或自托管的 OpenAI 兼容端点(vLLM、llama.cpp、Ollama 等)。' + description: '将 Hermes 指向本地或自托管的 OpenAI 兼容端点 (vLLM、llama.cpp、Ollama 等)。' } }, backToSignIn: '返回登录', @@ -1422,11 +1422,11 @@ export const zh: Translations = { modelPicker: { title: '切换模型', - current: '当前:', + current: '当前:', unknown: '(未知)', search: '筛选提供方和模型...', noModels: '未找到模型。', - persistGlobalSession: '全局保存(否则仅当前会话)', + persistGlobalSession: '全局保存 (否则仅当前会话)', persistGlobal: '全局保存', addProvider: '添加提供方', loadFailed: '无法加载模型', @@ -1480,7 +1480,7 @@ export const zh: Translations = { checkingInference: '正在检查推理', disconnected: '已断开', openSystem: '打开系统面板', - connection: label => `连接: ${label}`, + connection: label => `连接:${label}`, recentActivity: '最近活动', viewAllLogs: '查看全部日志 →', messagingPlatforms: '消息平台' @@ -1543,7 +1543,7 @@ export const zh: Translations = { noProjectTitle: '没有项目', noProjectBody: '从状态栏设置工作目录后即可浏览文件。', unreadableTitle: '无法读取', - unreadableBody: error => `无法读取此文件夹(${error})。`, + unreadableBody: error => `无法读取此文件夹 (${error})。`, emptyTitle: '空文件夹', emptyBody: '此文件夹为空。', treeErrorTitle: '文件树错误', @@ -1593,7 +1593,7 @@ export const zh: Translations = { copy: '复制', clear: '清除', empty: '暂无控制台消息。', - promptHeader: '预览控制台:', + promptHeader: '预览控制台:', sentTitle: '已发送到对话', sentMessage: count => `已将 ${count} 条日志添加到输入框` }, @@ -1607,14 +1607,14 @@ export const zh: Translations = { lookingRestart: taskId => `Hermes 正在查找要重启的预览服务器 (${taskId})`, restartingTitle: '正在重启预览服务器', restartingMessage: 'Hermes 正在后台工作。可在预览控制台查看进度。', - startRestartFailed: message => `无法启动服务器重启: ${message}`, + startRestartFailed: message => `无法启动服务器重启:${message}`, restartFailed: '服务器重启失败', hideConsole: '隐藏预览控制台', showConsole: '显示预览控制台', hideDevTools: '隐藏预览 DevTools', openDevTools: '打开预览 DevTools', finishedRestarting: message => `Hermes 已完成预览服务器重启${message ? `: ${message}` : ''}`, - failedRestarting: message => `服务器重启失败: ${message}`, + failedRestarting: message => `服务器重启失败:${message}`, unknownError: '未知错误', restartedTitle: '预览服务器已重启', reloadingNow: '正在重新加载预览。', @@ -1622,9 +1622,9 @@ export const zh: Translations = { restartFailedMessage: 'Hermes 无法重启服务器。', stillWorking: 'Hermes 仍在工作,但还没有收到重启结果。服务器命令可能正在前台运行。', workspaceReloading: '工作区已变更,正在重新加载预览', - fileChanged: url => `文件已变更,正在重新加载预览: ${url}`, - filesChanged: (count, url) => `${count} 个文件变更,正在重新加载预览: ${url}`, - watchFailed: message => `无法监听预览文件: ${message}`, + fileChanged: url => `文件已变更,正在重新加载预览:${url}`, + filesChanged: (count, url) => `${count} 个文件变更,正在重新加载预览:${url}`, + watchFailed: message => `无法监听预览文件:${message}`, moduleMimeDescription: '模块脚本使用了错误的 MIME 类型。这通常表示静态文件服务器正在服务 Vite/React 应用,而不是项目开发服务器。', loadFailedConsole: (code, message) => `加载失败${code ? ` (${code})` : ''}: ${message}`, @@ -1668,7 +1668,7 @@ export const zh: Translations = { reject: '拒绝', alwaysTitle: '始终允许此命令?', alwaysDescription: pattern => - `这会将 “${pattern}” 模式加入永久允许列表(~/.hermes/config.yaml)。Hermes 对类似命令将不再询问,包括当前会话和未来会话。`, + `这会将“${pattern}”模式加入永久允许列表 (~/.hermes/config.yaml)。Hermes 对类似命令将不再询问,包括当前会话和未来会话。`, alwaysAllow: '始终允许' }, clarify: { @@ -1676,7 +1676,7 @@ export const zh: Translations = { gatewayDisconnected: 'Hermes 网关未连接', sendFailed: '无法发送澄清响应', loadingQuestion: '正在加载问题…', - other: '其他(输入你的答案)', + other: '其他 (输入你的答案)', placeholder: '输入你的答案…', shortcut: '⌘/Ctrl + Enter 发送', back: '返回', @@ -1730,7 +1730,7 @@ export const zh: Translations = { emptySlashCommand: '空 slash 命令', desktopCommands: '桌面端命令', skillCommandsAvailable: count => `${count} 个技能命令可用。`, - warningLine: message => `警告: ${message}`, + warningLine: message => `警告:${message}`, yoloArmed: '此对话已启用 YOLO', yoloOff: 'YOLO 已关闭', yoloSystem: active => `此会话 YOLO ${active ? '已开启' : '已关闭'}`,