Files
webui/static/i18n.js
nesquena-hermes 27c2fd6c08 v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)

Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming <code> instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.

Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.

Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.

Fixes #240

* fix: restore custom model list discovery with config api key (#238)

get_available_models() now reads api_key from config.yaml before env vars:
  1. model.api_key
  2. providers.<active>.api_key / providers.custom.api_key
  3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)

Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.

Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.

* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)

Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.

* security: redact credentials from API responses and fix credential file permissions (#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with #237 container detection changes in server.py.

* fix: cancel button now interrupts agent and cleans up UI state (#244)

Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.

Changes:
  - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
  - api/streaming.py: stores agent in AGENT_INSTANCES after creation,
    checks CANCEL_FLAGS immediately after store (race condition fix),
    calls agent.interrupt() in cancel_stream(), cleans up in finally block
  - static/boot.js: removes stale setStatus(cancelling) call
  - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel

Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.

New test file: tests/test_cancel_interrupt.py with 6 unit tests.

* docs: v0.46.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 10:17:52 -07:00

804 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── i18n: locale bundles and t() helper ──────────────────────────────────────
// To add a new language: add an entry to LOCALES below with all keys translated.
// The language code must match a valid BCP 47 tag (used for speech recognition).
// Keys missing in a non-English locale fall back to English automatically.
const LOCALES = {
en: {
_lang: 'en',
_label: 'English',
_speech: 'en-US',
// boot.js
cancelling: 'Cancelling\u2026',
cancel_failed: 'Cancel failed: ',
mic_denied: 'Microphone access denied. Check browser permissions.',
mic_no_speech: 'No speech detected. Try again.',
mic_network: 'Speech recognition unavailable.',
mic_error: 'Voice input error: ',
session_imported: 'Session imported',
import_failed: 'Import failed: ',
import_invalid_json: 'Invalid JSON',
image_pasted: 'Image pasted: ',
// messages.js
edit_message: 'Edit message',
regenerate: 'Regenerate response',
copy: 'Copy',
copied: 'Copied!',
you: 'You',
thinking: 'Thinking',
expand_all: 'Expand all',
collapse_all: 'Collapse all',
edit_failed: 'Edit failed: ',
regen_failed: 'Regenerate failed: ',
reconnect_active: 'A response is still being generated. Reload when ready?',
reconnect_finished: 'A response was in progress when you last left. Messages may have updated.',
// approval card
approval_heading: 'Approval required',
approval_desc_prefix: 'Dangerous command detected',
approval_btn_once: 'Allow once',
approval_btn_once_title: 'Allow this one command (Enter)',
approval_btn_session: 'Allow session',
approval_btn_session_title: 'Allow for this conversation session',
approval_btn_always: 'Always allow',
approval_btn_always_title: 'Always allow this command pattern',
approval_btn_deny: 'Deny',
approval_btn_deny_title: 'Deny — do not run this command',
approval_responding: 'Responding\u2026',
untitled: 'Untitled',
n_messages: (n) => `${n} messages`,
model_unavailable: ' (unavailable)',
model_unavailable_title: 'This model is no longer in your current provider list',
// commands.js
cmd_help: 'List available commands',
cmd_clear: 'Clear conversation messages',
cmd_compact: 'Compress conversation context',
cmd_model: 'Switch model (e.g. /model gpt-4o)',
cmd_workspace: 'Switch workspace by name',
cmd_new: 'Start a new chat session',
cmd_usage: 'Toggle token usage display on/off',
cmd_theme: 'Switch theme (dark/light/slate/solarized/monokai/nord/oled)',
cmd_personality: 'Switch agent personality',
available_commands: 'Available commands:',
type_slash: 'Type / to see commands',
conversation_cleared: 'Conversation cleared',
model_usage: 'Usage: /model <name>',
no_model_match: 'No model matching "',
switched_to: 'Switched to ',
workspace_usage: 'Usage: /workspace <name>',
no_workspace_match: 'No workspace matching "',
switched_workspace: 'Switched to workspace: ',
workspace_switch_failed: 'Workspace switch failed: ',
new_session: 'New session created',
compressing: 'Requesting context compression...',
token_usage_on: 'Token usage on',
token_usage_off: 'Token usage off',
theme_usage: 'Usage: /theme ',
theme_set: 'Theme: ',
no_active_session: 'No active session',
no_personalities: 'No personalities found (add them to ~/.hermes/personalities/)',
available_personalities: 'Available personalities:',
personality_switch_hint: '\n\nUse `/personality <name>` to switch, or `/personality none` to clear.',
personalities_load_failed: 'Failed to load personalities',
personality_cleared: 'Personality cleared',
personality_set: 'Personality: ',
failed_colon: 'Failed: ',
// ui.js
no_workspace: 'No workspace',
// workspace.js
unsaved_confirm: 'You have unsaved changes in the preview. Discard and navigate?',
save: 'Save',
edit: 'Edit',
save_title: 'Save changes',
edit_title: 'Edit this file',
saved: 'Saved',
save_failed: 'Save failed: ',
image_load_failed: 'Could not load image',
file_open_failed: 'Could not open file',
downloading: (name) => `Downloading ${name}\u2026`,
double_click_rename: 'Double-click to rename',
renamed_to: 'Renamed to ',
rename_failed: 'Rename failed: ',
delete_title: 'Delete',
delete_confirm: (name) => `Delete ${name}?`,
deleted: 'Deleted ',
delete_failed: 'Delete failed: ',
new_file_prompt: 'New file name (e.g. notes.md):',
created: 'Created ',
create_failed: 'Create failed: ',
new_folder_prompt: 'New folder name:',
folder_created: 'Created folder ',
folder_create_failed: 'Create folder failed: ',
remove_title: 'Remove',
empty_dir: '(empty)',
upload_failed: 'Upload failed: ',
all_uploads_failed: (n) => `All ${n} upload(s) failed`,
// settings panel
settings_title: 'Settings',
settings_save_btn: 'Save Settings',
settings_label_model: 'Default Model',
settings_label_send_key: 'Send Key',
settings_label_theme: 'Theme',
settings_label_language: 'Language',
settings_label_token_usage: 'Show token usage',
settings_label_cli_sessions: 'Show CLI sessions',
settings_label_sync_insights: 'Sync to insights',
settings_label_check_updates: 'Check for updates',
settings_label_bot_name: 'Assistant Name',
settings_label_password: 'Access Password',
settings_saved: 'Settings saved',
settings_save_failed: 'Save failed: ',
settings_load_failed: 'Failed to load settings: ',
settings_saved_pw: 'Settings saved (password set \u2014 login now required)',
// login page (used server-side via /api/i18n/login endpoint)
login_title: 'Sign in',
login_subtitle: 'Enter your password to continue',
login_placeholder: 'Password',
login_btn: 'Sign in',
login_invalid_pw: 'Invalid password',
login_conn_failed: 'Connection failed',
// Sidebar & Tabs
tab_chat: 'Chat',
tab_tasks: 'Tasks',
tab_skills: 'Skills',
tab_memory: 'Memory',
tab_workspaces: 'Spaces',
tab_profiles: 'Profiles',
tab_todos: 'Todos',
new_conversation: 'New conversation',
filter_conversations: 'Filter conversations...',
scheduled_jobs: 'Scheduled jobs',
new_job: 'New job',
loading: 'Loading...',
search_skills: 'Search skills...',
new_skill: 'New skill',
personal_memory: 'Personal memory',
current_task_list: 'Current task list',
workspace_desc: 'Add and switch workspaces for your sessions.',
new_profile: 'New profile',
transcript: 'Transcript',
download_transcript: 'Download as Markdown',
import: 'Import',
// Settings detail
settings_label_sound: 'Notification sound',
settings_desc_sound: 'Play a sound when the assistant finishes a response.',
settings_label_notifications: 'Browser notifications',
settings_desc_notifications: 'Show a system notification when a response completes while the tab is in the background.',
settings_desc_token_usage: 'Displays input/output token count below each assistant reply. Also toggled with /usage.',
settings_desc_cli_sessions: 'Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.',
settings_desc_sync_insights: 'Mirrors WebUI token usage to state.db so hermes /insights includes browser session data. Off by default.',
settings_desc_check_updates: 'Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.',
settings_desc_bot_name: 'Display name for the assistant throughout the UI. Defaults to Hermes.',
settings_desc_password: 'Enter a new password to set or change it. Leave blank to keep current setting.',
password_placeholder: 'Enter new password…',
disable_auth: 'Disable Auth',
sign_out: 'Sign Out',
cancel: 'Cancel',
create_job: 'Create job',
save_skill: 'Save skill',
editing: 'Editing',
// Empty state
empty_title: 'What can I help with?',
empty_subtitle: 'Ask anything, run commands, explore files, or manage your scheduled tasks.',
suggest_files: 'What files are in this workspace?',
suggest_schedule: "What's on my schedule today?",
suggest_plan: 'Help me plan a small project.',
},
de: {
_lang: 'de',
_label: 'Deutsch',
_speech: 'de-DE',
// boot.js
cancelling: 'Wird abgebrochen\u2026',
cancel_failed: 'Abbrechen fehlgeschlagen: ',
mic_denied: 'Mikrofonzugriff verweigert. Überprüfen Sie die Browserberechtigungen.',
mic_no_speech: 'Keine Sprache erkannt. Versuchen Sie es erneut.',
mic_network: 'Spracherkennung nicht verfügbar.',
mic_error: 'Spracheingabefehler: ',
session_imported: 'Sitzung importiert',
import_failed: 'Import fehlgeschlagen: ',
import_invalid_json: 'Ungültiges JSON',
image_pasted: 'Bild eingefügt: ',
// messages.js
edit_message: 'Nachricht bearbeiten',
regenerate: 'Antwort regenerieren',
copy: 'Kopieren',
copied: 'Kopiert!',
you: 'Du',
thinking: 'Nachdenken',
expand_all: 'Alle ausklappen',
collapse_all: 'Alle einklappen',
edit_failed: 'Bearbeiten fehlgeschlagen: ',
regen_failed: 'Regeneration fehlgeschlagen: ',
reconnect_active: 'Eine Antwort wird noch generiert. Neu laden, wenn bereit?',
reconnect_finished: 'Eine Antwort war in Arbeit, als Sie zuletzt gegangen sind. Nachrichten könnten aktualisiert worden sein.',
// approval card
approval_heading: 'Genehmigung erforderlich',
approval_desc_prefix: 'Gefährlicher Befehl erkannt',
approval_btn_once: 'Einmal zulassen',
approval_btn_once_title: 'Diesen einen Befehl zulassen (Enter)',
approval_btn_session: 'Sitzung zulassen',
approval_btn_session_title: 'Für diese Konversationssitzung zulassen',
approval_btn_always: 'Immer zulassen',
approval_btn_always_title: 'Dieses Befehlsmuster immer zulassen',
approval_btn_deny: 'Ablehnen',
approval_btn_deny_title: 'Ablehnen \u2014 diesen Befehl nicht ausführen',
approval_responding: 'Antwortet\u2026',
untitled: 'Unbenannt',
n_messages: (n) => `${n} Nachrichten`,
model_unavailable: ' (nicht verfügbar)',
model_unavailable_title: 'Dieses Modell ist nicht mehr in Ihrer aktuellen Provider-Liste',
// commands.js
cmd_help: 'Verfügbare Befehle auflisten',
cmd_clear: 'Konversationsverlauf löschen',
cmd_compact: 'Kontext komprimieren',
cmd_model: 'Modell wechseln (z.B. /model gpt-4o)',
cmd_workspace: 'Workspace nach Namen wechseln',
cmd_new: 'Neue Chat-Sitzung starten',
cmd_usage: 'Token-Verbrauchsanzeige umschalten',
cmd_theme: 'Theme wechseln (dark/light/slate/solarized/monokai/nord/oled)',
cmd_personality: 'Agenten-Persönlichkeit wechseln',
available_commands: 'Verfügbare Befehle:',
type_slash: 'Tippe / für Befehle',
conversation_cleared: 'Konversation gelöscht',
model_usage: 'Nutzung: /model <name>',
no_model_match: 'Kein Modell gefunden für "',
switched_to: 'Gewechselt zu ',
workspace_usage: 'Nutzung: /workspace <name>',
no_workspace_match: 'Kein Workspace gefunden für "',
switched_workspace: 'Gewechselt zu Workspace: ',
workspace_switch_failed: 'Workspace-Wechsel fehlgeschlagen: ',
new_session: 'Neue Sitzung erstellt',
compressing: 'Kontext-Komprimierung wird angefordert...',
token_usage_on: 'Token-Verbrauch an',
token_usage_off: 'Token-Verbrauch aus',
theme_usage: 'Nutzung: /theme ',
theme_set: 'Theme: ',
no_active_session: 'Keine aktive Sitzung',
no_personalities: 'Keine Persönlichkeiten gefunden (füge sie in ~/.hermes/personalities/ hinzu)',
available_personalities: 'Verfügbare Persönlichkeiten:',
personality_switch_hint: '\n\nNutze `/personality <name>` zum Wechseln, oder `/personality none` zum Löschen.',
personalities_load_failed: 'Fehler beim Laden der Persönlichkeiten',
personality_cleared: 'Persönlichkeit gelöscht',
personality_set: 'Persönlichkeit: ',
failed_colon: 'Fehlgeschlagen: ',
// ui.js
no_workspace: 'Kein Workspace',
// workspace.js
unsaved_confirm: 'Sie haben ungespeicherte Änderungen in der Vorschau. Verwerfen und fortfahren?',
save: 'Speichern',
edit: 'Bearbeiten',
save_title: 'Änderungen speichern',
edit_title: 'Diese Datei bearbeiten',
saved: 'Gespeichert',
save_failed: 'Speichern fehlgeschlagen: ',
image_load_failed: 'Bild konnte nicht geladen werden',
file_open_failed: 'Datei konnte nicht geöffnet werden',
downloading: (name) => `Lade ${name} herunter\u2026`,
double_click_rename: 'Doppelklick zum Umbenennen',
renamed_to: 'Umbenannt in ',
rename_failed: 'Umbenennen fehlgeschlagen: ',
delete_title: 'Löschen',
delete_confirm: (name) => `${name} löschen?`,
deleted: 'Gelöscht ',
delete_failed: 'Löschen fehlgeschlagen: ',
new_file_prompt: 'Neuer Dateiname (z.B. notes.md):',
created: 'Erstellt ',
create_failed: 'Erstellen fehlgeschlagen: ',
new_folder_prompt: 'Neuer Ordnername:',
folder_created: 'Ordner erstellt ',
folder_create_failed: 'Ordner erstellen fehlgeschlagen: ',
remove_title: 'Entfernen',
empty_dir: '(leer)',
upload_failed: 'Upload fehlgeschlagen: ',
all_uploads_failed: (n) => `Alle ${n} Upload(s) fehlgeschlagen`,
// settings panel
settings_title: 'Einstellungen',
settings_save_btn: 'Einstellungen speichern',
settings_label_model: 'Standard-Modell',
settings_label_send_key: 'Sende-Taste',
settings_label_theme: 'Theme',
settings_label_language: 'Sprache',
settings_label_token_usage: 'Token-Verbrauch anzeigen',
settings_label_cli_sessions: 'CLI-Sitzungen anzeigen',
settings_label_sync_insights: 'Mit Insights synchronisieren',
settings_label_check_updates: 'Nach Updates suchen',
settings_label_bot_name: 'Assistenten-Name',
settings_label_password: 'Zugangspasswort',
settings_saved: 'Einstellungen gespeichert',
settings_save_failed: 'Speichern fehlgeschlagen: ',
settings_load_failed: 'Laden der Einstellungen fehlgeschlagen: ',
settings_saved_pw: 'Einstellungen gespeichert (Passwort gesetzt \u2014 Login jetzt erforderlich)',
// login page
login_title: 'Anmelden',
login_subtitle: 'Geben Sie Ihr Passwort ein, um fortzufahren',
login_placeholder: 'Passwort',
login_btn: 'Anmelden',
login_invalid_pw: 'Ungültiges Passwort',
login_conn_failed: 'Verbindung fehlgeschlagen',
// Sidebar & Tabs
tab_chat: 'Chat',
tab_tasks: 'Aufgaben',
tab_skills: 'Skills',
tab_memory: 'Gedächtnis',
tab_workspaces: 'Spaces',
tab_profiles: 'Profile',
tab_todos: 'Todos',
new_conversation: 'Neuer Chat',
filter_conversations: 'Chats filtern...',
scheduled_jobs: 'Geplante Aufgaben',
new_job: 'Neuer Job',
loading: 'Lädt...',
search_skills: 'Skills suchen...',
new_skill: 'Neuer Skill',
personal_memory: 'Persönliches Gedächtnis',
current_task_list: 'Aktuelle Aufgabenliste',
workspace_desc: 'Workspaces hinzufügen und wechseln.',
new_profile: 'Neues Profil',
transcript: 'Protokoll',
download_transcript: 'Als Markdown herunterladen',
import: 'Importieren',
// Settings detail
settings_label_sound: 'Benachrichtigungston',
settings_desc_sound: 'Spielt einen Ton ab, wenn der Assistent eine Antwort beendet.',
settings_label_notifications: 'Browser-Benachrichtigungen',
settings_desc_notifications: 'Zeigt eine Systembenachrichtigung an, wenn eine Antwort fertiggestellt wird, während der Tab im Hintergrund ist.',
settings_desc_token_usage: 'Zeigt die Anzahl der Input/Output-Token unter jeder Antwort des Assistenten an. Auch umschaltbar mit /usage.',
settings_desc_cli_sessions: 'Fügt Sitzungen aus der Hermes CLI (state.db) in die Sitzungsliste ein. Klicken Sie auf eine CLI-Sitzung, um sie zu importieren und das Gespräch fortzusetzen.',
settings_desc_sync_insights: 'Spiegelt den WebUI-Token-Verbrauch in die state.db, sodass hermes /insights Browser-Sitzungsdaten enthält. Standardmäßig aus.',
settings_desc_check_updates: 'Zeigt ein Banner an, wenn neuere Versionen der WebUI oder des Agenten verfügbar sind. Führt regelmäßig einen Git-Fetch im Hintergrund aus.',
settings_desc_bot_name: 'Anzeigename für den Assistenten in der UI. Standardmäßig Hermes.',
settings_desc_password: 'Geben Sie ein neues Passwort ein, um es zu setzen oder zu ändern. Leer lassen, um die aktuelle Einstellung beizubehalten.',
password_placeholder: 'Neues Passwort eingeben…',
disable_auth: 'Authentifizierung deaktivieren',
sign_out: 'Abmelden',
cancel: 'Abbrechen',
create_job: 'Job erstellen',
save_skill: 'Skill speichern',
editing: 'Bearbeitung',
// Empty state
empty_title: 'Wie kann ich helfen?',
empty_subtitle: 'Frage mich alles, führe Befehle aus, erkunde Dateien oder verwalte deine Aufgaben.',
suggest_files: 'Welche Dateien sind in diesem Workspace?',
suggest_schedule: 'Was steht heute auf meinem Plan?',
suggest_plan: 'Hilf mir, ein kleines Projekt zu planen.',
},
zh: {
_lang: 'zh',
_label: '\u7b80\u4f53\u4e2d\u6587',
_speech: 'zh-CN',
// boot.js
cancelling: '\u6b63\u5728\u53d6\u6d88...',
cancel_failed: '\u53d6\u6d88\u5931\u8d25\uff1a',
mic_denied: '\u9ea6\u514b\u98ce\u8bbf\u95ee\u88ab\u62d2\u7edd\uff0c\u8bf7\u68c0\u67e5\u6d4f\u89c8\u5668\u6743\u9650\u3002',
mic_no_speech: '\u6ca1\u6709\u68c0\u6d4b\u5230\u8bed\u97f3\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002',
mic_network: '\u8bed\u97f3\u8bc6\u522b\u5f53\u524d\u4e0d\u53ef\u7528\u3002',
mic_error: '\u8bed\u97f3\u8f93\u5165\u51fa\u9519\uff1a',
session_imported: '\u4f1a\u8bdd\u5df2\u5bfc\u5165',
import_failed: '\u5bfc\u5165\u5931\u8d25\uff1a',
import_invalid_json: 'JSON \u65e0\u6548',
image_pasted: '\u5df2\u7c98\u8d34\u56fe\u7247\uff1a',
// messages.js
edit_message: '\u7f16\u8f91\u6d88\u606f',
regenerate: '\u91cd\u65b0\u751f\u6210\u56de\u590d',
copy: '\u590d\u5236',
copied: '\u5df2\u590d\u5236',
you: '\u4f60',
thinking: '\u601d\u8003\u8fc7\u7a0b',
expand_all: '\u5168\u90e8\u5c55\u5f00',
collapse_all: '\u5168\u90e8\u6298\u53e0',
edit_failed: '\u7f16\u8f91\u5931\u8d25\uff1a',
regen_failed: '\u91cd\u65b0\u751f\u6210\u5931\u8d25\uff1a',
reconnect_active: '\u56de\u590d\u4ecd\u5728\u751f\u6210\u4e2d\uff0c\u51c6\u5907\u597d\u540e\u8981\u91cd\u65b0\u52a0\u8f7d\u5417\uff1f',
reconnect_finished: '\u4f60\u79bb\u5f00\u65f6\u6709\u56de\u590d\u6b63\u5728\u751f\u6210\uff0c\u6d88\u606f\u5185\u5bb9\u53ef\u80fd\u5df2\u7ecf\u66f4\u65b0\u3002',
// approval card
approval_heading: '需要审批',
approval_desc_prefix: '检测到危险命令',
approval_btn_once: '允许一次',
approval_btn_once_title: '允许执行此命令一次Enter',
approval_btn_session: '本次允许',
approval_btn_session_title: '本次会话期间允许',
approval_btn_always: '始终允许',
approval_btn_always_title: '始终允许此命令模式',
approval_btn_deny: '拒绝',
approval_btn_deny_title: '拒绝 — 不执行此命令',
approval_responding: '处理中…',
untitled: '\u672a\u547d\u540d',
n_messages: (n) => `${n} \u6761\u6d88\u606f`,
model_unavailable: '\uff08\u4e0d\u53ef\u7528\uff09',
model_unavailable_title: '\u8fd9\u4e2a\u6a21\u578b\u5df2\u7ecf\u4e0d\u5728\u5f53\u524d provider \u5217\u8868\u4e2d',
// commands.js
cmd_help: '\u67e5\u770b\u53ef\u7528\u547d\u4ee4',
cmd_clear: '\u6e05\u7a7a\u5f53\u524d\u5bf9\u8bdd\u6d88\u606f',
cmd_compact: '\u538b\u7f29\u5bf9\u8bdd\u4e0a\u4e0b\u6587',
cmd_model: '\u5207\u6362\u6a21\u578b\uff08\u4f8b\u5982 /model gpt-4o\uff09',
cmd_workspace: '\u6309\u540d\u79f0\u5207\u6362\u5de5\u4f5c\u533a',
cmd_new: '\u65b0\u5efa\u804a\u5929\u4f1a\u8bdd',
cmd_usage: '\u5207\u6362 token \u7528\u91cf\u663e\u793a',
cmd_theme: '\u5207\u6362\u4e3b\u9898\uff08dark/light/slate/solarized/monokai/nord/oled\uff09',
cmd_personality: '\u5207\u6362 Agent \u4eba\u8bbe',
available_commands: '\u53ef\u7528\u547d\u4ee4\uff1a',
type_slash: '\u8f93\u5165 / \u53ef\u67e5\u770b\u547d\u4ee4',
conversation_cleared: '\u5bf9\u8bdd\u5df2\u6e05\u7a7a',
model_usage: '\u7528\u6cd5\uff1a/model <name>',
no_model_match: '\u6ca1\u6709\u5339\u914d\u201c',
switched_to: '\u5df2\u5207\u6362\u5230 ',
workspace_usage: '\u7528\u6cd5\uff1a/workspace <name>',
no_workspace_match: '\u6ca1\u6709\u5339\u914d\u201c',
switched_workspace: '\u5df2\u5207\u6362\u5de5\u4f5c\u533a\uff1a',
workspace_switch_failed: '\u5de5\u4f5c\u533a\u5207\u6362\u5931\u8d25\uff1a',
new_session: '\u5df2\u65b0\u5efa\u4f1a\u8bdd',
compressing: '\u6b63\u5728\u8bf7\u6c42\u538b\u7f29\u4e0a\u4e0b\u6587...',
token_usage_on: 'Token \u7528\u91cf\u663e\u793a\u5df2\u5f00\u542f',
token_usage_off: 'Token \u7528\u91cf\u663e\u793a\u5df2\u5173\u95ed',
theme_usage: '\u7528\u6cd5\uff1a/theme ',
theme_set: '\u4e3b\u9898\uff1a',
no_active_session: '\u5f53\u524d\u6ca1\u6709\u6d3b\u52a8\u4f1a\u8bdd',
no_personalities: '\u6ca1\u6709\u627e\u5230\u4eba\u8bbe\uff08\u53ef\u6dfb\u52a0\u5230 ~/.hermes/personalities/\uff09',
available_personalities: '\u53ef\u7528\u4eba\u8bbe\uff1a',
personality_switch_hint: '\n\n\u4f7f\u7528 `/personality <name>` \u5207\u6362\uff0c\u6216\u7528 `/personality none` \u6e05\u7a7a\u3002',
personalities_load_failed: '\u52a0\u8f7d\u4eba\u8bbe\u5931\u8d25',
personality_cleared: '\u4eba\u8bbe\u5df2\u6e05\u7a7a',
personality_set: '\u5f53\u524d\u4eba\u8bbe\uff1a',
failed_colon: '\u5931\u8d25\uff1a',
// ui.js
no_workspace: '\u672a\u9009\u62e9\u5de5\u4f5c\u533a',
// workspace.js
unsaved_confirm: '\u9884\u89c8\u533a\u6709\u672a\u4fdd\u5b58\u4fee\u6539\uff0c\u8981\u653e\u5f03\u66f4\u6539\u5e76\u7ee7\u7eed\u8df3\u8f6c\u5417\uff1f',
save: '\u4fdd\u5b58',
edit: '\u7f16\u8f91',
save_title: '\u4fdd\u5b58\u4fee\u6539',
edit_title: '\u7f16\u8f91\u6b64\u6587\u4ef6',
saved: '\u5df2\u4fdd\u5b58',
save_failed: '\u4fdd\u5b58\u5931\u8d25\uff1a',
image_load_failed: '\u56fe\u7247\u52a0\u8f7d\u5931\u8d25',
file_open_failed: '\u65e0\u6cd5\u6253\u5f00\u6587\u4ef6',
downloading: (name) => `\u6b63\u5728\u4e0b\u8f7d ${name}...`,
double_click_rename: '\u53cc\u51fb\u91cd\u547d\u540d',
renamed_to: '\u5df2\u91cd\u547d\u540d\u4e3a ',
rename_failed: '\u91cd\u547d\u540d\u5931\u8d25\uff1a',
delete_title: '\u5220\u9664',
delete_confirm: (name) => `\u8981\u5220\u9664 ${name} \u5417\uff1f`,
deleted: '\u5df2\u5220\u9664 ',
delete_failed: '\u5220\u9664\u5931\u8d25\uff1a',
new_file_prompt: '\u65b0\u6587\u4ef6\u540d\uff08\u4f8b\u5982 notes.md\uff09\uff1a',
created: '\u5df2\u521b\u5efa ',
create_failed: '\u521b\u5efa\u5931\u8d25\uff1a',
new_folder_prompt: '\u65b0\u6587\u4ef6\u5939\u540d\u79f0\uff1a',
folder_created: '\u5df2\u521b\u5efa\u6587\u4ef6\u5939 ',
folder_create_failed: '\u521b\u5efa\u6587\u4ef6\u5939\u5931\u8d25\uff1a',
remove_title: '\u79fb\u9664',
empty_dir: '(\u7a7a)',
upload_failed: '\u4e0a\u4f20\u5931\u8d25\uff1a',
all_uploads_failed: (n) => `${n} \u4e2a\u6587\u4ef6\u5168\u90e8\u4e0a\u4f20\u5931\u8d25`,
// settings panel
settings_title: '\u8bbe\u7f6e',
settings_save_btn: '\u4fdd\u5b58\u8bbe\u7f6e',
settings_label_model: '\u9ed8\u8ba4\u6a21\u578b',
settings_label_send_key: '\u53d1\u9001\u5feb\u6377\u952e',
settings_label_theme: '\u4e3b\u9898',
settings_label_language: '\u8bed\u8a00',
settings_label_token_usage: '\u663e\u793a token \u7528\u91cf',
settings_label_cli_sessions: '\u663e\u793a CLI \u4f1a\u8bdd',
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
settings_label_check_updates: '\u68c0\u67e5\u66f4\u65b0',
settings_label_bot_name: '\u52a9\u624b\u540d\u79f0',
settings_label_password: '\u8bbf\u95ee\u5bc6\u7801',
settings_saved: '\u8bbe\u7f6e\u5df2\u4fdd\u5b58',
settings_save_failed: '\u4fdd\u5b58\u5931\u8d25\uff1a',
settings_load_failed: '\u8bbe\u7f6e\u52a0\u8f7d\u5931\u8d25\uff1a',
settings_saved_pw: '\u8bbe\u7f6e\u5df2\u4fdd\u5b58\uff08\u5bc6\u7801\u5df2\u8bbe\u7f6e\u2014\u73b0\u5728\u9700\u8981\u767b\u5f55\uff09',
// login page
login_title: '\u767b\u5f55',
login_subtitle: '\u8f93\u5165\u5bc6\u7801\u7ee7\u7eed\u4f7f\u7528',
login_placeholder: '\u5bc6\u7801',
login_btn: '\u767b\u5f55',
login_invalid_pw: '\u5bc6\u7801\u9519\u8bef',
login_conn_failed: '\u8fde\u63a5\u5931\u8d25',
// missing keys from English
tab_chat: '\u804a\u5929',
tab_memory: '\u8a18\u61b6',
tab_skills: '\u6280\u80fd',
tab_tasks: '\u4efb\u52d9',
tab_todos: '\u5f85\u8e29',
tab_workspaces: '\u5de5\u4f5c\u5340',
new_conversation: '\u65b0\u5b58\u5c0d\u8a71',
filter_conversations: '\u7b5c\u9078\u5b58\u5c0d\u8a71',
scheduled_jobs: '\u5b58\u5287\u4efb\u52d9',
new_job: '\u65b0\u4efb\u52d9',
search_skills: '\u641c\u5c0b\u6280\u80fd',
new_skill: '\u65b0\u6280\u80fd',
save_skill: '\u5132\u5b58\u6280\u80fd',
personal_memory: '\u500b\u4eba\u8a18\u61b6',
current_task_list: '\u76ee\u524d\u4efb\u52d9\u6e05\u55ae',
new_profile: '\u65b0\u914d\u7f6e\u6a94',
transcript: '\u8a18\u9304',
download_transcript: '\u4e0b\u8f09\u8a18\u9304',
import: '\u5c0e\u5165',
editing: '\u7de8\u8f2f\u4e2d',
empty_title: '\u7a7a\u767c\u5b58\u7a7a\u9593',
empty_subtitle: '\u9ede\u64ca\u4e0a\u65b9\u6309\u9215\u958b\u59cb\u5c0d\u8a71',
cancel: '\u53d6\u6d88',
loading: '\u52a0\u8f09\u4e2d',
create_job: '\u5efa\u7acb\u4efb\u52d9',
suggest_plan: '\u5efa\u8b70\u8a08\u5287',
suggest_schedule: '\u5efa\u8b70\u6642\u7a0b',
suggest_files: '\u5efa\u8b70\u6a94\u6848',
sign_out: '\u767b\u51fa',
password_placeholder: '\u5bc6\u7801',
disable_auth: '\u505c\u7528\u9a57\u8b49',
settings_label_sound: '\u901a\u77e5\u8072\u97f3',
settings_label_notifications: '\u700f\u89bd\u901a\u77e5',
settings_desc_sound: '\u52a9\u624b\u5b8c\u6210\u56de\u7b54\u6642\u64a9\u653e\u8072\u97f3\u3002',
settings_desc_notifications: '\u7576\u5206\u9801\u5728\u5f8c\u53f0\u6642\uff0c\u6709\u56de\u7b54\u5b8c\u6210\u6e05\u55ae\u6703\u986f\u793a\u7cfb\u7d71\u901a\u77e5\u3002',
settings_desc_token_usage: '\u5728\u52a9\u624b\u6bcf\u6b21\u56de\u7b54\u4e0b\u65b9\u986f\u793a Input/Output token \u6578\u91cf\u3002\u4e5f\u53ef\u4ee5\u7528 /usage \u5207\u63db\u3002',
settings_desc_cli_sessions: '\u5c07 Hermes CLI (\u7684 state.db) \u4e2d\u7684\u4f1a\u8a71\u6dfb\u52a0\u5230\u4f1a\u8a71\u6e05\u55ae\u3002\u9ede\u64ca\u4e00\u500b CLI \u4f1a\u8a71\u5c07\u5c0e\u5165\u5b83\u7a0b\u5f0f\u4e26\u7e7c\u7e8c\u5b58\u5c0d\u8a71\u3002',
settings_desc_sync_insights: '\u5c07 WebUI token \u4f7f\u7528\u60c5\u6cc1\u540c\u6b65\u5230 state.db\uff0c\u8a93 hermes /insights \u5305\u542b\u700f\u89bd\u5668\u4f1a\u8a71\u6578\u64da\u3002\u9810\u8a2d\u70b8\u555f\u7528\u3002',
settings_desc_check_updates: '\u7576\u6709\u66f4\u65b0\u7684 WebUI \u6216\u52a9\u624b\u7248\u672c\u6642\u986f\u793a\u6a19\u8a18\u3002\u5c07\u5728\u5f8c\u81ea\u6b63\u5e38\u57f7\u884c Git-Fetch\u3002',
settings_desc_bot_name: '\u52a9\u624b\u5728 UI \u4e2d\u7684\u986f\u793a\u540d\u7a31\u3002\u9810\u8a2d\u70b8\u7528\u6539\u3002',
settings_desc_password: '\u8a2d\u5b9a WebUI \u767b\u5165\u5bc6\u7801\u3002\u5047\u5982\u5df2\u8a2d\u7f6e\uff0c\u6bcf\u6b21\u52a0\u8f09\u90fd\u9700\u8981\u767b\u5165\u3002',
settings_label_sound: '\u901a\u77e5\u8072\u97f3',
},
// Traditional Chinese (zh-Hant)
'zh-Hant': {
_lang: 'zh-Hant',
_label: '\u7e41\u9ad4\u4e2d\u6587',
_speech: 'zh-TW',
// boot.js
cancelling: '\u6b63\u5728\u53d6\u6d88...',
cancel_failed: '\u53d6\u6d88\u5931\u6557\uff1a',
mic_denied: '\u9ea6\u514b\u98a8\u8a2a\u554f\u88ab\u62d2\u7d75\uff0c\u8acb\u6aa2\u67e5\u700f\u89bd\u5668\u6b0a\u9650\u3002',
mic_no_speech: '\u6c92\u6709\u6aa2\u6e2c\u5230\u8a71\u97f3\uff0c\u8acb\u518d\u5617\u4e00\u6b21\u3002',
mic_network: '\u8a71\u97f3\u8b58\u5225\u76ee\u524d\u4e0d\u53ef\u7528\u3002',
mic_error: '\u8a71\u97f3\u8f38\u5165\u51fa\u932f\uff1a',
session_imported: '\u6703\u8a71\u5df2\u5c0e\u5165',
import_failed: '\u5c0e\u5165\u5931\u6557\uff1a',
import_invalid_json: 'JSON \u7121\u6548',
image_pasted: '\u5df2\u7c98\u8cbc\u5716\u7247\uff1a',
// messages.js
edit_message: '\u7de8\u8f2f\u8a0a\u606f',
regenerate: '\u91cd\u65b0\u751f\u6210\u56de\u8986',
copy: '\u8907\u88fd',
copied: '\u5df2\u8907\u88fd',
you: '\u4f60',
thinking: '\u601d\u8003\u904e\u7a0b',
expand_all: '\u5168\u90e8\u5c55\u958b',
collapse_all: '\u5168\u90e8\u6298\u758a',
edit_failed: '\u7de8\u8f2f\u5931\u6557\uff1a',
regen_failed: '\u91cd\u65b0\u751f\u6210\u5931\u6557\uff1a',
reconnect_active: '\u56de\u8986\u4ecd\u5728\u751f\u6210\u4e2d\uff0c\u6e96\u5099\u597d\u5f8c\u8981\u91cd\u65b0\u52a0\u8f09\u55ce\uff1f',
reconnect_finished: '\u4f60\u96e2\u958b\u6642\u6709\u56de\u8986\u6b63\u5728\u751f\u6210\uff0c\u8a0a\u606f\u5167\u5bb9\u53ef\u80fd\u5df2\u7d93\u66f4\u65b0\u3002',
// approval card
approval_heading: '\u9700\u8981\u5ba1\u6838',
approval_desc_prefix: '\u6aa2\u6e2c\u5230\u5371\u96aa\u547d\u4ee4',
approval_btn_once: '\u5141\u8a31\u4e00\u6b21',
approval_btn_once_title: '\u5141\u8a31\u57f7\u884c\u6b64\u547d\u4ee4\u4e00\u6b21\uff08Enter\uff09',
approval_btn_session: '\u672c\u6b21\u5141\u8a31',
approval_btn_session_title: '\u672c\u6b21\u6703\u8a71\u671f\u9593\u5141\u8a31',
approval_btn_always: '\u59c4\u59b9\u5141\u8a31',
approval_btn_always_title: '\u59c4\u59b9\u5141\u8a31\u6b64\u547d\u4ee4\u6a21\u5f0f',
approval_btn_deny: '\u62d2\u7edd',
approval_btn_deny_title: '\u62d2\u7edd — \u4e0d\u57f7\u884c\u6b64\u547d\u4ee4',
approval_responding: '\u8655\u7406\u4e2d\u2026',
untitled: '\u672a\u547d\u540d',
n_messages: (n) => `${n} \u689d\u8a0a\u606f`,
model_unavailable: '\uff08\u4e0d\u53ef\u7528\uff09',
model_unavailable_title: '\u6b64\u6a21\u578b\u5df2\u7d93\u4e0d\u5728\u7576\u524d provider \u5217\u8868\u4e2d',
// commands.js
cmd_help: '\u67e5\u770b\u53ef\u7528\u547d\u4ee4',
cmd_clear: '\u6e05\u7a7a\u7576\u524d\u5c0d\u8a71\u8a0a\u606f',
cmd_compact: '\u58d3\u7e2e\u5c0d\u8a71\u4e0a\u4e0b\u6587',
cmd_model: '\u5207\u63db\u6a21\u578b\uff08\u4f8b\u5982 /model gpt-4o\uff09',
cmd_workspace: '\u6309\u540d\u7a31\u5207\u63db\u5de5\u4f5c\u5340',
cmd_new: '\u65b0\u5efa\u804a\u5929\u6703\u8a71',
cmd_usage: '\u5207\u63db token \u7528\u91cf\u986f\u793a',
cmd_theme: '\u5207\u63db\u4e3b\u984c\uff08dark/light/slate/solarized/monokai/nord/oled\uff09',
cmd_personality: '\u5207\u63db Agent \u4eba\u8a2d',
available_commands: '\u53ef\u7528\u547d\u4ee4\uff1a',
type_slash: '\u8f38\u5165 / \u53ef\u67e5\u770b\u547d\u4ee4',
conversation_cleared: '\u5c0d\u8a71\u5df2\u6e05\u7a7a',
model_usage: '\u7528\u6cd5\uff1a/model <name>',
no_model_match: '\u6c92\u6709\u5339\u914d\u201c',
switched_to: '\u5df2\u5207\u63db\u5230 ',
workspace_usage: '\u7528\u6cd5\uff1a/workspace <name>',
no_workspace_match: '\u6c92\u6709\u5339\u914d\u201c',
switched_workspace: '\u5df2\u5207\u63db\u5de5\u4f5c\u5340\uff1a',
workspace_switch_failed: '\u5de5\u4f5c\u5340\u5207\u63db\u5931\u6557\uff1a',
new_session: '\u5df2\u65b0\u5efa\u6703\u8a71',
compressing: '\u6b63\u5728\u8981\u6c42\u58d3\u7e2e\u4e0a\u4e0b\u6587...',
token_usage_on: 'Token \u7528\u91cf\u986f\u793a\u5df2\u958b\u555f',
token_usage_off: 'Token \u7528\u91cf\u986f\u793a\u5df2\u95dc\u9589',
theme_usage: '\u7528\u6cd5\uff1a/theme ',
theme_set: '\u4e3b\u984c\uff1a',
no_active_session: '\u7576\u524d\u6c92\u6709\u6d3b\u52d5\u6703\u8a71',
no_personalities: '\u6c92\u6709\u627e\u5230\u4eba\u8a2d\uff08\u53ef\u6dfb\u52a0\u5230 ~/.hermes/personalities/\uff09',
available_personalities: '\u53ef\u7528\u4eba\u8a2d\uff1a',
personality_switch_hint: '\n\n\u4f7f\u7528 `/personality <name>` \u5207\u63db\uff0c\u6216\u7528 `/personality none` \u6e05\u7a7a\u3002',
personalities_load_failed: '\u52a0\u8f7d\u4eba\u8a2d\u5931\u6557',
personality_cleared: '\u4eba\u8a2d\u5df2\u6e05\u7a7a',
personality_set: '\u7576\u524d\u4eba\u8a2d\uff1a',
failed_colon: '\u5931\u6557\uff1a',
// ui.js
no_workspace: '\u672a\u9078\u64c7\u5de5\u4f5c\u5340',
// workspace.js
unsaved_confirm: '\u9810\u89bd\u5340\u6709\u672a\u5132\u5b58\u4fee\u6539\uff0c\u8981\u653e\u68c4\u66f4\u6539\u5e76\u7e7c\u7e8c\u8df3\u8ee2\u55ce\uff1f',
save: '\u5132\u5b58',
edit: '\u7de8\u8f2f',
save_title: '\u5132\u5b58\u4fee\u6539',
edit_title: '\u7de8\u8f2f\u6b64\u6587\u4ef6',
saved: '\u5df2\u5132\u5b58',
save_failed: '\u5132\u5b58\u5931\u6557\uff1a',
image_load_failed: '\u5716\u7247\u52a0\u8f09\u5931\u6557',
file_open_failed: '\u7121\u6cd5\u6253\u958b\u6587\u4ef6',
downloading: (name) => `\u6b63\u5728\u4e0b\u8f09 ${name}...`,
double_click_rename: '\u96d9\u64ca\u91cd\u547d\u540d',
renamed_to: '\u5df2\u91cd\u547d\u540d\u70ba ',
rename_failed: '\u91cd\u547d\u540d\u5931\u6557\uff1a',
delete_title: '\u522a\u9664',
delete_confirm: (name) => `\u8981\u522a\u9664 ${name} \u55ce\uff1f`,
deleted: '\u5df2\u522a\u9664 ',
delete_failed: '\u522a\u9664\u5931\u6557\uff1a',
new_file_prompt: '\u65b0\u6587\u4ef6\u540d\uff08\u4f8b\u5982 notes.md\uff09\uff1a',
created: '\u5df2\u5275\u5efa ',
create_failed: '\u5275\u5efa\u5931\u6557\uff1a',
new_folder_prompt: '\u65b0\u6587\u4ef6\u593e\u540d\u7a31\uff1a',
folder_created: '\u5df2\u5275\u5efa\u6587\u4ef6\u593e ',
folder_create_failed: '\u5275\u5efa\u6587\u4ef6\u593e\u5931\u6557\uff1a',
remove_title: '\u79fb\u9664',
empty_dir: '(\u7a7a)',
upload_failed: '\u4e0a\u50b3\u5931\u6557\uff1a',
all_uploads_failed: (n) => `${n} \u500b\u6587\u4ef6\u5168\u90e8\u4e0a\u50b3\u5931\u6557`,
// settings panel
settings_title: '\u8a2d\u5b9a',
settings_save_btn: '\u5132\u5b58\u8a2d\u5b9a',
settings_label_model: '\u9ed8\u8a8d\u6a21\u578b',
settings_label_send_key: '\u767c\u9001\u5feb\u6377\u9375',
settings_label_theme: '\u4e3b\u984c',
settings_label_language: '\u8a9d\u8a00',
settings_label_token_usage: '\u986f\u793a token \u7528\u91cf',
settings_label_cli_sessions: '\u986f\u793a CLI \u6703\u8a71',
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
settings_label_check_updates: '\u6aa2\u67e5\u66f4\u65b0',
settings_label_bot_name: '\u52a9\u624b\u540d\u7a31',
settings_label_password: '\u8a2a\u8aad\u5bc6\u78bc',
settings_saved: '\u8a2d\u5b9a\u5df2\u5132\u5b58',
settings_save_failed: '\u5132\u5b58\u5931\u6557\uff1a',
settings_load_failed: '\u8a2d\u5b9a\u52a0\u8f09\u5931\u6557\uff1a',
settings_saved_pw: '\u8a2d\u5b9a\u5df2\u5132\u5b58\uff08\u5bc6\u78bc\u5df2\u8a2d\u5b9a\u2014\u73fe\u5728\u9700\u8981\u767b\u5f55\uff09',
// login page
login_title: '\u767b\u5f55',
login_subtitle: '\u8f38\u5165\u5bc6\u78bc\u7e7c\u7e8c\u4f7f\u7528',
login_placeholder: '\u5bc6\u78bc',
login_btn: '\u767b\u5f55',
login_invalid_pw: '\u5bc6\u78bc\u932f\u8aa4',
login_conn_failed: '\u9023\u63a5\u5931\u6557',
// missing keys from English
tab_chat: '\u804a\u5929',
tab_memory: '\u8a18\u61b6',
tab_skills: '\u6280\u80fd',
tab_tasks: '\u4efb\u52d9',
tab_todos: '\u5f85\u8e29',
tab_workspaces: '\u5de5\u4f5c\u5340',
new_conversation: '\u65b0\u5b58\u5c0d\u8a71',
filter_conversations: '\u7b5c\u9078\u5b58\u5c0d\u8a71',
scheduled_jobs: '\u5b58\u5287\u4efb\u52d9',
new_job: '\u65b0\u4efb\u52d9',
search_skills: '\u641c\u5c0b\u6280\u80fd',
new_skill: '\u65b0\u6280\u80fd',
save_skill: '\u5132\u5b58\u6280\u80fd',
personal_memory: '\u500b\u4eba\u8a18\u61b6',
current_task_list: '\u76ee\u524d\u4efb\u52d9\u6e05\u55ae',
new_profile: '\u65b0\u914d\u7f6e\u6a94',
transcript: '\u8a18\u9304',
download_transcript: '\u4e0b\u8f09\u8a18\u9304',
import: '\u5c0e\u5165',
editing: '\u7de8\u8f2f\u4e2d',
empty_title: '\u7a7a\u767c\u5b58\u7a7a\u9593',
empty_subtitle: '\u9ede\u64ca\u4e0a\u65b9\u6309\u9215\u958b\u59cb\u5c0d\u8a71',
cancel: '\u53d6\u6d88',
loading: '\u52a0\u8f09\u4e2d',
create_job: '\u5efa\u7acb\u4efb\u52d9',
suggest_plan: '\u5efa\u8b70\u8a08\u5287',
suggest_schedule: '\u5efa\u8b70\u6642\u7a0b',
suggest_files: '\u5efa\u8b70\u6a94\u6848',
sign_out: '\u767b\u51fa',
password_placeholder: '\u5bc6\u78bc',
disable_auth: '\u505c\u7528\u9a57\u8b49',
settings_label_sound: '\u901a\u77e5\u8072\u97f3',
settings_label_notifications: '\u700f\u89bd\u901a\u77e5',
settings_desc_sound: '\u52a9\u624b\u5b8c\u6210\u56de\u7b54\u6642\u64a9\u653e\u8072\u97f3\u3002',
settings_desc_notifications: '\u7576\u5206\u9801\u5728\u5f8c\u81ea\u6642\uff0c\u6709\u56de\u7b54\u5b8c\u6210\u6e05\u55ae\u6703\u986f\u793a\u7cfb\u7d71\u901a\u77e5\u3002',
settings_desc_token_usage: '\u5728\u52a9\u624b\u6bcf\u6b21\u56de\u7b54\u4e0b\u65b9\u986f\u793a Input/Output token \u6578\u91cf\u3002\u4e5f\u53ef\u4ee5\u7528 /usage \u5207\u63db\u3002',
settings_desc_cli_sessions: '\u5c07 Hermes CLI (\u7684 state.db) \u4e2d\u7684\u6703\u8a71\u6dfb\u52a0\u5230\u6703\u8a71\u6e05\u55ae\u3002\u9ede\u64ca\u4e00\u500b CLI \u6703\u8a71\u5c07\u5c0e\u5165\u5b83\u7a0b\u5f0f\u4e26\u7e7c\u7e8c\u5b58\u5c0d\u8a71\u3002',
settings_desc_sync_insights: '\u5c07 WebUI token \u4f7f\u7528\u60c5\u6cc1\u540c\u6b65\u5230 state.db\uff0c\u8a93 hermes /insights \u5305\u542b\u700f\u89bd\u5668\u6703\u8a71\u6578\u64da\u3002\u9810\u8a2d\u70b8\u555f\u7528\u3002',
settings_desc_check_updates: '\u7576\u6709\u66f4\u65b0\u7684 WebUI \u6216\u52a9\u624b\u7248\u672c\u6642\u986f\u793a\u6a19\u8a18\u3002\u5c07\u5728\u5f8c\u81ea\u6b63\u5e38\u57f7\u884c Git-Fetch\u3002',
settings_desc_bot_name: '\u52a9\u624b\u5728 UI \u4e2d\u7684\u986f\u793a\u540d\u7a31\u3002\u9810\u8a2d\u70b8\u7528\u6539\u3002',
settings_desc_password: '\u8a2d\u5b9a WebUI \u767b\u5165\u5bc6\u78bc\u3002\u5047\u5982\u5df2\u8a2d\u7f6e\uff0c\u6bcf\u6b21\u52a0\u8f09\u90fd\u9700\u8981\u767b\u5165\u3002',
settings_label_sound: '\u901a\u77e5\u8072\u97f3',
// boot.js
cancelling: '\u6b63\u5728\u53d6\u6d88...',
cancel_failed: '\u53d6\u6d88\u5931\u6557\uff1a',
mic_denied: '\u9ea6\u514b\u98a8\u8a2a\u554f\u88ab\u62d2\u7d75\uff0c\u8acb\u6aa2\u67e5\u700f\u89bd\u5668\u6b0a\u9650\u3002',
mic_no_speech: '\u6c92\u6709\u6aa2\u6e2c\u5230\u8a71\u97f3\uff0c\u8acb\u518d\u5617\u4e00\u6b21\u3002',
mic_network: '\u8a71\u97f3\u8b58\u5225\u76ee\u524d\u4e0d\u53ef\u7528\u3002',
mic_error: '\u8a71\u97f3\u8f38\u5165\u51fa\u932f\uff1a',
session_imported: '\u6703\u8a71\u5df2\u5c0e\u5165',
import_failed: '\u5c0e\u5165\u5931\u6557\uff1a',
import_invalid_json: 'JSON \u7121\u6548',
image_pasted: '\u5df2\u7c98\u8cbc\u5716\u7247\uff1a',
// messages.js
edit_message: '\u7de8\u8f2f\u8a0a\u606f',
regenerate: '\u91cd\u65b0\u751f\u6210\u56de\u8986',
copy: '\u8907\u88fd',
copied: '\u5df2\u8907\u88fd',
// ui.js
workspace_desc: '\u8acb\u9078\u64c7\u5de5\u4f5c\u5340\uff0c\u6216\u8f09\u5165\u65b0\u540d\u7a31\u5beb\u4e00\u500b',
tab_profiles: '\u914d\u7f6e',
},
};
// Active locale — defaults to English; overridden by loadLocale() at boot.
let _locale = LOCALES.en;
/**
* Translate a key. Falls back to English if the key is missing in the active locale.
* Supports function values (for interpolated strings): call t('key', arg).
* @param {string} key
* @param {...*} args - forwarded to function-valued translations
* @returns {string}
*/
function t(key, ...args) {
const val = _locale[key] ?? LOCALES.en[key];
if (val === undefined) return key; // final fallback: return key itself
return typeof val === 'function' ? val(...args) : val;
}
/**
* Switch locale by language code (e.g. 'en', 'zh').
* Persists to localStorage and updates the <html lang> attribute.
* @param {string} lang
*/
function setLocale(lang) {
const resolved = LOCALES[lang] ? lang : 'en';
_locale = LOCALES[resolved];
localStorage.setItem('hermes-lang', resolved);
document.documentElement.lang = _locale._speech || resolved;
}
/**
* Load locale from localStorage (called once at boot, before DOMContentLoaded).
* Server-persisted preference is applied later in loadSettingsPanel().
*/
function loadLocale() {
const saved = localStorage.getItem('hermes-lang');
setLocale(saved && LOCALES[saved] ? saved : 'en');
}
/**
* Re-stamp all [data-i18n] elements in the DOM with the current locale.
* Safe to call at any time — missing keys fall back to English.
* Call after setLocale() to make static HTML text update without a reload.
*/
function applyLocaleToDOM() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const val = t(key);
if (val && val !== key) el.textContent = val;
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
const val = t(key);
if (val && val !== key) el.title = val;
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
const val = t(key);
if (val && val !== key) el.placeholder = val;
});
}
// Apply saved locale immediately so there's no flash of English on reload.
loadLocale();