Files
webui/static/i18n.js
nesquena-hermes 42dd2b562d fix: warn on provider/model mismatch, surface auth errors (#266)
* fix: warn on provider/model mismatch, surface auth errors (#266)

Fixes #266 — WebUI silently ignores provider/model selection mismatch.

The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while
Hermes is configured for a different provider (e.g. local Ollama) sends
the request to the wrong endpoint, which returns a 401 Unauthorized error
with no UI indication of why.

Three-layer fix:

1. api/streaming.py — detect 401/auth errors explicitly
   Added is_auth_error detection covering '401', 'AuthenticationError',
   'authentication', 'unauthorized', 'invalid api key', and the specific
   Ollama error string 'no cookie auth credentials'. Auth errors emit
   apperror with type='auth_mismatch' and a hint pointing to 'hermes model'.

2. static/ui.js — expose active_provider and warn on selection
   - populateModelDropdown() stores data.active_provider from /api/models
     as window._activeProvider (the field was already in the response but
     the frontend never used it)
   - New _checkProviderMismatch(modelId) helper: compares the selected
     model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against
     the active provider. Skips the check for 'openrouter' and 'custom'
     to avoid false positives on configs that legitimately route any model.

3. static/boot.js — warn on model dropdown change
   modelSelect.onchange calls _checkProviderMismatch() and shows a toast
   when the selected model looks incompatible with the configured provider.

4. static/messages.js — distinct UI label for auth errors
   apperror handler now distinguishes type='auth_mismatch' and shows
   'Provider mismatch' as the error label instead of 'Error'.

5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label
   keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant).

Tests: 21 new tests in tests/test_provider_mismatch.py covering all
five change areas. 679/679 total pass (658 baseline + 21 new).

* fix: t() call args spread + use i18n label for auth mismatch

1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single
   array arg to t(). Since t(key, ...args) spreads, the function
   received the array as m and undefined as p. Fixed to pass as
   separate args: t('provider_mismatch_warning', modelId, ap).

2. messages.js: 'Provider mismatch' label was hardcoded instead of
   using t('provider_mismatch_label'). Now uses the i18n key with
   fallback for when t() isn't available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:25:18 -07:00

1039 lines
53 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',
provider_mismatch_warning: (m,p)=>`"${m}" may not work with your configured provider (${p}). Send anyway, or run \`hermes model\` in your terminal to switch.`,
provider_mismatch_label: 'Provider mismatch',
// 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',
cmd_skills: 'List available Hermes skills',
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 agent 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',
dialog_confirm_title: 'Confirm action',
dialog_prompt_title: 'Enter a value',
dialog_confirm_btn: 'Confirm',
discard: 'Discard',
clear: 'Clear',
create: 'Create',
remove: 'Remove',
project_name_prompt: 'Project name:',
// 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.',
},
es: {
_lang: 'es',
_label: 'Español',
_speech: 'es-ES',
// boot.js
cancelling: 'Cancelando…',
cancel_failed: 'Error al cancelar: ',
mic_denied: 'Acceso al micrófono denegado. Revisa los permisos del navegador.',
mic_no_speech: 'No se detectó voz. Inténtalo de nuevo.',
mic_network: 'El reconocimiento de voz no está disponible.',
mic_error: 'Error de entrada por voz: ',
session_imported: 'Sesión importada',
import_failed: 'Error al importar: ',
import_invalid_json: 'JSON inválido',
image_pasted: 'Imagen pegada: ',
// messages.js
edit_message: 'Editar mensaje',
regenerate: 'Regenerar respuesta',
copy: 'Copiar',
copied: '¡Copiado!',
you: 'Tú',
thinking: 'Pensando',
expand_all: 'Expandir todo',
collapse_all: 'Contraer todo',
edit_failed: 'Error al editar: ',
regen_failed: 'Error al regenerar: ',
reconnect_active: 'Todavía se está generando una respuesta. ¿Recargar cuando termine?',
reconnect_finished: 'Había una respuesta en curso cuando te fuiste. Puede que los mensajes se hayan actualizado.',
// approval card
approval_heading: 'Se requiere aprobación',
approval_desc_prefix: 'Se detectó un comando peligroso',
approval_btn_once: 'Permitir una vez',
approval_btn_once_title: 'Permitir solo este comando (Enter)',
approval_btn_session: 'Permitir en la sesión',
approval_btn_session_title: 'Permitir durante esta sesión de conversación',
approval_btn_always: 'Permitir siempre',
approval_btn_always_title: 'Permitir siempre este patrón de comando',
approval_btn_deny: 'Denegar',
approval_btn_deny_title: 'Denegar — no ejecutar este comando',
approval_responding: 'Respondiendo…',
untitled: 'Sin título',
n_messages: (n) => `${n} mensajes`,
model_unavailable: ' (no disponible)',
model_unavailable_title: 'Este modelo ya no está en tu lista actual de proveedores',
provider_mismatch_warning: (m,p)=>`"${m}" puede no funcionar con tu proveedor configurado (${p}). Envía de todas formas, o ejecuta \`hermes model\` en la terminal para cambiar.`,
provider_mismatch_label: 'Proveedor incompatible',
// commands.js
cmd_help: 'Listar los comandos disponibles',
cmd_clear: 'Borrar los mensajes de la conversación',
cmd_compact: 'Comprimir el contexto de la conversación',
cmd_model: 'Cambiar de modelo (p. ej. /model gpt-4o)',
cmd_workspace: 'Cambiar de espacio de trabajo por nombre',
cmd_new: 'Iniciar una nueva sesión de chat',
cmd_usage: 'Activar o desactivar el uso de tokens',
cmd_theme: 'Cambiar tema (dark/light/slate/solarized/monokai/nord/oled)',
cmd_personality: 'Cambiar la personalidad del agente',
cmd_skills: 'Listar las skills de Hermes disponibles',
available_commands: 'Comandos disponibles:',
type_slash: 'Escribe / para ver los comandos',
conversation_cleared: 'Conversación borrada',
model_usage: 'Uso: /model <name>',
no_model_match: 'No hay ningún modelo que coincida con "',
switched_to: 'Se cambió a ',
workspace_usage: 'Uso: /workspace <name>',
no_workspace_match: 'No hay ningún espacio de trabajo que coincida con "',
switched_workspace: 'Se cambió al espacio de trabajo: ',
workspace_switch_failed: 'Error al cambiar de espacio de trabajo: ',
new_session: 'Nueva sesión creada',
compressing: 'Solicitando compresión del contexto...',
token_usage_on: 'Uso de tokens activado',
token_usage_off: 'Uso de tokens desactivado',
theme_usage: 'Uso: /theme ',
theme_set: 'Tema: ',
no_active_session: 'No hay ninguna sesión activa',
no_personalities: 'No se encontraron personalidades (añádelas a ~/.hermes/personalities/)',
available_personalities: 'Personalidades disponibles:',
personality_switch_hint: '\n\nUsa `/personality <name>` para cambiar, o `/personality none` para limpiar.',
personalities_load_failed: 'No se pudieron cargar las personalidades',
personality_cleared: 'Personalidad borrada',
personality_set: 'Personalidad: ',
failed_colon: 'Error: ',
// ui.js
no_workspace: 'Sin espacio de trabajo',
// workspace.js
unsaved_confirm: 'Tienes cambios sin guardar en la vista previa. ¿Descartar y navegar?',
save: 'Guardar',
edit: 'Editar',
save_title: 'Guardar cambios',
edit_title: 'Editar este archivo',
saved: 'Guardado',
save_failed: 'Error al guardar: ',
image_load_failed: 'No se pudo cargar la imagen',
file_open_failed: 'No se pudo abrir el archivo',
downloading: (name) => `Descargando ${name}`,
double_click_rename: 'Haz doble clic para renombrar',
renamed_to: 'Renombrado a ',
rename_failed: 'Error al renombrar: ',
delete_title: 'Eliminar',
delete_confirm: (name) => `¿Eliminar ${name}?`,
deleted: 'Eliminado ',
delete_failed: 'Error al eliminar: ',
new_file_prompt: 'Nombre del archivo nuevo (p. ej. notes.md):',
created: 'Creado ',
create_failed: 'Error al crear: ',
new_folder_prompt: 'Nombre de la carpeta nueva:',
folder_created: 'Carpeta creada ',
folder_create_failed: 'Error al crear la carpeta: ',
remove_title: 'Quitar',
empty_dir: '(vacío)',
upload_failed: 'Error al subir: ',
all_uploads_failed: (n) => `Fallaron las ${n} subida(s)`,
// settings panel
settings_title: 'Configuración',
settings_save_btn: 'Guardar configuración',
settings_label_model: 'Modelo predeterminado',
settings_label_send_key: 'Tecla de envío',
settings_label_theme: 'Tema',
settings_label_language: 'Idioma',
settings_label_token_usage: 'Mostrar uso de tokens',
settings_label_cli_sessions: 'Mostrar sesiones de CLI',
settings_label_sync_insights: 'Sincronizar con insights',
settings_label_check_updates: 'Buscar actualizaciones',
settings_label_bot_name: 'Nombre del asistente',
settings_label_password: 'Contraseña de acceso',
settings_saved: 'Configuración guardada',
settings_save_failed: 'Error al guardar: ',
settings_load_failed: 'Error al cargar la configuración: ',
settings_saved_pw: 'Configuración guardada (contraseña establecida — ahora se requiere iniciar sesión)',
// login page (used server-side via /api/i18n/login endpoint)
login_title: 'Iniciar sesión',
login_subtitle: 'Introduce tu contraseña para continuar',
login_placeholder: 'Contraseña',
login_btn: 'Entrar',
login_invalid_pw: 'Contraseña inválida',
login_conn_failed: 'Error de conexión',
dialog_confirm_title: 'Confirmar acción',
dialog_prompt_title: 'Introduce un valor',
dialog_confirm_btn: 'Confirmar',
discard: 'Descartar',
clear: 'Borrar',
create: 'Crear',
remove: 'Quitar',
project_name_prompt: 'Nombre del proyecto:',
// Sidebar & Tabs
tab_chat: 'Chat',
tab_tasks: 'Tareas',
tab_skills: 'Habilidades',
tab_memory: 'Memoria',
tab_workspaces: 'Espacios',
tab_profiles: 'Perfiles',
tab_todos: 'Todos',
new_conversation: 'Nueva conversación',
filter_conversations: 'Filtrar conversaciones...',
scheduled_jobs: 'Tareas programadas',
new_job: 'Nueva tarea',
loading: 'Cargando...',
search_skills: 'Buscar skills...',
new_skill: 'Nueva skill',
personal_memory: 'Memoria personal',
current_task_list: 'Lista de tareas actual',
workspace_desc: 'Añade y cambia espacios de trabajo para tus sesiones.',
new_profile: 'Nuevo perfil',
transcript: 'Transcripción',
download_transcript: 'Descargar como Markdown',
import: 'Importar',
// Settings detail
settings_label_sound: 'Sonido de notificación',
settings_desc_sound: 'Reproduce un sonido cuando el asistente termina una respuesta.',
settings_label_notifications: 'Notificaciones del navegador',
settings_desc_notifications: 'Muestra una notificación del sistema cuando una respuesta termina mientras la pestaña está en segundo plano.',
settings_desc_token_usage: 'Muestra el conteo de tokens de entrada/salida debajo de cada respuesta del asistente. También se puede alternar con /usage.',
settings_desc_cli_sessions: 'Fusiona las sesiones del CLI de Hermes (state.db) en la lista de sesiones. Haz clic en una sesión de CLI para importarla y continuar la conversación.',
settings_desc_sync_insights: 'Refleja el uso de tokens de la WebUI en state.db para que hermes /insights incluya datos de sesiones del navegador. Desactivado por defecto.',
settings_desc_check_updates: 'Muestra un banner cuando haya versiones más nuevas de la WebUI o del Agent. Ejecuta periódicamente un git fetch en segundo plano.',
settings_desc_bot_name: 'Nombre visible del asistente en toda la UI. Por defecto es Hermes.',
settings_desc_password: 'Introduce una nueva contraseña para establecerla o cambiarla. Déjalo en blanco para mantener la configuración actual.',
password_placeholder: 'Introduce una contraseña nueva…',
disable_auth: 'Desactivar autenticación',
sign_out: 'Cerrar sesión',
cancel: 'Cancelar',
create_job: 'Crear tarea',
save_skill: 'Guardar skill',
editing: 'Editando',
// Empty state
empty_title: '¿En qué puedo ayudarte?',
empty_subtitle: 'Pregunta lo que quieras, ejecuta comandos, explora archivos o gestiona tus tareas programadas.',
suggest_files: '¿Qué archivos hay en este espacio de trabajo?',
suggest_schedule: '¿Qué tengo hoy en mi agenda?',
suggest_plan: 'Ayúdame a planificar un proyecto pequeño.',
},
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',
provider_mismatch_warning: (m,p)=>`"${m}" funktioniert möglicherweise nicht mit Ihrem konfigurierten Provider (${p}). Trotzdem senden, oder \`hermes model\` im Terminal ausführen.`,
provider_mismatch_label: 'Provider-Konflikt',
// 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',
cmd_skills: 'Verfügbare Hermes-Skills auflisten',
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: 'Agent-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',
dialog_confirm_title: 'Aktion bestätigen',
dialog_prompt_title: 'Wert eingeben',
dialog_confirm_btn: 'Bestätigen',
discard: 'Verwerfen',
clear: 'Leeren',
create: 'Erstellen',
remove: 'Entfernen',
project_name_prompt: 'Projektname:',
// 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',
provider_mismatch_warning: (m,p)=>`\"${m}\" \u53ef\u80fd\u65e0\u6cd5\u5728\u5f53\u524d\u914d\u7f6e\u7684\u63d0\u4f9b\u5546 (${p}) \u4e0b\u5de5\u4f5c\u3002\u76f4\u63a5\u53d1\u9001\uff0c\u6216\u5728\u7ec8\u7aef\u8fd0\u884c \`hermes model\` \u5207\u6362\u3002`,
provider_mismatch_label: '\u63d0\u4f9b\u5546\u4e0d\u5339\u914d',
// 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',
cmd_skills: '\u5217\u51fa\u53ef\u7528\u7684 Hermes \u6280\u80fd',
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',
dialog_confirm_title: '确认操作',
dialog_prompt_title: '输入内容',
dialog_confirm_btn: '确认',
discard: '放弃',
clear: '清空',
create: '创建',
remove: '移除',
project_name_prompt: '项目名称:',
// 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',
provider_mismatch_warning: (m,p)=>`\"${m}\" \u53ef\u80fd\u7121\u6cd5\u5728\u7576\u524d\u914d\u7f6e\u7684\u63d0\u4f9b\u8005 (${p}) \u4e0b\u904b\u4f5c\u3002\u5c1a\u9001\uff0c\u6216\u5728\u7d42\u7aef\u57f7\u884c \`hermes model\` \u5207\u63db\u3002`,
provider_mismatch_label: '\u63d0\u4f9b\u8005\u4e0d\u76f8\u7b26',
// 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',
cmd_skills: '\u5217\u51fa\u53ef\u7528\u7684 Hermes \u6280\u80fd',
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
dialog_confirm_title: '確認操作',
dialog_prompt_title: '輸入內容',
dialog_confirm_btn: '確認',
discard: '放棄',
clear: '清空',
create: '建立',
remove: '移除',
project_name_prompt: '專案名稱:',
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();