diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 53075c5d49bbd..398c6c7120d81 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -259,9 +259,11 @@ long transcript or a busy terminal. - Every user-facing string goes through `useI18n()` (`src/i18n/context.tsx`). No literals in JSX. -- **Update all locales together** — `en`, `ja`, `zh`, `zh-hant`. A string change - in `en.ts` that skips the others is a regression (drifted punctuation, - stale labels). Keep trailing-punctuation and tone consistent across all four. +- **Update all locales together** — `en`, `de`, `ja`, `zh`, `zh-hant`. A string + change in `en.ts` that skips the others is a regression (drifted punctuation, + stale labels). Keep trailing-punctuation and tone consistent across all five. + Partial locales use `defineLocale()` so untranslated keys fall back to English + while staying type-checked. ## State (TypeScript) @@ -299,13 +301,7 @@ The detailed state contract lives in the scoped - [ ] Keybind hints read from the store via `useKeybindHint` / `TipKeybindLabel`? - [ ] Overlay uses `shadow-nous` + `border-(--stroke-nous)`, no hard border? - [ ] Flat — no card-in-card, no gratuitous row dividers? -- [ ] No automatic navigation, focus steal, or pane opening from background - events? -- [ ] Direct manipulation paints immediately and rolls back cleanly on failure? -- [ ] Hot interactions avoid broad subscriptions, layout thrash, and - `transition-all`? -- [ ] Keyboard ownership and single-action `Esc` behavior are correct? -- [ ] All four locales updated for any new/changed string? +- [ ] All five locales updated for any new/changed string? - [ ] `cursor-pointer`, focus ring, and `Esc`-to-close behave? - [ ] Touched a primitive, token, or variant? Its named-contract entry in this file is updated in the same change. diff --git a/apps/desktop/src/i18n/catalog.ts b/apps/desktop/src/i18n/catalog.ts index 19556cb45c491..bb055baa75fda 100644 --- a/apps/desktop/src/i18n/catalog.ts +++ b/apps/desktop/src/i18n/catalog.ts @@ -1,4 +1,5 @@ import { en } from './en' +import { de } from './de' import { ja } from './ja' import type { Locale, Translations } from './types' import { zh } from './zh' @@ -6,6 +7,7 @@ import { zhHant } from './zh-hant' export const TRANSLATIONS: Record = { en, + de, zh, 'zh-hant': zhHant, ja diff --git a/apps/desktop/src/i18n/context.test.tsx b/apps/desktop/src/i18n/context.test.tsx index 3028c86ffa07d..f361662d3df99 100644 --- a/apps/desktop/src/i18n/context.test.tsx +++ b/apps/desktop/src/i18n/context.test.tsx @@ -135,7 +135,7 @@ describe('I18nProvider', () => { it('does not overwrite unsupported configured languages', async () => { const configClient: I18nConfigClient = { - getConfig: vi.fn().mockResolvedValue({ display: { language: 'de' } }), + getConfig: vi.fn().mockResolvedValue({ display: { language: 'xx' } }), saveConfig: vi.fn() } @@ -152,6 +152,25 @@ describe('I18nProvider', () => { expect(configClient.saveConfig).not.toHaveBeenCalled() }) + it('loads de from display.language config', async () => { + const configClient: I18nConfigClient = { + getConfig: vi.fn().mockResolvedValue({ display: { language: 'de' } }), + saveConfig: vi.fn() + } + + render( + + + + ) + + await waitFor(() => expect(screen.getByTestId('loading').textContent).toBe('false')) + + expect(screen.getByTestId('locale').textContent).toBe('de') + expect(screen.getByTestId('label').textContent).toBe('Sprache') + expect(configClient.saveConfig).not.toHaveBeenCalled() + }) + it('reads latest config before saving language and preserves unrelated values', async () => { const saveConfig = vi.fn().mockResolvedValue({ ok: true }) diff --git a/apps/desktop/src/i18n/de.test.ts b/apps/desktop/src/i18n/de.test.ts new file mode 100644 index 0000000000000..c0debae9eee2a --- /dev/null +++ b/apps/desktop/src/i18n/de.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' + +import { de } from './de' +import { en } from './en' + +// Guards the German locale against the regressions the hermes-sweeper flagged: +// visible copy left in English, and import/export label collisions. These are +// behaviour contracts (relationships), not snapshots of specific wording. +describe('de locale content', () => { + it('translates visible boot-failure and update copy', () => { + expect(de.boot.failure.title).not.toBe(en.boot.failure.title) + expect(de.boot.failure.description).not.toBe(en.boot.failure.description) + expect(de.notifications.seeWhatsNew).not.toBe(en.notifications.seeWhatsNew) + }) + + it('renders keybind action labels in German, not English identifiers', () => { + // These values are display copy shown in the command palette, not IDs. + for (const key of Object.keys(en.keybinds.actions) as (keyof typeof en.keybinds.actions)[]) { + expect(de.keybinds.actions[key]).not.toBe(en.keybinds.actions[key]) + } + }) + + it('does not collide import and export labels', () => { + expect(de.settings.importConfig).not.toBe(de.settings.exportConfig) + expect(de.settings.importConfig).not.toBe(en.settings.importConfig) + }) + + it('contains no corrupted placeholders or untranslated English leaves', () => { + // Walks every string leaf in the German catalog and rejects corruption + // markers that would surface verbatim to the user (e.g. "***", TODO, + // leftover English sentences). A locale that degrades should fall back to + // English via defineLocale(), never render a placeholder. + const corruption = /\*\*\*|FIXME|XXX|PLACEHOLDER|lorem ipsum/i + const englishSentence = /\b(the|is|are|was|were|your|you|this|that|with|from|click|settings|open|close|save)\b/i + + const visit = (node: unknown, path: string): void => { + if (typeof node === 'string') { + expect(corruption.test(node), `corruption in ${path}`).toBe(false) + // flag stray English only for leaf strings that look like full sentences + if (node.trim().length > 0 && /\s/.test(node) && englishSentence.test(node)) { + // allow legitimate English proper nouns / codes by checking word ratio + const words = node.split(/\s+/).filter(Boolean) + const englishWords = words.filter(w => englishSentence.test(w)).length + if (words.length >= 4 && englishWords / words.length > 0.5) { + throw new Error(`possible untranslated English leaf at ${path}: "${node}"`) + } + } + return + } + if (node && typeof node === 'object') { + for (const [k, v] of Object.entries(node)) { + visit(v, path ? `${path}.${k}` : k) + } + } + } + visit(de as unknown as Record, 'de') + }) +}) diff --git a/apps/desktop/src/i18n/de.ts b/apps/desktop/src/i18n/de.ts new file mode 100644 index 0000000000000..1c61b61ab29e3 --- /dev/null +++ b/apps/desktop/src/i18n/de.ts @@ -0,0 +1,2524 @@ +import { defineLocale } from './define-locale' + +import type { Translations } from './types' + +export const de = defineLocale({ + common: { + apply: 'Anwenden', + back: 'Zurück', + save: 'Speichern', + saving: 'Speichern…', + cancel: 'Abbrechen', + change: 'Ändern', + choose: 'Auswählen', + clear: 'Leeren', + close: 'Schließen', + collapse: 'Einklappen', + confirm: 'Bestätigen', + connect: 'Verbinden', + connecting: 'Verbinde…', + continue: 'Fortfahren', + copied: 'Kopiert', + copy: 'Kopieren', + copyFailed: 'Kopieren fehlgeschlagen', + delete: 'Löschen', + docs: 'Dokumentation', + done: 'Fertig', + error: 'Fehler', + expand: 'Ausklappen', + failed: 'Fehlgeschlagen', + formatJson: 'JSON formatieren', + free: 'Kostenlos', + loading: 'Lädt…', + notSet: 'Nicht festgelegt', + refresh: 'Aktualisieren', + remove: 'Entfernen', + replace: 'Ersetzen', + retry: 'Erneut versuchen', + run: 'Ausführen', + send: 'Senden', + set: 'Festlegen', + skip: 'Überspringen', + update: 'Aktualisieren', + tryHint: term => `Versuche „${term}“`, + on: 'Ein', + off: 'Aus' + }, + + fileMenu: { + revealFinder: 'Im Finder anzeigen', + revealExplorer: 'Im Datei-Explorer anzeigen', + revealFileManager: 'Enthaltenden Ordner öffnen', + revealInSidebar: 'Im Dateibaum anzeigen', + copyPath: 'Pfad kopieren', + copyRelativePath: 'Relativen Pfad kopieren', + rename: 'Umbenennen…', + delete: 'Löschen', + renameTitle: 'Umbenennen', + renameLabel: 'Neuer Name', + deleteTitle: name => `${name} löschen?`, + deleteBody: 'Es wird in den Papierkorb verschoben — du kannst es dort wiederherstellen.', + pathCopied: 'Pfad kopiert' + }, + + boot: { + ready: 'Hermes Desktop ist bereit', + desktopBootFailedWithMessage: message => `Desktop-Start fehlgeschlagen: ${message}`, + steps: { + connectingGateway: 'Verbinde mit Live-Desktop-Gateway', + loadingSettings: 'Hermes-Einstellungen werden geladen', + loadingSessions: 'Kürzliche Sitzungen werden geladen', + startingDesktopConnection: 'Desktop-Verbindung wird gestartet', + startingHermesDesktop: 'Hermes Desktop wird gestartet…' + }, + errors: { + backgroundExited: 'Hermes-Hintergrundprozess beendet.', + backgroundExitedDuringStartup: 'Hermes-Hintergrundprozess beim Start beendet.', + backendStopped: 'Backend gestoppt', + desktopBootFailed: 'Desktop-Start fehlgeschlagen', + gatewayConnectionLost: 'Verbindung zum Gateway verloren', + gatewaySignInRequired: 'Gateway-Anmeldung erforderlich', + ipcBridgeUnavailable: 'Desktop-IPC-Brücke nicht verfügbar.' + }, + failure: { + title: 'Hermes konnte nicht starten', + description: + 'Das Hintergrund-Gateway ist nicht hochgefahren. Versuche einen der Wiederherstellungsschritte unten. Nichts davon löscht deine Chats oder Einstellungen.', + remoteTitle: 'Remote-Gateway-Anmeldung erforderlich', + remoteDescription: + 'Deine Remote-Gateway-Sitzung ist abgelaufen. Melde dich erneut an, um die Verbindung wiederherzustellen. Hier werden weder deine Chats noch Einstellungen gelöscht.', + retry: 'Erneut versuchen', + repairInstall: 'Installation reparieren', + useLocalGateway: 'Lokales Gateway verwenden', + gatewaySettings: 'Gateway-Einstellungen', + back: 'Zurück', + openLogs: 'Protokolle öffnen', + repairHint: 'Reparatur startet den Installer erneut und kann auf einem neuen Rechner einige Minuten dauern.', + remoteSignInHint: signInLabel => + `Meldet die gespeicherte Remote-Browser-Sitzung ab und öffnet dann ${signInLabel}. Mit „Lokales Gateway" wechselst du stattdessen zum gebündelten Backend.`, + signOutAndSignIn: 'Abmelden & erneut anmelden', + remoteFailureHint: 'Prüfe die Gateway-URL und die Anmeldung unter Gateway-Einstellungen oder wechsle zum lokalen Gateway.', + hideRecentLogs: 'Letzte Protokolle ausblenden', + showRecentLogs: 'Letzte Protokolle anzeigen', + signedInTitle: 'Angemeldet', + signedInMessage: 'Verbindung zum Remote-Gateway wird wiederhergestellt…', + signInIncompleteTitle: 'Anmeldung unvollständig', + signInIncompleteMessage: 'Das Anmeldefenster schloss, bevor die Authentifizierung abgeschlossen war.', + signInFailed: 'Anmeldung fehlgeschlagen', + signInToRemoteGateway: 'Bei Remote-Gateway anmelden', + signInWithProvider: provider => `Mit ${provider} anmelden`, + identityProvider: 'dein Identitätsanbieter' + } + }, + + notifications: { + region: 'Benachrichtigungen', + hide: 'Ausblenden', + show: 'Anzeigen', + more: count => `${count} weitere ${count === 1 ? 'Benachrichtigung' : 'Benachrichtigungen'}`, + clearAll: 'Alle löschen', + dismiss: 'Benachrichtigung verwerfen', + details: 'Details', + copyDetail: 'Detail kopieren', + copyDetailFailed: 'Benachrichtigungsdetail konnte nicht kopiert werden', + backendOutOfDateTitle: 'Backend veraltet', + backendOutOfDateMessage: + 'Dein Hermes-Backend ist älter als dieser Desktop-Build und funktioniert möglicherweise nicht korrekt. Aktualisiere es, um sie abzugleichen.', + installMethodUnsupportedTitle: 'Nicht unterstützte Installationsmethode', + updateHermes: 'Hermes aktualisieren', + updateReadyTitle: 'Aktualisierung bereit', + updateReadyMessage: count => `${count} neue ${count === 1 ? 'Änderung' : 'Änderungen'} verfügbar.`, + seeWhatsNew: 'Neuigkeiten ansehen', + errors: { + elevenLabsNeedsKey: 'ElevenLabs-STT benötigt ELEVENLABS_API_KEY.', + elevenLabsRejectedKey: 'ElevenLabs hat den API-Schlüssel abgelehnt (401).', + methodNotAllowed: + 'Das Desktop-Backend hat die Anfrage abgelehnt (405 Method Not Allowed). Starte Hermes Desktop neu.', + microphonePermission: 'Mikrofon-Berechtigung wurde verweigert.', + openaiRejectedApiKey: 'OpenAI hat den API-Schlüssel abgelehnt.', + openaiRejectedApiKeyWithStatus: status => `OpenAI hat den API-Schlüssel abgelehnt (${status} invalid_api_key).`, + openaiTtsNeedsKey: 'OpenAI-TTS benötigt VOICE_TOOLS_OPENAI_KEY oder OPENAI_API_KEY.' + }, + voice: { + configureSpeechToText: 'Konfiguriere Sprache-zu-Text, um den Sprachmodus zu nutzen.', + couldNotStartSession: 'Sprachsitzung konnte nicht gestartet werden', + microphoneAccessDenied: 'Mikrofonzugriff verweigert.', + microphoneConstraintsUnsupported: 'Mikrofon-Einschränkungen werden von diesem Gerät nicht unterstützt.', + microphoneFailed: 'Mikrofon fehlgeschlagen', + microphoneInUse: 'Das Mikrofon wird bereits von einer anderen App verwendet.', + microphonePermissionDenied: 'Mikrofon-Berechtigung wurde verweigert.', + microphoneStartFailed: 'Mikrofonaufnahme konnte nicht gestartet werden.', + microphoneUnsupported: 'Diese Laufzeitumgebung unterstützt keine Mikrofonaufnahme.', + noMicrophone: 'Es wurde kein Mikrofon gefunden.', + noSpeechDetected: 'Keine Sprache erkannt', + playbackFailed: 'Sprachwiedergabe fehlgeschlagen', + recordingFailed: 'Sprachaufnahme fehlgeschlagen', + transcriptionFailed: 'Sprachtranskription fehlgeschlagen', + transcriptionUnavailable: 'Sprachtranskription ist noch nicht verfügbar.', + tryRecordingAgain: 'Versuche erneut aufzunehmen.', + unavailable: 'Sprache nicht verfügbar' + }, + native: { + approvalTitle: 'Freigabe erforderlich', + approveAction: 'Freigeben', + rejectAction: 'Ablehnen', + inputTitle: 'Eingabe erforderlich', + inputBody: 'Hermes wartet auf deine Antwort.', + turnDoneTitle: 'Hermes ist fertig', + turnDoneBody: 'Die Antwort ist bereit.', + turnErrorTitle: 'Schritt fehlgeschlagen', + backgroundDoneTitle: 'Hintergrundaufgabe beendet', + backgroundFailedTitle: 'Hintergrundaufgabe fehlgeschlagen' + } + }, + + remoteDisplayBanner: { + message: reason => + `Software-Rendering aktiv — Remote-Anzeige erkannt (${reason}). GPU-Beschleunigung ist deaktiviert, um Flackern zu verhindern.` + }, + + titlebar: { + hideSidebar: 'Seitenleiste ausblenden', + showSidebar: 'Seitenleiste anzeigen', + search: 'Suche', + searchTitle: 'Sitzungen, Ansichten und Aktionen suchen', + swapSidebarSides: 'Seitenleisten-Seiten tauschen', + swapSidebarSidesTitle: 'Sitzungs- und Datei-Browser-Seiten tauschen', + hideRightSidebar: 'Rechte Seitenleiste ausblenden', + showRightSidebar: 'Rechte Seitenleiste anzeigen', + muteHaptics: 'Haptik stummschalten', + unmuteHaptics: 'Haptik aktivieren', + openSettings: 'Einstellungen öffnen', + openStarmap: 'Gedächtnisgraph öffnen', + openKeybinds: 'Tastenkürzel' + }, + + keybinds: { + title: 'Tastenkürzel', + subtitle: open => `Klicke ein Kürzel, um es neu zu belegen · ${open} öffnet dieses Panel erneut.`, + rebind: 'Neu belegen', + reset: 'Auf Standard zurücksetzen', + resetAll: 'Alle zurücksetzen', + pressKey: 'Taste drücken…', + set: 'Festlegen', + conflictWith: label => `Ebenfalls gebunden an „${label}“`, + categories: { + composer: 'Eingabe', + profiles: 'Profile', + session: 'Sitzung', + navigation: 'Navigation', + view: 'Ansicht' + }, + actions: { + 'keybinds.openPanel': 'Tastenkürzel öffnen', + 'nav.commandPalette': 'Befehlspalette öffnen', + 'nav.commandCenter': 'Kommandozentrale öffnen', + 'nav.settings': 'Einstellungen öffnen', + 'nav.profiles': 'Profile öffnen', + 'nav.skills': 'Skills öffnen', + 'nav.messaging': 'Nachrichten öffnen', + 'nav.artifacts': 'Artefakte öffnen', + 'nav.cron': 'Geplante Aufgaben öffnen', + 'nav.agents': 'Agenten öffnen', + 'session.new': 'Neue Sitzung', + 'session.newWindow': 'Neue Sitzung im Fenster', + 'session.next': 'Nächste Sitzung', + 'session.prev': 'Vorherige Sitzung', + 'session.slot.1': 'Zu letzter Sitzung 1 wechseln', + 'session.slot.2': 'Zu letzter Sitzung 2 wechseln', + 'session.slot.3': 'Zu letzter Sitzung 3 wechseln', + 'session.slot.4': 'Zu letzter Sitzung 4 wechseln', + 'session.slot.5': 'Zu letzter Sitzung 5 wechseln', + 'session.slot.6': 'Zu letzter Sitzung 6 wechseln', + 'session.slot.7': 'Zu letzter Sitzung 7 wechseln', + 'session.slot.8': 'Zu letzter Sitzung 8 wechseln', + 'session.slot.9': 'Zu letzter Sitzung 9 wechseln', + 'session.focusSearch': 'Sitzungen durchsuchen', + 'session.togglePin': 'Aktuelle Sitzung anheften / lösen', + 'workspace.newWorktree': 'Neuer Worktree', + 'composer.focus': 'Eingabefeld fokussieren', + 'composer.modelPicker': 'Modellauswahl öffnen', + 'composer.voice': 'Sprachunterhaltung starten / stoppen', + 'view.toggleSidebar': 'Sitzungs-Seitenleiste umschalten', + 'view.toggleRightSidebar': 'Dateibrowser umschalten', + 'view.toggleReview': 'Review-Bereich umschalten', + 'view.showFiles': 'Dateibrowser anzeigen', + 'view.showTerminal': 'Terminal umschalten', + 'view.newTerminal': 'Neues Terminal', + 'view.nextTerminal': 'Nächstes Terminal', + 'view.prevTerminal': 'Vorheriges Terminal', + 'view.closeTerminal': 'Terminal schließen', + 'view.terminalSelection': 'Terminal-Auswahl an Eingabefeld senden', + 'view.closePreviewTab': 'Vorschau-Tab schließen', + 'view.flipPanes': 'Seitenleisten-Seiten tauschen', + 'appearance.toggleMode': 'Hell / Dunkel umschalten', + 'profile.default': 'Zum Standardprofil wechseln', + 'profile.switch.1': 'Zu Profil 1 wechseln', + 'profile.switch.2': 'Zu Profil 2 wechseln', + 'profile.switch.3': 'Zu Profil 3 wechseln', + 'profile.switch.4': 'Zu Profil 4 wechseln', + 'profile.switch.5': 'Zu Profil 5 wechseln', + 'profile.switch.6': 'Zu Profil 6 wechseln', + 'profile.switch.7': 'Zu Profil 7 wechseln', + 'profile.switch.8': 'Zu Profil 8 wechseln', + 'profile.switch.9': 'Zu Profil 9 wechseln', + 'profile.switch.10': 'Zu Profil 10 wechseln', + 'profile.switch.11': 'Zu Profil 11 wechseln', + 'profile.switch.12': 'Zu Profil 12 wechseln', + 'profile.switch.13': 'Zu Profil 13 wechseln', + 'profile.switch.14': 'Zu Profil 14 wechseln', + 'profile.switch.15': 'Zu Profil 15 wechseln', + 'profile.switch.16': 'Zu Profil 16 wechseln', + 'profile.switch.17': 'Zu Profil 17 wechseln', + 'profile.switch.18': 'Zu Profil 18 wechseln', + 'profile.next': 'Nächstes Profil', + 'profile.prev': 'Vorheriges Profil', + 'profile.toggleAll': 'Alle-Profile-Ansicht umschalten', + 'profile.create': 'Profil erstellen', + 'composer.send': 'Nachricht senden', + 'composer.newline': 'Zeilenumbruch einfügen', + 'composer.steer': 'Laufenden Turn steuern', + 'composer.sendQueued': 'Nächsten wartenden Turn senden', + 'composer.mention': 'Dateien, Ordner, URLs referenzieren', + 'composer.slash': 'Slash-Befehlspalette', + 'composer.help': 'Schnellhilfe', + 'composer.history': 'Popover / Verlauf durchblättern', + 'composer.cancel': 'Popover schließen · Lauf abbrechen' + } + }, + + language: { + label: 'Sprache', + description: 'Wähle die Sprache für die Desktop-Oberfläche.', + saving: 'Sprache wird gespeichert…', + saveError: 'Sprachaktualisierung fehlgeschlagen', + switchTo: 'Sprache wechseln', + searchPlaceholder: 'Sprachen suchen…', + noResults: 'Keine Sprachen gefunden' + }, + + settings: { + closeSettings: 'Einstellungen schließen', + exportConfig: 'Konfiguration exportieren', + importConfig: 'Konfiguration importieren', + resetToDefaults: 'Auf Standardwerte zurücksetzen', + resetConfirm: 'Alle Einstellungen auf Hermes-Standardwerte zurücksetzen?', + exportFailed: 'Export fehlgeschlagen', + resetFailed: 'Zurücksetzen fehlgeschlagen', + nav: { + providers: 'Anbieter', + providerAccounts: 'Konten', + providerApiKeys: 'API-Schlüssel', + gateway: 'Gateway', + apiKeys: 'Werkzeuge & Schlüssel', + keysTools: 'Werkzeuge', + keysSettings: 'Einstellungen', + mcp: 'MCP', + archivedChats: 'Archivierte Chats', + about: 'Über', + notifications: 'Benachrichtigungen' + }, + notifications: { + title: 'Benachrichtigungen', + intro: + 'Systemeigene Desktop-Benachrichtigungen, getrennt von In-App-Toasts. Diese sind gerätespezifisch — jeder Rechner behält seine eigenen Einstellungen.', + enableAll: 'Benachrichtigungen aktivieren', + enableAllDesc: 'Hauptschalter. Deaktiviere ihn, um alle folgenden Benachrichtigungen zu stummen.', + focusedHint: 'Abschluss-Benachrichtigungen erscheinen nur, wenn Hermes im Hintergrund läuft.', + kinds: { + approval: { + label: 'Freigabe erforderlich', + description: 'Ein Befehl wartet auf deine Freigabe oder Ablehnung.' + }, + input: { + label: 'Eingabe erforderlich', + description: 'Hermes hat eine Frage gestellt oder benötigt ein Passwort oder Geheimnis.' + }, + turnDone: { + label: 'Antwort bereit', + description: 'Ein Schritt wurde beendet, während Hermes im Hintergrund lief.' + }, + turnError: { + label: 'Schritt fehlgeschlagen', + description: 'Ein Schritt endete mit einem Fehler.' + }, + backgroundDone: { + label: 'Hintergrundaufgabe beendet', + description: 'Ein hintergrund ausgeführter Terminal-Befehl wurde abgeschlossen.' + } + }, + test: 'Test-Benachrichtigung senden', + testTitle: 'Hermes', + testBody: 'Benachrichtigungen funktionieren.', + testSent: 'Test gesendet. Wenn nichts erscheint, prüfe die Benachrichtigungsrechte deines Betriebssystems und „Nicht stören".', + testUnsupported: 'Dieses System unterstützt keine systemeigenen Benachrichtigungen.', + completionSoundTitle: 'Abschluss-Sound', + completionSoundDesc: 'Wird abgespielt, wenn ein Agent-Schritt endet. Wähle eine Voreinstellung und höre sie hier an.', + completionSoundPreview: 'Vorschau' + }, + sections: { + model: 'Modell', + chat: 'Chat', + appearance: 'Erscheinungsbild', + workspace: 'Arbeitsbereich', + safety: 'Sicherheit', + memory: 'Gedächtnis & Kontext', + voice: 'Stimme', + advanced: 'Erweitert' + }, + searchPlaceholder: { + about: 'Über Hermes Desktop', + config: 'Einstellungen suchen...', + gateway: 'Gateway-Verbindung...', + keys: 'API-Schlüssel suchen...', + mcp: 'MCP-Server suchen...', + sessions: 'Archivierte Sitzungen suchen...' + }, + modeOptions: { + light: { label: 'Hell', description: 'Helle Desktop-Oberflächen' }, + dark: { label: 'Dunkel', description: 'Blendarmer Arbeitsbereich' }, + system: { label: 'System', description: 'Dem Erscheinungsbild des Betriebssystems folgen' } + }, + appearance: { + title: 'Erscheinungsbild', + intro: + 'Dies sind nur für den Desktop geltende Anzeigeeinstellungen. Der Modus steuert die Helligkeit; das Design steuert die Akzentpalette und das Chat-Oberflächendesign.', + colorMode: 'Farbmodus', + colorModeDesc: 'Wähle einen festen Modus oder lasse Hermes deiner Systemeinstellung folgen.', + toolViewTitle: 'Werkzeug-Aufruf-Anzeige', + toolViewDesc: 'Produkt versteckt rohe Werkzeug-Daten; Technisch zeigt volle Ein-/Ausgabe.', + uiScaleTitle: 'Benutzeroberflächen-Skalierung', + uiScaleDesc: (percent: number) => + `Skaliert Text und Steuerelemente in der gesamten App. Cmd/Ctrl mit +, - und 0 funktioniert ebenfalls. Aktuell: ${percent}%.`, + translucencyTitle: 'Fenster-Transparenz', + translucencyDesc: 'Durch das gesamte Fenster hindurch deinen Desktop sehen. Nur macOS und Windows.', + embedsTitle: 'Eingebettete Medien', + embedsDesc: + 'Reiche Vorschauen laden von Drittanbieter-Seiten (YouTube, X, …). „Fragen" zeigt einen Platzhalter, bis du jedes erlaubst; „Immer" lädt sie automatisch; „Aus" behält einfache Links.', + embedsAsk: 'Fragen', + embedsAlways: 'Immer', + embedsOff: 'Aus', + embedsReset: (count: number) => `${count} erlaubte ${count === 1 ? 'Dienst' : 'Dienste'} zurücksetzen`, + product: 'Produkt', + productDesc: 'Benutzerfreundliche Werkzeug-Aktivität mit prägnanten Zusammenfassungen.', + technical: 'Technisch', + technicalDesc: 'Enthält rohe Werkzeug-Argumente/Ergebnisse und Low-Level-Details.', + themeTitle: 'Design', + themeDesc: 'Nur Desktop-Paletten. Der gewählte Modus wird darüber angewendet.', + themeProfileNote: profile => `Für das Profil ${profile} gespeichert — jedes Profil behält sein eigenes Design.`, + installTitle: 'Aus VS Code installieren', + installDesc: + 'Füge eine Marketplace-Erweiterungs-ID (z. B. dracula-theme.theme-dracula) ein, um deren Farbschema in eine Desktop-Palette umzuwandeln.', + installPlaceholder: 'herausgeber.erweiterung', + installButton: 'Installieren', + installing: 'Installiere…', + installError: 'Dieses Design konnte nicht installiert werden.', + installed: name => `„${name}" installiert.`, + removeTheme: 'Design entfernen', + importedBadge: 'Design entfernen', + pet: { + title: 'Pet', + intro: + 'Nimm ein animiertes Petdex-Maskottchen an, das über der App schwebt und auf das reagiert, was Hermes tut — läuft während Werkzeuge ausgeführt werden, feiert bei Erfolg, schmollt bei Fehlern.', + restartHint: + 'Pets brauchen einen kurzen Neustart — die laufende App startete, bevor diese Funktion hinzugefügt wurde. Beende und öffne Hermes erneut, dann komm hierher zurück.', + on: 'Ein', + off: 'Aus', + scaleTitle: 'Größe', + scaleDesc: 'Die schwebende Figur skalieren. Wirkt sofort überall.', + roamTitle: 'Umherstreifen', + roamDesc: 'Lass das Pet im Leerlauf eigenständig durch das Fenster streifen.', + chooseTitle: 'Pet auswählen', + chooseDesc: 'Die Auswahl installiert es (falls nötig) und macht es aktiv.', + searchPlaceholder: 'Pets suchen…', + unreachable: 'Die petdex-Galerie konnte nicht erreicht werden. Prüfe deine Verbindung und öffne diese Seite erneut.', + noMatch: query => `Keine Pets passen zu „${query}".`, + installedTag: 'Installiert', + generatedTag: 'Generiert', + countCapped: (cap, total) => `${cap} von ${total} werden angezeigt — tippe, um einzugrenzen.`, + count: n => `${n} ${n === 1 ? 'Pet' : 'Pets'}.`, + uninstall: name => `${name} deinstallieren`, + delete: name => `${name} löschen`, + deleteTitle: name => `${name} löschen?`, + deleteBody: 'Dies löscht das Pet endgültig – es kann nicht erneut installiert werden.', + deleteConfirm: 'Löschen', + rename: name => `${name} umbenennen`, + renameTitle: 'Pet umbenennen', + renamePlaceholder: 'Benenne dein Pet', + renameSave: 'Speichern', + exportPet: name => `${name} exportieren`, + adoptFailed: slug => `${slug} konnte nicht übernommen werden`, + uninstallFailed: slug => `${slug} konnte nicht deinstalliert werden`, + renameFailed: slug => `${slug} konnte nicht umbenannt werden`, + exportFailed: slug => `${slug} konnte nicht exportiert werden`, + noneAvailable: 'Momentan keine Pets zum Aktivieren verfügbar.', + turnOnFailed: 'Das Pet konnte nicht aktiviert werden.', + turnOffFailed: 'Das Pet konnte nicht deaktiviert werden.' + } + }, + about: { + heading: 'Hermes Desktop', + version: value => `Version ${value}`, + versionUnavailable: 'Version nicht verfügbar', + updates: 'Aktualisierungen', + checkNow: 'Jetzt prüfen', + checking: 'Prüfe…', + seeWhatsNew: 'Neuigkeiten ansehen', + updateNow: 'Jetzt aktualisieren', + releaseNotes: 'Versionshinweise', + onLatest: 'Du verwendest die neueste Version.', + installing: 'Eine Aktualisierung wird derzeit installiert.', + cantUpdate: 'Dieser Build kann sich nicht aus der App heraus aktualisieren.', + cantReach: 'Der Update-Server konnte nicht erreicht werden.', + tapCheck: 'Tippe „Jetzt prüfen", um nach Updates zu suchen.', + updateReady: count => `Eine neue Aktualisierung ist bereit (${count} ${count === 1 ? 'Änderung' : 'Änderungen'} enthalten).`, + lastChecked: age => `Zuletzt geprüft ${age}`, + justNowSuffix: '· just now', + automaticUpdates: 'Automatische Aktualisierungen', + automaticUpdatesDesc: + 'Hermes sucht im Hintergrund automatisch nach Updates und informiert dich, wenn eines bereit ist.', + branchCommit: (branch, commit) => `Zweig ${branch} · Commit ${commit}`, + never: 'nie', + justNow: 'gerade eben', + minAgo: count => `vor ${count} Min.`, + hoursAgo: count => `vor ${count} Std.`, + daysAgo: count => `vor ${count} Tagen` + }, + config: { + none: 'Keine', + noneParen: '(keine)', + notSet: 'Nicht festgelegt', + commaSeparated: 'Komma-getrennte Werte', + loading: 'Hermes-Konfiguration wird geladen...', + emptyTitle: 'Nichts zu konfigurieren', + emptyDesc: 'Dieser Abschnitt hat keine anpassbaren Einstellungen.', + failedLoad: 'Einstellungen konnten nicht geladen werden', + autosaveFailed: 'Automatisches Speichern fehlgeschlagen', + imported: 'Automatisches Speichern fehlgeschlagen', + invalidJson: 'Ungültiges Konfigurations-JSON' + }, + credentials: { + pasteKey: 'Schlüssel einfügen', + pasteLabelKey: label => `${label}-Schlüssel einfügen`, + optional: 'Optional', + enterValueFirst: 'Zuerst einen Wert eingeben.', + couldNotSave: 'Anmeldedaten konnten nicht gespeichert werden.', + remove: 'Entfernen', + getKey: 'Schlüssel abrufen', + saving: 'Speichern' + }, + envActions: { + actionsFor: label => `Aktionen für ${label}`, + credentialActions: 'Anmeldedaten-Aktionen', + docs: 'Dokumentation', + hideValue: 'Wert ausblenden', + revealValue: 'Wert anzeigen', + replace: 'Ersetzen', + set: 'Festlegen', + clear: 'Leeren' + }, + gateway: { + loading: 'Gateway-Einstellungen werden geladen...', + unavailableTitle: 'Gateway-Einstellungen nicht verfügbar', + unavailableDesc: 'Die Desktop-IPC-Brücke bietet keine Gateway-Einstellungen.', + title: 'Gateway-Verbindung', + envOverride: 'Env-Überschreibung', + intro: + 'Hermes Desktop startet standardmäßig sein eigenes lokales Gateway. Verwende ein Remote-Gateway, wenn diese App ein bereits laufendes Hermes-Backend auf einem anderen Rechner oder hinter einem vertrauenswürdigen Proxy steuern soll. Wähle unten ein Profil, um ihm einen eigenen Remote-Host zuzuweisen.', + appliesTo: 'Gilt für', + allProfiles: 'Alle Profile', + defaultConnection: 'Standardverbindung für jedes Profil ohne eigene Überschreibung.', + profileConnection: profile => + `Verbindung wird nur verwendet, wenn „${profile}" das aktive Profil ist. Setze sie auf Lokal, um die Standardeinstellung zu übernehmen.`, + envOverrideTitle: 'Umgebungsvariablen steuern diese Desktop-Sitzung.', + envOverrideDesc: + 'Setze HERMES_DESKTOP_REMOTE_URL und HERMES_DESKTOP_REMOTE_TOKEN zurück, um die gespeicherte Einstellung unten zu verwenden.', + modeTitle: 'Verbindungsmodus', + localTitle: 'Lokales Gateway', + localDesc: 'Startet ein privates Hermes-Backend auf localhost. Das ist der Standard und funktioniert offline.', + remoteTitle: 'Remote-Gateway', + remoteDesc: 'Verbindet diese Desktop-Shell mit einem Remote-Hermes-Backend.', + remoteAuthHint: + 'Gehostete Gateways nutzen OAuth oder Benutzername und Passwort; selbst gehostete können ein Sitzungs-Token nutzen.', + cloudTitle: 'Hermes Cloud', + cloudDesc: 'Melde dich einmal bei Hermes Cloud an und wähle aus den Agenten deines Kontos — keine URL zum Einfügen.', + cloudSignInTitle: 'Hermes Cloud', + cloudSignIn: 'Bei Hermes Cloud anmelden', + cloudSignedIn: 'Bei Hermes Cloud angemeldet', + cloudNeedsSignIn: 'Melde dich bei Hermes Cloud an, um die Agenten deines Kontos zu entdecken.', + cloudSignedInDesc: 'Du bist angemeldet. Wähle unten einen Agenten; die Sitzung wird automatisch aktualisiert.', + cloudAgentsTitle: 'Deine Agenten', + cloudOrgPickerTitle: 'Eine Organisation wählen', + cloudOrgSelect: 'Auswählen', + cloudOrgChange: 'Organisation ändern', + cloudOrgRole: role => `Rolle: ${role}`, + cloudLoadingAgents: 'Deine Agenten werden geladen…', + cloudNoAgents: { + before: 'No agents found on this account. Create one in the', + linkText: 'Nous-Portal', + after: ', dann aktualisieren.' + }, + cloudRefresh: 'Aktualisieren', + cloudConnect: 'Verbinden', + cloudConnecting: 'Verbinde…', + cloudDiscoverFailed: 'Deine Hermes-Cloud-Agenten konnten nicht geladen werden', + cloudConnectFailed: 'Verbindung zu diesem Agenten fehlgeschlagen', + cloudSignInFailed: 'Hermes-Cloud-Anmeldung fehlgeschlagen', + cloudSignedOutTitle: 'Aus Hermes Cloud abgemeldet', + cloudSignedOutMessage: 'Die Hermes-Cloud-Sitzung wurde gelöscht.', + cloudConnectedTitle: 'Verbunden', + cloudConnectedPill: 'Verbunden', + cloudConnectedTo: name => `Verbunden mit ${name}.`, + cloudAgentProvisioning: 'Bereitstellung…', + cloudStatusLabel: status => `Status: ${status}`, + remoteUrlTitle: 'Remote-URL', + remoteUrlDesc: 'Basis-URL für das Remote-Dashboard-Backend. Pfad-Präfixe werden unterstützt, zum Beispiel /hermes.', + probing: 'Es wird geprüft, wie sich dieses Gateway authentifiziert…', + probeError: 'Dieses Gateway konnte noch nicht erreicht werden. Prüfe die URL — die Authentifizierungsmethode erscheint, sobald es antwortet.', + signedIn: 'Angemeldet', + signIn: 'Anmelden', + signOut: 'Abmelden', + signInWith: provider => `Mit ${provider} anmelden`, + authTitle: 'Authentifizierung', + authSignedInPassword: + 'Dieses Gateway verwendet Benutzername und Passwort. Du bist angemeldet; die Sitzung wird automatisch aktualisiert.', + authSignedInOauth: 'Dieses Gateway verwendet OAuth. Du bist angemeldet; die Sitzung wird automatisch aktualisiert.', + authNeedsPassword: 'Dieses Gateway verwendet Benutzername und Passwort. Melde dich an, um diese Desktop-App zu autorisieren.', + authNeedsOauth: provider => `Dieses Gateway verwendet OAuth. Melde dich mit ${provider} an, um diese Desktop-App zu autorisieren.`, + tokenTitle: 'Sitzungs-Token', + tokenDesc: 'Das Dashboard-Sitzungs-Token für REST- und WebSocket-Zugriff. Leer lassen, um das gespeicherte Token beizubehalten.', + existingToken: value => `Vorhandenes Token ${value}`, + savedToken: 'Gespeichert', + pasteSessionToken: 'Sitzungs-Token einfügen', + testRemote: 'Remote testen', + saveForRestart: 'Für den nächsten Neustart speichern', + saveAndReconnect: 'Speichern und neu verbinden', + diagnostics: 'Diagnose', + diagnosticsDesc: 'desktop.log im Datei-Manager anzeigen — nützlich, wenn das Gateway nicht startet.', + openLogs: 'Protokolle öffnen', + incompleteTitle: 'Remote-Gateway unvollständig', + incompleteSignIn: 'Gib eine Remote-URL ein und melde dich an, bevor du zu Remote wechselst.', + incompleteToken: 'Gib eine Remote-URL und ein Sitzungs-Token ein, bevor du zu Remote wechselst.', + incompleteSignInTest: 'Gib eine Remote-URL ein und melde dich an, bevor du testest.', + incompleteTokenTest: 'Gib eine Remote-URL und ein Sitzungs-Token ein, bevor du testest.', + enterUrlFirst: 'Gib zuerst eine Remote-URL ein.', + restartingTitle: 'Gateway-Verbindung wird neu gestartet', + savedTitle: 'Gateway-Einstellungen gespeichert', + restartingMessage: 'Hermes Desktop verbindet sich mit den gespeicherten Einstellungen neu — die Shell bleibt offen.', + savedMessage: 'Für den nächsten Neustart gespeichert.', + connectedTo: (baseUrl, version) => `Connected to ${baseUrl}${version ?` · Hermes ${version}` : ''}`, + reachableTitle: 'Remote-Gateway erreichbar', + signedOutTitle: 'Abgemeldet', + signedOutMessage: 'Die Remote-Gateway-Sitzung wurde gelöscht.', + failedLoad: 'Gateway-Einstellungen konnten nicht geladen werden', + signInFailed: 'Anmeldung fehlgeschlagen', + signOutFailed: 'Abmeldung fehlgeschlagen', + testFailed: 'Remote-Gateway-Test fehlgeschlagen', + applyFailed: 'Gateway-Einstellungen konnten nicht angewendet werden', + saveFailed: 'Gateway-Einstellungen konnten nicht gespeichert werden' + }, + keys: { + loading: 'API-Schlüssel und Anmeldedaten werden geladen...', + failedLoad: 'API-Schlüssel konnten nicht geladen werden', + empty: 'In dieser Kategorie ist noch nichts konfiguriert.' + }, + mcp: { + loading: 'MCP-Server werden geladen...', + failedLoad: 'MCP-Konfiguration konnte nicht geladen werden', + nameRequiredTitle: 'Name erforderlich', + nameRequiredMessage: 'Gib diesem MCP-Server einen Konfigurationsschlüssel.', + objectRequired: 'Server-Konfiguration muss ein JSON-Objekt sein', + invalidJson: 'Ungültiges MCP-JSON', + saveFailed: 'Speichern fehlgeschlagen', + removeFailed: 'Entfernen fehlgeschlagen', + gatewayUnavailableTitle: 'Gateway nicht verfügbar', + gatewayUnavailableMessage: 'Verbinde das Gateway erneut, bevor du MCP neu lädst.', + reloadedTitle: 'MCP-Werkzeuge neu geladen', + reloadedMessage: 'Neue Werkzeug-Schemas gelten für neue Schritte.', + reloadFailed: 'MCP-Neuladen fehlgeschlagen', + savedTitle: 'MCP-Server gespeichert', + savedMessage: name => `${name} gilt nach dem MCP-Neuladen.`, + newServer: 'Neuer Server', + reload: 'MCP neu laden', + reloading: 'Lade neu...', + emptyTitle: 'Keine MCP-Server', + emptyDesc: 'Füge einen stdio- oder HTTP-Server hinzu, um MCP-Werkzeuge bereitzustellen.', + disabled: 'Deaktiviert', + editServer: 'Server bearbeiten', + name: 'Name', + serverJson: 'Server-JSON', + remove: 'Entfernen', + saveServer: 'Server speichern', + test: 'Verbindung testen', + testing: 'Teste...', + testOk: count => `Verbunden — ${count} ${count === 1 ? 'Werkzeug' : 'Werkzeuge'} verfügbar`, + testFailed: 'Verbindung fehlgeschlagen', + enableServer: name => `${name} aktivieren`, + disableServer: name => `${name} deaktivieren`, + serverEnabled: name => `${name} aktiviert — gilt für neue Sitzungen.`, + serverDisabled: name => `${name} deaktiviert — gilt für neue Sitzungen.`, + toggleFailed: name => `Umschalten von ${name} fehlgeschlagen`, + tabServers: 'Server', + tabCatalog: 'Katalog', + catalogLoading: 'MCP-Katalog wird geladen...', + catalogLoadFailed: 'MCP-Katalog konnte nicht geladen werden', + catalogEmpty: 'Keine Katalogeinträge verfügbar.', + catalogInstalled: 'Installiert', + catalogEnabled: 'Aktiviert', + catalogNeedsInstall: 'Build erforderlich', + catalogInstall: 'Installieren', + catalogInstalling: 'Installiere...', + catalogInstallStarted: name => `Installiere ${name}... gilt für neue Sitzungen, sobald fertig.`, + catalogInstallFailed: name => `Installation von ${name} fehlgeschlagen`, + catalogEnvPrompt: name => `${name} benötigt Anmeldedaten`, + catalogEnvRequired: 'Fülle die erforderlichen Werte vor der Installation aus.', + capabilitySummary: (tools, prompts, resources) => + `${[`${tools} tools`, ...(prompts ? [`${prompts} prompts`] : []), ...(resources ? [`${resources} resources`] : [])].join(', ')} enabled`, + statusConnecting: 'Verbinde…', + statusNeedsAuth: 'Authentifizierung erforderlich', + statusError: 'Fehler', + statusOff: 'Aus', + allServers: 'Alle Server', + authenticatedTitle: 'Authentifiziert', + authenticatedMessage: (server, count) => `${server}: ${count} Werkzeuge`, + waitingForBrowser: 'Warte auf Browser…', + authenticate: 'Authentifizieren', + unsavedConnect: 'Nicht gespeichert — speichere mcp.json, um zu verbinden.', + enableTool: tool => `${tool} aktivieren`, + disableTool: tool => `${tool} deaktivieren`, + noOutput: 'Noch keine Ausgabe.' + }, + model: { + loading: 'Modellkonfiguration wird geladen...', + appliesDesc: 'Gilt für neue Sitzungen. Nutze die Modell-Auswahl in der Eingabe, um den aktiven Chat live umzuschalten.', + provider: 'Anbieter', + model: 'Modell', + applying: 'Wende an...', + defaultsLabel: 'Standardwerte', + reasoning: 'Schlussfolgern', + reasoningOff: 'Aus', + defaultsFailed: 'Modell-Standardwerte konnten nicht gespeichert werden', + auxiliaryTitle: 'Hilfsmodelle', + resetAllToMain: 'Alle auf Haupt zurücksetzen', + auxiliaryDesc: 'Hilfsaufgaben laufen standardmäßig auf dem Hauptmodell. Weise einer Aufgabe ein eigenes Modell zu, um sie zu überschreiben.', + setToMain: 'Auf Haupt gesetzt', + change: 'Ändern', + autoUseMain: 'auto · Hauptmodell verwenden', + providerDefault: '(Anbieter-Standard)', + fallbackAdd: 'Fallback hinzufügen', + fallbackEmpty: 'Keine Fallback-Modelle — das Standardmodell wird verwendet, sofern es nicht fehlschlägt.', + notInCatalog: 'ist nicht in der Modellliste dieses Anbieters — Aufrufe können auf ein Backup ausweichen.', + tasks: { + vision: { label: 'Vision', hint: 'Image analysis' }, + web_extract: { label: 'Web-Extraktion', hint: 'Page summarization' }, + compression: { label: 'Komprimierung', hint: 'Context compaction' }, + skills_hub: { label: 'Fähigkeiten-Hub', hint: 'Skill search' }, + approval: { label: 'Freigabe', hint: 'Smart auto-approve' }, + mcp: { label: 'MCP', hint: 'MCP tool routing' }, + title_generation: { label: 'Titel-Generierung', hint: 'Session titles' }, + curator: { label: 'Kurator', hint: 'Skill-usage review' } + } + }, + providers: { + connectAccount: 'Konto verbinden', + haveApiKey: 'Stattdessen einen API-Schlüssel?', + intro: + 'Mit einem Abo anmelden — kein API-Schlüssel zum Kopieren. Hermes führt die Browser-Anmeldung für dich direkt hier in der App aus.', + connected: 'Verbunden', + collapse: 'Einklappen', + connectAnother: 'Weiteren Anbieter verbinden', + otherProviders: 'Andere Anbieter', + disconnect: 'Trennen', + disconnectInTerminal: 'Trennen (führt den Entfernungsbefehl im Terminal aus)', + removeConfirm: provider => `${provider} entfernen?`, + removeExternalGeneric: provider => `${provider} wird von seiner eigenen CLI verwaltet — entferne es dort.`, + removeKeyManaged: provider => `${provider} ist über einen API-Schlüssel konfiguriert. Entferne es bei den API-Schlüsseln.`, + removeTerminalConfirm: (provider, command) => + `${provider} trennen? Dies führt „${command}" im Terminal aus, um die Anmeldedaten zu löschen.`, + removeTerminalRunning: provider => `Führe ${provider}-Trennung im Terminal aus…`, + removedTitle: 'Konto entfernt', + removedMessage: provider => `${provider} wurde entfernt.`, + failedRemove: provider => `${provider} konnte nicht entfernt werden`, + noProviderKeys: 'Keine Anbieter-API-Schlüssel verfügbar.', + searchKeys: 'Anbieter suchen…', + noKeysMatch: 'Keine Anbieter passen zu deiner Suche.', + loading: 'Anbieter werden geladen...' + }, + sessions: { + loading: 'Archivierte Sitzungen werden geladen…', + archivedTitle: 'Archivierte Sitzungen', + archivedIntro: + 'Archivierte Chats sind in der Seitenleiste ausgeblendet, behalten aber alle ihre Nachrichten. Strg/⌘-Klick auf einen Chat in der Seitenleiste archiviert ihn.', + emptyArchivedTitle: 'Nichts archiviert', + emptyArchivedDesc: 'Archiviere einen Chat, um ihn hier auszublenden.', + unarchive: 'Aus Archiv holen', + deletePermanently: 'Dauerhaft löschen', + messages: count => `${count} ${count === 1 ? 'Nachricht' : 'Nachrichten'}`, + restored: 'Wiederhergestellt', + deleteConfirm: title => `„${title}" dauerhaft löschen? Dies kann nicht rückgängig gemacht werden.`, + defaultDirTitle: 'Standard-Projektverzeichnis', + defaultDirDesc: + 'Neue Sitzungen starten in diesem Ordner, sofern du keinen anderen wählst. Lass es leer, um dein Home-Verzeichnis zu verwenden.', + defaultDirUpdated: 'Standard-Projektverzeichnis aktualisiert — starte einen neuen Chat (Strg/⌘+N), damit es wirksam wird', + defaultsTo: label => `Standardmäßig ${label}.`, + change: 'Ändern', + choose: 'Auswählen', + clear: 'Leeren', + notSet: 'Nicht festgelegt', + failedLoad: 'Archivierte Sitzungen konnten nicht geladen werden', + unarchiveFailed: 'Aus dem Archiv holen fehlgeschlagen', + deleteFailed: 'Löschen fehlgeschlagen', + updateDirFailed: 'Standardverzeichnis konnte nicht aktualisiert werden', + clearDirFailed: 'Standardverzeichnis konnte nicht geleert werden' + }, + toolsets: { + loadingConfig: 'Konfiguration wird geladen', + savedTitle: 'Anmeldedaten gespeichert', + savedMessage: key => `${key} aktualisiert.`, + removedTitle: 'Anmeldedaten entfernt', + removedMessage: key => `${key} entfernt.`, + failedSave: key => `Speichern von ${key} fehlgeschlagen`, + failedRemove: key => `Entfernen von ${key} fehlgeschlagen`, + failedReveal: key => `Anzeigen von ${key} fehlgeschlagen`, + removeConfirm: key => `${key} aus .env entfernen?`, + set: 'Festlegen', + notSet: 'Nicht festgelegt', + selectedTitle: 'Anbieter ausgewählt', + selectedMessage: provider => `${provider} ist jetzt aktiv.`, + failedSelect: provider => `Auswahl von ${provider} fehlgeschlagen`, + failedLoad: 'Werkzeugkonfiguration konnte nicht geladen werden', + noProviderOptions: 'Dieses Werkzeugset hat keine Anbieteroptionen — aktiviere es, und es funktioniert mit deiner aktuellen Einrichtung.', + noProviders: 'Für dieses Werkzeugset sind derzeit keine Anbieter verfügbar.', + ready: 'Bereit', + nousIncluded: 'In einem Nous-Abo enthalten — bei Nous-Portal anmelden, um zu aktivieren.', + noApiKeyRequired: 'Kein API-Schlüssel erforderlich.', + postSetupHint: step => + `Dieses Backend benötigt eine einmalige Installation (${step}). Läuft auf diesem Rechner — kann einige Minuten dauern.`, + postSetupRun: 'Einrichtung ausführen', + postSetupRunning: 'Installiere…', + postSetupStarting: 'Starte…', + postSetupCompleteTitle: 'Einrichtung abgeschlossen', + postSetupCompleteMessage: step => `${step} installiert.`, + postSetupErrorTitle: 'Einrichtung mit Fehlern abgeschlossen', + postSetupErrorMessage: step => `Prüfe das ${step}-Protokoll.`, + postSetupFailed: step => `Ausführung der ${step}-Einrichtung fehlgeschlagen`, + loadingModels: 'Modellkatalog wird geladen...', + modelSectionTitle: 'Modell', + modelCount: count => `${count} ${count === 1 ? 'Modell' : 'Modelle'}`, + modelInUse: 'In Verwendung', + modelDefault: 'Standard', + modelInactiveHint: 'Wähle zuerst dieses Backend, um sein Modell zu ändern.', + modelSelectedTitle: 'Modell ausgewählt', + modelSelectedMessage: model => `${model} gilt für neue Sitzungen.`, + failedSelectModel: model => `Auswahl von ${model} fehlgeschlagen` + } + }, + + skills: { + tabSkills: 'Fähigkeiten', + tabToolsets: 'Werkzeuge', + tabMcp: 'MCP', + tabHub: 'Hub durchsuchen', + all: 'Alle', + searchSkills: 'Fähigkeiten suchen...', + searchToolsets: 'Werkzeuge suchen...', + refresh: 'Fähigkeiten aktualisieren', + refreshing: 'Fähigkeiten werden aktualisiert', + loading: 'Funktionen werden geladen...', + noSkillsTitle: 'Keine Fähigkeiten gefunden', + noSkillsDesc: 'Versuche eine breitere Suche oder eine andere Kategorie.', + noToolsetsTitle: 'Keine Werkzeugsätze gefunden', + noToolsetsDesc: 'Versuche eine breitere Suchanfrage.', + noDescription: 'Keine Beschreibung.', + configured: 'Konfiguriert', + needsKeys: 'Schlüssel erforderlich', + toolsetsEnabled: (enabled, total) => `${enabled}/${total} Werkzeugsätze aktiviert`, + configureToolset: label => `${label} konfigurieren`, + toggleToolset: label => `${label}-Werkzeugset umschalten`, + skillsLoadFailed: 'Fähigkeiten konnten nicht geladen werden', + toolsetsRefreshFailed: 'Werkzeugsätze konnten nicht aktualisiert werden', + skillEnabled: 'Fähigkeit aktiviert', + skillDisabled: 'Fähigkeit deaktiviert', + toolsetEnabled: 'Werkzeugset aktiviert', + toolsetDisabled: 'Werkzeugset deaktiviert', + appliesToNewSessions: name => `${name} gilt für neue Sitzungen.`, + failedToUpdate: name => `Aktualisierung von ${name} fehlgeschlagen`, + sortMostUsed: 'Am häufigsten genutzt', + sortAlpha: 'A–Z', + sortMostUsedDesc: '↓ Am häufigsten genutzt', + sortLeastUsedAsc: '↑ Am wenigsten genutzt', + enableAll: 'Alle aktivieren', + disableAll: 'Alle deaktivieren', + disableUnused: 'Unbenutzte deaktivieren', + bulkUpdated: count => `${count} ${count === 1 ? 'Element' : 'Elemente'} für neue Sitzungen aktualisiert.`, + bulkNoChange: 'Nichts zu ändern.', + usageCount: count => `${count}× genutzt`, + provenance: { + agent: 'Gelernt', + bundled: 'Eingebaut', + hub: 'Hub' + }, + emptyNoneFound: noun => `Kein ${noun} gefunden`, + emptyNothingMatches: query => `Nichts passt zu „${query}".`, + emptyNoneAvailable: noun => `Noch kein ${noun} verfügbar.`, + changesApplyNewSessions: 'Änderungen gelten für neue Sitzungen.', + skillUpdated: 'Fähigkeit aktualisiert', + edit: 'Bearbeiten', + archive: 'Archivieren', + skillArchivedTitle: 'Fähigkeit archiviert', + skillArchivedMessage: 'Wiederherstellbar über hermes curator restore.', + hub: { + searchPlaceholder: 'Den Fähigkeiten-Hub durchsuchen', + search: 'Suche', + searching: 'Suche...', + connectingHubs: 'Verbindung zu Fähigkeiten-Hubs wird hergestellt...', + connectedHubs: 'Verbundene Hubs:', + featured: 'Empfohlene Fähigkeiten', + landingHint: + 'Durchsuche den Hub, um installierbare Fähigkeiten aus dem offiziellen Index, GitHub und Community-Quellen zu finden.', + noResults: 'Keine passenden Fähigkeiten im Hub gefunden.', + resultCount: (count, ms) => `${count} result${count === 1 ? '' : 's'}${ms !== null ?` in ${ms}ms` : ''}`, + timedOut: sources => `Zeitüberschreitung: ${sources}`, + installed: 'Installiert', + install: 'Installieren', + installing: 'Installiere...', + uninstall: 'Deinstallieren', + uninstalling: 'Deinstalliere...', + updateAll: 'Aktualisierung installiert', + updating: 'Aktualisiere...', + preview: 'Vorschau', + scan: 'Scannen', + scanning: 'Scanne...', + close: 'Schließen', + files: 'Dateien', + noReadme: 'Diese Fähigkeit hat keine SKILL.md-Vorschau.', + trust: { + builtin: 'Eingebaut', + trusted: 'Vertrauenswürdig', + community: 'Community' + }, + verdictSafe: 'Sicher', + verdictCaution: 'Vorsicht', + verdictDangerous: 'Gefährlich', + policyAllow: 'Installation erlaubt', + policyAsk: 'Vor der Installation prüfen', + policyBlock: 'Installation durch Richtlinie blockiert', + findings: count => `${count} ${count === 1 ? 'Befund' : 'Befunde'}`, + noFindings: 'Keine Sicherheitsbefunde.', + installStarted: name => `Installiere ${name}...`, + uninstallStarted: name => `Deinstalliere ${name}...`, + updateStarted: 'Installierte Fähigkeiten werden aktualisiert...', + actionFailed: 'Fähigkeiten-Aktion fehlgeschlagen', + actionLog: 'Aktionsprotokoll', + loadFailed: 'Fähigkeiten-Hub konnte nicht geladen werden', + previewFailed: 'Fähigkeiten-Vorschau fehlgeschlagen', + scanFailed: 'Sicherheitsscan fehlgeschlagen', + searchFailed: 'Hub-Suche fehlgeschlagen' + } + }, + + starmap: { + title: 'Gedächtnisgraph', + subtitle: (nodes, clusters) => `${nodes} Fähigkeiten in ${clusters} Kategorien`, + close: 'Gedächtnisgraph schließen', + refresh: 'Aktualisieren', + memory: 'Gedächtnis', + filterAll: 'Alle', + filterUsed: 'Genutzt', + filterLearned: 'Gelernt', + viewGraph: 'Graph', + loadFailed: 'Gedächtnisgraph konnte nicht geladen werden', + loading: 'Lädt…', + emptyTitle: 'Noch nichts gelernt', + emptyDesc: 'Während Hermes Fähigkeiten und Erinnerungen für deine Arbeit aufbaut, erscheinen sie hier.', + share: 'Karte teilen', + shareHint: + 'Kopiere den Code, um diese Karte zu teilen, oder füge einen ein, um sie zu laden. Sie enthält nur das Layout, nicht dein Gedächtnis oder den Fähigkeiten-Text.', + shareTitle: 'Karte importieren / exportieren', + sharePlaceholder: 'Karten-Code einfügen…', + copy: 'Karten-Code kopieren', + copied: 'Kopiert!', + importMap: 'Kopiert!', + importBtn: 'Kopiert!', + importEmpty: 'Kopiert!', + importSuccess: nodes => `Kopiert!`, + importedBadge: 'Kopiert!', + resetToMine: 'Zurück zu meiner Karte' + }, + agents: { + close: 'Agenten schließen', + title: 'Erzeugungsbaum', + subtitle: 'Live-Aktivität von Subagenten für den aktuellen Schritt.', + emptyTitle: 'Keine Live-Subagenten', + emptyDesc: 'Wenn ein Schritt Arbeit delegiert, streamen untergeordnete Agenten ihren Fortschritt hier.', + running: 'Läuft', + failed: 'Fehlgeschlagen', + done: 'Fertig', + streaming: 'Streamt', + files: 'Dateien', + moreFiles: count => `+${count} weitere Dateien`, + delegation: index => `Delegation ${index}`, + workers: count => `${count} Worker`, + workersActive: count => `${count} aktiv`, + agentsCount: count => `${count} ${count === 1 ? 'Agent' : 'Agenten'}`, + activeCount: count => `${count} aktiv`, + failedCount: count => `${count} fehlgeschlagen`, + toolsCount: count => `${count} Werkzeuge`, + filesCount: count => `${count} Dateien`, + updatedAgo: age => `Aktualisiert ${age}`, + ageNow: 'Jetzt', + ageSeconds: seconds => `vor ${seconds}s`, + ageMinutes: minutes => `vor ${minutes}m`, + ageHours: hours => `vor ${hours}h`, + durationSeconds: seconds => `${seconds}s`, + durationMinutes: (minutes, seconds) => `${minutes}m ${seconds}s`, + tokens: value => `${value} Tokens` + }, + + commandCenter: { + close: 'Befehlszentrum schließen', + paletteTitle: 'Befehlspalette', + back: 'Zurück', + searchPlaceholder: 'Sitzungen, Ansichten und Aktionen suchen', + goTo: 'Gehe zu', + goToSession: 'Zu Sitzung gehen', + branches: 'Zweige', + startInBranch: branch => `Neues Gespräch in ${branch}`, + commandCenter: 'Befehlszentrum', + appearance: 'Erscheinungsbild', + settings: 'Einstellungen', + changeTheme: 'Design ändern', + changeColorMode: 'Farbmodus ändern…', + pets: { + title: 'Pets', + placeholder: 'Pets suchen…', + loading: 'Petdex-Galerie wird geladen…', + error: 'Die Petdex-Galerie konnte nicht erreicht werden.', + staleBackend: 'Starte Hermes neu, um Pets zu nutzen — das Backend stammt aus einer Zeit vor dieser Funktion.', + empty: 'Keine passenden Pets.', + turnOff: 'Deaktivieren', + turnOn: 'Aktivieren', + installed: 'Installiert', + generatedTag: 'Generiert', + adoptFailed: 'Dieses Pet konnte nicht übernommen werden.', + toggleFailed: 'Das Pet konnte nicht umgeschaltet werden.', + noneAvailable: 'Keine Pets verfügbar — wähle unten eines zur Installation.' + }, + generatePet: { + title: 'Ein Pet generieren', + placeholder: 'Beschreibe ein Pet zum Generieren…', + promptHint: 'Gib eine Beschreibung ein und drücke Enter, um vier Varianten zu entwerfen.', + readyHint: 'Drücke Enter, um vier Varianten aus deiner Beschreibung zu entwerfen.', + generate: 'Generieren', + generating: 'Generiere…', + retry: 'Erneut versuchen', + hatch: 'Schlüpfen', + spawning: 'Erzeuge…', + hatching: 'Dein Pet schlüpft…', + hatchingSub: 'Zum Leben erwecken…', + hatched: 'Es ist geschlüpft!', + hatchRow: (_state, done, total) => `Skizziere Frame ${done} von ${total}…`, + hatchComposing: 'Füge es zusammen…', + hatchSaving: 'Fast geschafft…', + namePlaceholder: 'Benenne dein Pet', + staleBackend: 'Aktualisiere Hermes, um Pets zu generieren.', + backgroundHint: 'Du kannst das schließen — Hermes benachrichtigt dich, wenn es fertig ist.', + slowProviderHint: 'Dies kann mehrere Minuten dauern', + remix: 'Neu mischen', + remixConfirmTitle: 'Diesen Look neu mischen?', + remixConfirmBody: + 'Dies erzeugt einen neuen Satz Entwürfe, der diesen als Ausgangspunkt nutzt. Das kann mehrere Minuten dauern.', + genericError: 'Generierung fehlgeschlagen — versuche es erneut oder wähle einen Vorschlag.', + referenceImageTooLarge: 'Referenzbild ist zu groß. Nutze eines unter 16 MB.', + referenceImageInvalid: 'Dieses Referenzbild konnte nicht gelesen werden. Versuche eine PNG-, JPG-, WebP- oder GIF-Datei.', + adopt: 'Übernehmen', + startOver: 'Neu beginnen' + }, + installTheme: { + title: 'Design installieren…', + pageTitle: 'Design installieren', + placeholder: 'VS Code Marketplace durchsuchen...', + loading: 'Marketplace wird durchsucht...', + error: 'Der Marketplace konnte nicht erreicht werden.', + empty: 'Keine passenden Designs.', + install: 'Installieren', + installing: 'Installiere...', + installed: 'Installiert', + installs: count => `${count} Installationen` + }, + settingsFields: 'Einstellungsfelder', + mcpServers: 'MCP-Server', + archivedChats: 'Archivierte Chats', + sections: { maintenance: 'Wartung', sessions: 'Sessions', system: 'System', usage: 'Usage' }, + sectionDescriptions: { + maintenance: 'Diagnose, Backups, Kurator und Gedächtnisdaten', + sessions: 'Sitzungen suchen und verwalten', + system: 'Status, Protokolle und Systemaktionen', + usage: 'Token, Kosten und Fähigkeitsaktivität im Zeitverlauf' + }, + nav: { + newChat: { title: 'Neue Sitzung', detail: 'Start a fresh session' }, + settings: { title: 'Einstellungen', detail: 'Configure Hermes desktop' }, + skills: { title: 'Funktionen', detail: 'Skills, tools, and MCP servers' }, + messaging: { title: 'Messaging', detail: 'Set up Telegram, Slack, Discord, and more' }, + artifacts: { title: 'Artefakte', detail: 'Browse generated outputs' } + }, + sectionEntries: { + sessions: { title: 'Sitzungspanel', detail: 'Search, pin, and manage sessions' }, + system: { title: 'Systempanel', detail: 'Gateway status, logs, restart/update' }, + usage: { title: 'Nutzungspanel', detail: 'Token, cost, and skill activity' } + }, + providerNavigate: 'Navigieren', + providerSessions: 'Sitzungen', + refresh: 'Aktualisieren', + refreshing: 'Aktualisiere...', + noResults: 'Keine passenden Ergebnisse gefunden.', + pinSession: 'Sitzung anheften', + unpinSession: 'Sitzung lösen', + exportSession: 'Sitzung exportieren', + deleteSession: 'Sitzung löschen', + noSessions: 'Noch keine Sitzungen.', + gatewayRunning: 'Messaging-Gateway läuft', + gatewayStopped: 'Messaging-Gateway gestoppt', + hermesActiveSessions: (version, count) => `Hermes ${version} · Aktive Sitzungen ${count}`, + restartGateway: 'Gateway neu starten', + gatewayRestartFailed: 'Gateway-Neustart fehlgeschlagen.', + updateHermes: 'Hermes aktualisieren', + actionRunning: 'Läuft', + actionDone: 'Fertig', + actionFailed: 'Fehlgeschlagen', + actionStartedWaiting: 'Aktion gestartet, warte auf Status...', + loadingStatus: 'Status wird geladen...', + recentLogs: 'Letzte Protokolle', + noLogs: 'Noch keine Protokolle geladen.', + days: count => `${count}d`, + statSessions: 'Sitzungen', + statApiCalls: 'API-Aufrufe', + statTokens: 'Token ein/aus', + statCost: 'Geschätzte Kosten', + actualCost: cost => `Tatsächlich ${cost}`, + loadingUsage: 'Nutzung wird geladen...', + noUsage: period => `Keine Nutzung in den letzten ${period} Tagen.`, + retry: 'Erneut versuchen', + dailyTokens: 'Tägliche Token', + input: 'Eingabe', + output: 'Ausgabe', + noDailyActivity: 'Keine tägliche Aktivität.', + topModels: 'Top-Modelle', + noModelUsage: 'Noch keine Modellnutzung.', + topSkills: 'Top-Fähigkeiten', + noSkillActivity: 'Noch keine Fähigkeitsaktivität.', + actions: count => `${count} Aktionen`, + logFile: 'Protokolldatei', + logLevel: 'Ebene', + logSearchPlaceholder: 'Protokollzeilen filtern...', + maintenance: { + runOps: 'Diagnose', + doctor: 'Doctor ausführen', + doctorDesc: 'Gesundheitscheck von Installation, Konfiguration und Anbietern', + securityAudit: 'Sicherheitsaudit', + securityAuditDesc: 'Konfiguration und Fähigkeiten auf riskante Einstellungen scannen', + backup: 'Backup erstellen', + backupDesc: 'Konfiguration, Erinnerungen, Fähigkeiten und Sitzungen zippen', + debugShare: 'Debug-Teilen', + debugShareDesc: 'Bereinigten Bericht + Protokolle hochladen, teilbare Links erhalten (auto-löschen nach 6h)', + debugShareRunning: 'Debug-Bericht wird hochgeladen...', + debugShareLinks: 'Links teilen', + debugShareFailed: 'Debug-Teilen fehlgeschlagen', + copyLink: 'Link kopieren', + linkCopied: 'Link kopiert', + curator: 'Fähigkeiten-Kurator', + curatorDesc: 'Hintergrundprüfung, die veraltete, von Agenten erstellte Fähigkeiten archiviert', + curatorPaused: 'Pausiert', + curatorActive: 'Aktiv', + curatorDisabled: 'Deaktiviert', + curatorLastRun: when => `Letzter Lauf ${when}`, + curatorNeverRan: 'Nie ausgeführt', + pause: 'Pausieren', + resume: 'Fortsetzen', + runNow: 'Jetzt ausführen', + memoryData: 'Gedächtnisdaten', + memoryDataDesc: 'Eingebaute Gedächtnisdateien, die in jede Sitzung eingefügt werden', + memoryProvider: name => `Aktiver Anbieter: ${name}`, + builtinMemory: 'Eingebaut', + memoryFile: 'Agenten-Gedächtnis (MEMORY.md)', + userFile: 'Benutzerprofil (USER.md)', + bytes: size => size, + empty: 'Leer', + resetMemory: 'Gedächtnis zurücksetzen', + resetUser: 'Profil zurücksetzen', + resetAll: 'Beide zurücksetzen', + resetConfirm: target => `${target} löschen? Dies kann nicht rückgängig gemacht werden.`, + resetDone: files => `${files} gelöscht.`, + resetFailed: 'Gedächtnis-Zurücksetzen fehlgeschlagen', + actionStarted: name => `${name} gestartet — Protokoll wird verfolgt...`, + actionFailed: name => `${name} konnte nicht gestartet werden`, + running: 'Läuft...', + viewLog: 'Aktionsprotokoll' + } + }, + + messaging: { + search: 'Messaging suchen...', + loading: 'Messaging-Plattformen werden geladen...', + loadFailed: 'Messaging-Plattformen konnten nicht geladen werden', + states: { + connected: 'Verbunden', + connecting: 'Verbinde…', + disabled: 'Deaktiviert', + fatal: 'Fehler', + gateway_stopped: 'Messaging-Gateway gestoppt', + not_configured: 'Einrichtung erforderlich', + pending_restart: 'Neustart erforderlich', + retrying: 'Erneuter Versuch', + startup_failed: 'Start fehlgeschlagen' + }, + unknown: 'Unbekannt', + hintPendingRestart: 'Starte das Gateway über die Statusleiste neu, um diese Änderung anzuwenden.', + hintGatewayStopped: 'Starte das Gateway über die Statusleiste, um zu verbinden.', + credentialsSet: 'Anmeldedaten festgelegt', + needsSetup: 'Einrichtung erforderlich', + gatewayStopped: 'Messaging-Gateway gestoppt', + getCredentials: 'Deine Anmeldedaten abrufen', + openSetupGuide: 'Einrichtungsanleitung öffnen', + required: 'Erforderlich', + recommended: 'Empfohlen', + advanced: count => `Erweitert (${count})`, + noTokenNeeded: 'Diese Plattform benötigt hier kein Token. Nutze die obige Einrichtungsanleitung und aktiviere sie dann unten.', + enabled: 'Aktiviert', + disabled: 'Deaktiviert', + unsavedChanges: 'Nicht gespeicherte Änderungen', + saving: 'Speichere...', + saveChanges: 'Änderungen speichern', + saved: 'Gespeichert', + replaceValue: 'Aktuellen Wert ersetzen', + openDocs: 'Dokumentation öffnen', + clearField: key => `${key} leeren`, + enableAria: name => `${name} aktivieren`, + disableAria: name => `${name} deaktivieren`, + platformEnabled: name => `${name} aktiviert`, + platformDisabled: name => `${name} deaktiviert`, + restartToApply: 'Diese Änderung wirkt nach einem Gateway-Neustart.', + setupSaved: name => `${name}-Einrichtung gespeichert`, + restartToReconnect: 'Neue Anmeldedaten wirken nach einem Gateway-Neustart.', + keyCleared: key => `${key} geleert`, + setupUpdated: name => `${name}-Einrichtung wurde aktualisiert.`, + failedUpdate: name => `Aktualisierung von ${name} fehlgeschlagen`, + failedSave: name => `Speichern von ${name} fehlgeschlagen`, + failedClear: key => `Leeren von ${key} fehlgeschlagen`, + fieldCopy: { + TELEGRAM_BOT_TOKEN: { + label: 'Bot-Token', + help: 'Erstelle einen Bot bei @BotFather und füge dann den von ihm erhaltenen Token ein.', + placeholder: 'Telegram-Bot-Token einfügen' + }, + TELEGRAM_ALLOWED_USERS: { + label: 'Erlaubte Telegram-Benutzer-IDs', + help: 'Empfohlen. Komma-getrennte numerische IDs von @userinfobot. Ohne dies kann jeder deinen Bot per DM erreichen.' + }, + TELEGRAM_PROXY: { label: 'Proxy-URL', help: 'Only needed on networks where Telegram is blocked.' }, + DISCORD_BOT_TOKEN: { + label: 'Bot-Token', + help: 'Erstelle eine Anwendung im Discord Developer Portal, füge einen Bot hinzu und füge dann dessen Token ein.' + }, + DISCORD_ALLOWED_USERS: { + label: 'Erlaubte Discord-Benutzer-IDs', + help: 'Empfohlen. Komma-getrennte Discord-Benutzer-IDs.' + }, + DISCORD_REPLY_TO_MODE: { label: 'Antwortstil', help: 'first, all, or off.' }, + DISCORD_ALLOW_ALL_USERS: { + label: 'Alle Discord-Benutzer zulassen', + help: 'Nur für Entwicklung. Wenn wahr, kann jedermann den Bot ohne Allowlist per DM erreichen.' + }, + DISCORD_HOME_CHANNEL: { + label: 'Home-Kanal-ID', + help: 'Kanal, in dem der Bot proaktive Nachrichten sendet (Cron-Ausgabe, Erinnerungen).' + }, + DISCORD_HOME_CHANNEL_NAME: { + label: 'Home-Kanal-Name', + help: 'Anzeigename für den Home-Kanal in Protokollen und Statusausgaben.' + }, + BLUEBUBBLES_ALLOW_ALL_USERS: { + label: 'Alle iMessage-Benutzer zulassen', + help: 'Wenn wahr, die BlueBubbles-Allowlist überspringen.' + }, + MATTERMOST_ALLOW_ALL_USERS: { label: 'Alle Mattermost-Benutzer zulassen' }, + MATTERMOST_HOME_CHANNEL: { label: 'Home-Kanal' }, + QQ_ALLOW_ALL_USERS: { label: 'Alle QQ-Benutzer zulassen' }, + QQBOT_HOME_CHANNEL: { label: 'QQ-Home-Kanal', help: 'Default channel or group for cron delivery.' }, + QQBOT_HOME_CHANNEL_NAME: { label: 'QQ-Home-Kanal-Name' }, + SLACK_BOT_TOKEN: { + label: 'Slack-Bot-Token', + help: 'Nutze den Bot-Token aus OAuth & Permissions nach der Installation deiner Slack-App.', + placeholder: 'Slack-Bot-Token einfügen' + }, + SLACK_APP_TOKEN: { + label: 'Slack-App-Token', + help: 'Nutze den auf App-Ebene erforderlichen Token für den Socket-Modus.', + placeholder: 'Slack-App-Token einfügen' + }, + SLACK_ALLOWED_USERS: { label: 'Erlaubte Slack-Benutzer-IDs', help: 'Recommended. Comma-separated Slack user IDs.' }, + MATTERMOST_URL: { label: 'Server-URL', placeholder: 'https://mattermost.example.com' }, + MATTERMOST_TOKEN: { label: 'Bot-Token' }, + MATTERMOST_ALLOWED_USERS: { + label: 'Erlaubte Benutzer-IDs', + help: 'Empfohlen. Komma-getrennte Mattermost-Benutzer-IDs.' + }, + MATRIX_HOMESERVER: { label: 'Homeserver-URL', placeholder: 'https://matrix.org' }, + MATRIX_ACCESS_TOKEN: { label: 'Zugriffs-Token' }, + MATRIX_USER_ID: { label: 'Bot-Benutzer-ID', placeholder: '@hermes:example.org' }, + MATRIX_ALLOWED_USERS: { + label: 'Erlaubte Matrix-Benutzer-IDs', + help: 'Empfohlen. Komma-getrennte Benutzer-IDs im Format @user:server.' + }, + SIGNAL_HTTP_URL: { + label: 'Signal-Bridge-URL', + placeholder: 'http://127.0.0.1:8080', + help: 'URL einer laufenden signal-cli-REST-Bridge.' + }, + SIGNAL_ACCOUNT: { label: 'Telefonnummer', help: 'The number registered with your signal-cli bridge.' }, + SIGNAL_ALLOWED_USERS: { label: 'Erlaubte Signal-Benutzer', help: 'Recommended. Comma-separated Signal identifiers.' }, + WHATSAPP_ENABLED: { + label: 'WhatsApp-Bridge aktivieren', + help: 'Wird automatisch durch den Umschalter unten gesetzt. Nicht ändern, es sei denn, du weißt, dass du es brauchst.' + }, + WHATSAPP_MODE: { label: 'Bridge-Modus' }, + WHATSAPP_ALLOWED_USERS: { + label: 'Erlaubte WhatsApp-Benutzer', + help: 'Empfohlen. Komma-getrennte Telefonnummern oder WhatsApp-IDs.' + } + }, + platformIntro: {} + }, + + profiles: { + close: 'Profile schließen', + nameHint: 'Kleinbuchstaben, Ziffern, Bindestriche und Unterstriche. Muss mit einem Buchstaben oder einer Ziffer beginnen.', + title: 'Profile', + count: count => `${count} ${count === 1 ? 'Profil' : 'Profile'}`, + search: 'Profile suchen...', + loading: 'Profile werden geladen...', + newProfile: 'Neues Profil', + allProfiles: 'Alle Profile', + showAllProfiles: 'Alle Profile anzeigen', + switchToProfile: name => `Zu ${name} wechseln`, + manageProfiles: 'Profile verwalten…', + actionsFor: name => `Aktionen für ${name}`, + color: 'Farbe…', + colorFor: name => `Farbe für ${name}`, + setColor: color => `Farbe ${color} festlegen`, + autoColor: 'Auto', + noProfiles: 'Noch keine Profile.', + selectPrompt: 'Wähle ein Profil, um seine Details anzuzeigen.', + refresh: 'Profile aktualisieren', + refreshing: 'Profile werden aktualisiert', + default: 'Standard', + skills: count => `${count} ${count === 1 ? 'Fähigkeit' : 'Fähigkeiten'}`, + env: 'Env', + defaultBadge: 'Standard', + rename: 'Umbenennen', + renameMenu: 'Umbenennen…', + editSoul: 'SOUL.md bearbeiten…', + copySetup: 'Einrichtung kopieren', + copying: 'Kopiere...', + modelLabel: 'Modell', + skillsLabel: 'Fähigkeiten', + notSet: 'Nicht festgelegt', + soulDesc: 'Der System-Prompt und die Persona-Anweisungen, die in diesem Profil festgelegt sind.', + soulOptional: 'Optional', + soulPlaceholder: mode => `Der System-Prompt / die Persona für dieses Profil.\nLeer lassen, um die ${mode}-Standardeinstellung beizubehalten.`, + soulPlaceholderCloned: 'Geklont', + soulPlaceholderEmpty: 'Leer', + unsavedChanges: 'Nicht gespeicherte Änderungen', + loadingSoul: 'SOUL.md wird geladen...', + emptySoul: 'Leere SOUL.md — beginne, die Persona zu schreiben...', + saving: 'Speichere...', + saveSoul: 'SOUL.md speichern', + deleteTitle: 'Profil löschen?', + deleteDescPrefix: 'This will delete', + deleteDescMid: 'and remove its', + deleteDescSuffix: 'directory. This cannot be undone.', + deleting: 'Lösche...', + createDesc: 'Profile sind unabhängige Hermes-Umgebungen: separate Konfiguration, Fähigkeiten und SOUL.md.', + nameLabel: 'Name', + cloneFrom: 'Klonen von', + cloneFromNone: 'Keine (leer)', + cloneFromDesc: 'Kopiert Konfiguration, Fähigkeiten und SOUL.md aus dem ausgewählten Quellprofil.', + cloneFromDefault: 'Von Standard klonen', + cloneFromDefaultDesc: 'Kopiert Konfiguration, Fähigkeiten und SOUL.md aus deinem Standardprofil.', + invalidName: hint => `Ungültiger Name. ${hint}`, + nameRequired: 'Name ist erforderlich.', + creating: 'Erstelle...', + createAction: 'Profil erstellen', + renameTitle: 'Profil umbenennen', + renameDescPrefix: 'Renaming updates the profile directory and any wrapper scripts in', + renameDescSuffix: '.', + newNameLabel: 'Neuer Name', + renaming: 'Benenne um...', + created: 'Profil erstellt', + renamed: 'Profil umbenannt', + deleted: 'Profil gelöscht', + setupCopied: 'Einrichtungsbefehl kopiert', + soulSaved: 'SOUL.md gespeichert', + failedLoad: 'Profile konnten nicht geladen werden', + failedDelete: 'Profil konnte nicht gelöscht werden', + failedCopy: 'Einrichtungsbefehl konnte nicht kopiert werden', + failedLoadSoul: 'SOUL.md konnte nicht geladen werden', + failedSaveSoul: 'SOUL.md konnte nicht gespeichert werden', + failedCreate: 'Profil konnte nicht erstellt werden', + failedRename: 'Profil konnte nicht umbenannt werden' + }, + + cron: { + close: 'Cron schließen', + title: 'Geplante Aufgaben', + count: count => `${count} ${count === 1 ? 'Aufgabe' : 'Aufgaben'}`, + search: 'Cron-Aufgaben suchen...', + loading: 'Cron-Aufgaben werden geladen...', + states: { + enabled: 'Aktiviert', + scheduled: 'Geplant', + running: 'Läuft', + paused: 'Pausiert', + disabled: 'Deaktiviert', + error: 'Fehler', + completed: 'Abgeschlossen' + }, + deliveryLabels: { + local: 'Diese Desktop-App', + telegram: 'Telegram', + discord: 'Discord', + slack: 'Slack', + email: 'E-Mail' + }, + scheduleLabels: { + daily: 'Täglich', + weekdays: 'Wochentags', + weekly: 'Wöchentlich', + monthly: 'Monatlich', + hourly: 'Alle 15 Minuten', + 'every-15-minutes': 'Alle 15 Minuten', + custom: 'Benutzerdefiniert' + }, + scheduleHints: { + daily: 'Jeden Tag um 9:00', + weekdays: 'Montag bis Freitag um 9:00', + weekly: 'Jeden Montag um 9:00', + monthly: 'Am ersten Tag jedes Monats um 9:00', + hourly: 'Alle 15 Minuten', + 'every-15-minutes': 'Alle 15 Minuten', + custom: 'Cron-Syntax oder natürliche Sprache' + }, + days: { + '0': 'Sunday', + '1': 'Monday', + '2': 'Tuesday', + '3': 'Wednesday', + '4': 'Thursday', + '5': 'Friday', + '6': 'Saturday', + '7': 'Sunday' + }, + dayFallback: value => `Tag ${value}`, + everyDayAt: time => `Jeden Tag um ${time}`, + weekdaysAt: time => `Wochentags um ${time}`, + everyDayOfWeekAt: (day, time) => `Jeden ${day} um ${time}`, + monthlyOnDayAt: (dayOfMonth, time) => `Monatlich am ${dayOfMonth}. um ${time}`, + topOfHour: 'Zu Beginn jeder Stunde', + everyHourAt: minute => `Jede Stunde um :${minute}`, + newCron: 'Neuer Cron', + emptyDescNew: + 'Plane einen Prompt ein, der auf einem Cron-Ausdruck läuft. Hermes führt ihn aus und liefert die Ergebnisse an das gewählte Ziel.', + emptyDescSearch: 'Versuche eine breitere Suchanfrage.', + emptyTitleNew: 'Noch keine geplanten Aufgaben', + emptyTitleSearch: 'Keine Treffer', + last: 'Zuletzt:', + next: 'Nächste:', + noRuns: 'Noch keine Ausführungen', + manage: 'Verwalten', + showRuns: 'Ausführungen anzeigen', + hideRuns: 'Ausführungen ausblenden', + runHistory: 'Ausführungsverlauf', + actionsFor: title => `Aktionen für ${title}`, + actionsTitle: 'Cron-Aufgaben-Aktionen', + resume: 'Cron fortsetzen', + pause: 'Cron pausieren', + resumeTitle: 'Fortsetzen', + pauseTitle: 'Pausieren', + triggerNow: 'Jetzt auslösen', + edit: 'Cron bearbeiten', + deleteTitle: 'Cron-Aufgabe löschen?', + deleteDescPrefix: 'This will remove', + deleteDescSuffix: 'permanently. It will stop firing immediately.', + deleting: 'Lösche...', + resumed: 'Cron fortgesetzt', + paused: 'Cron pausiert', + triggered: 'Cron ausgelöst', + deleted: 'Cron gelöscht', + created: 'Cron erstellt', + updated: 'Cron aktualisiert', + failedLoad: 'Cron-Aufgaben konnten nicht geladen werden', + failedUpdate: 'Cron-Aufgabe konnte nicht aktualisiert werden', + failedTrigger: 'Cron-Aufgabe konnte nicht ausgelöst werden', + failedDelete: 'Cron-Aufgabe konnte nicht gelöscht werden', + failedSave: 'Cron-Aufgabe konnte nicht gespeichert werden', + editTitle: 'Cron-Aufgabe bearbeiten', + createTitle: 'Neue Cron-Aufgabe', + editDesc: 'Aktualisiere Zeitplan, Prompt oder Ziel. Änderungen gelten für den nächsten Lauf.', + createDesc: 'Plane einen Prompt für die automatische Ausführung ein. Nutze Cron-Syntax oder einen natürlichen Ausdruck wie „alle 15 Minuten".', + nameLabel: 'Name', + namePlaceholder: 'Morgen-Briefing', + promptLabel: 'Prompt', + promptPlaceholder: 'Fasse meine ungelesenen Slack-Threads zusammen und sende mir die Top 5 per E-Mail...', + frequencyLabel: 'Häufigkeit', + deliverLabel: 'Zustellen an', + customScheduleLabel: 'Benutzerdefinierter Zeitplan', + customPlaceholder: '0 9 * * * oder Wochentags um 9 Uhr', + customHint: 'Cron-Ausdruck oder Ausdrücke wie „jede Stunde" oder „Wochentags um 9 Uhr".', + optional: 'Optional', + promptRequired: 'Prompt ist erforderlich.', + promptScheduleRequired: 'Prompt und Zeitplan sind erforderlich.', + scheduleRequired: 'Zeitplan ist erforderlich.', + scriptOnlyEditHint: 'Nur-Skript-Job (kein KI-Prompt). Job-ID:', + saveChanges: 'Änderungen speichern', + createAction: 'Cron erstellen' + }, + + artifacts: { + search: 'Artefakte suchen...', + refresh: 'Artefakte aktualisieren', + refreshing: 'Artefakte werden aktualisiert', + indexing: 'Kürzliche Sitzungsartefakte werden indexiert', + tabAll: 'Alle', + tabImages: 'Bilder', + tabFiles: 'Dateien', + tabLinks: 'Links', + noArtifactsTitle: 'Keine Artefakte gefunden', + noArtifactsDesc: 'Generierte Bilder und Datei-Ausgaben erscheinen hier, sobald Sitzungen sie erzeugen.', + failedLoad: 'Artefakte konnten nicht geladen werden', + openFailed: 'Öffnen fehlgeschlagen', + itemsImage: 'Bilder', + itemsLink: 'Links', + itemsFile: 'Dateien', + itemsGeneric: 'Elemente', + zero: '0', + rangeOf: (start, end, total) => `${start}–${end} von ${total}`, + goToPage: (itemLabel, page) => `Zu Seite ${page} (${itemLabel}) gehen`, + colTitleLink: 'Link-Titel', + colTitleFile: 'Name', + colTitleDefault: 'Titel / Name', + colLocationLink: 'URL', + colLocationFile: 'Pfad', + colLocationDefault: 'Ort', + colSession: 'Sitzung', + kindImage: 'Bild', + kindFile: 'Datei', + kindLink: 'Link', + chat: 'Chat', + copyUrl: 'URL kopieren', + copyPath: 'Pfad kopieren' + }, + + sidebar: { + nav: { + 'new-session': 'Neue Sitzung', + skills: 'Funktionen', + messaging: 'Messaging', + artifacts: 'Artefakte' + }, + searchAria: 'Sitzungen suchen', + searchPlaceholder: 'Sitzungen suchen…', + clearSearch: 'Suche leeren', + noMatch: query => `Keine Sitzungen passen zu „${query}".`, + results: 'Ergebnisse', + pinned: 'Angeheftet', + sessions: 'Sitzungen', + cronJobs: 'Cron-Aufgaben', + groupAriaGrouped: 'Sitzungen als einfache Liste anzeigen', + groupAriaUngrouped: 'Sitzungen nach Arbeitsbereich gruppieren', + showProjects: 'Projekte anzeigen', + showSessions: 'Sitzungen anzeigen', + groupTitleGrouped: 'Sitzungen auflösen', + groupTitleUngrouped: 'Nach Arbeitsbereich gruppieren', + allPinned: 'Alles hier ist angeheftet. Löse einen Chat, um ihn in „Kürzlich" anzuzeigen.', + shiftClickHint: 'Umschalt-Klick auf einen Chat zum Anheften', + noWorkspace: 'Kein Arbeitsbereich', + noProject: 'Kein Projekt', + projectEmpty: 'Noch keine Sitzungen', + noSessions: 'Noch keine Sitzungen', + projects: { + sectionLabel: 'Projekte', + newButton: 'Neues Projekt', + createTitle: 'Neues Projekt', + createDesc: 'Benenne einen Arbeitsbereich und füge einen oder mehrere Ordner hinzu.', + renameTitle: 'Projekt umbenennen', + addFolderTitle: 'Ordner hinzufügen', + namePlaceholder: 'z. B. Skunkworks', + foldersLabel: 'Ordner', + ideaLabel: 'Idee', + ideaPlaceholder: 'Worum geht es in diesem Projekt? (wird in IDEA.md gespeichert)', + ideaGenerate: 'Idee generieren', + ideaGenerating: 'Generiere…', + ideaShuffle: 'Vorlagen mischen', + noFolders: 'Noch keine Ordner hinzugefügt.', + addFolder: 'Ordner hinzufügen', + primaryBadge: 'Primär', + removeFolder: 'Entfernen', + create: 'Erstellen', + menu: 'Projektaktionen', + menuRename: 'Umbenennen', + menuAppearance: 'Erscheinungsbild', + noColor: 'Keine Farbe', + menuAddFolder: 'Ordner hinzufügen', + menuSetActive: 'Aktiv setzen', + menuDelete: 'Löschen', + reveal: 'Im Ordner anzeigen', + copyPath: 'Pfad kopieren', + removeFromSidebar: 'Aus Seitenleiste ausblenden', + createFailed: 'Projekt konnte nicht erstellt werden', + staleBackend: + 'Aktualisiere das Hermes-Backend, um Projekte zu erstellen — dein Backend ist älter als diese Desktop-App (Einstellungen → Aktualisierungen → Backend).', + deleteConfirm: 'Dies entfernt das gespeicherte Projekt aus Hermes. Dateien, Git-Repos und Worktrees bleiben unberührt.', + startWork: 'Neuer Worktree', + newWorktreeTitle: 'Neuer Worktree', + newWorktreeDesc: 'Benenne den Zweig für diesen Worktree.', + branchPlaceholder: 'z. B. meine-funktion', + startWorkFailed: 'Worktree konnte nicht erstellt werden', + convertBranch: 'Einen Zweig umwandeln…', + convertBranchTitle: 'Zweig umwandeln', + convertBranchDesc: 'Öffne ausgecheckte Zweige oder erstelle einen Worktree für einen freien Zweig.', + convertBranchPlaceholder: 'Zweige suchen…', + convertBranchInstead: 'Einen vorhandenen Zweig umwandeln', + branchOpenExisting: 'Öffnen', + branchSwitchHome: 'Home wechseln', + branchCreateWorktree: 'Neuer Worktree', + branchesLoading: 'Zweige werden geladen…', + noBranches: 'Keine Zweige gefunden', + removeWorktree: 'Worktree entfernen', + removeWorktreeFailed: 'Worktree konnte nicht entfernt werden (nicht committete Änderungen?)', + removeWorktreeConfirm: + 'Entferne es aus Git (löscht das Worktree-Verzeichnis; der Zweig bleibt) oder blende nur die Spur in der Seitenleiste aus und lasse das Worktree auf der Festplatte.', + removeWorktreeDirty: + 'Dieses Worktree hat nicht committete Änderungen. Erzwinge das Entfernen (verwirft diese Änderungen) oder blende nur die Spur aus und behalte es auf der Festplatte.', + forceRemove: 'Erzwingt entfernen', + enter: label => `${label} öffnen`, + reorder: label => `${label} umsortieren`, + toggle: label => `${label}-Sitzungen umschalten`, + back: 'Alle Projekte' + }, + newSessionIn: label => `Neue Sitzung in ${label}`, + showMoreIn: (count, label) => `${count} weitere in ${label} anzeigen`, + loading: 'Lädt…', + loadMore: 'Mehr laden', + loadCount: step => `${step} mehr laden`, + row: { + pin: 'Anheften', + unpin: 'Lösen', + copyId: 'ID kopieren', + export: 'Exportieren', + branchFrom: 'Zweig', + rename: 'Umbenennen', + archive: 'Archivieren', + newWindow: 'Neues Fenster', + copyIdFailed: 'Sitzungs-ID konnte nicht kopiert werden', + actionsFor: title => `Aktionen für ${title}`, + sessionActions: 'Sitzungsaktionen', + sessionRunning: 'Sitzung läuft', + needsInput: 'Benötigt deine Eingabe', + waitingForAnswer: 'Wartet auf deine Antwort', + handoffOrigin: platform => `Übergeben von ${platform}`, + renamed: 'Umbenannt', + renameFailed: 'Umbenennen fehlgeschlagen', + renameTitle: 'Sitzung umbenennen', + renameDesc: 'Gib diesem Chat einen merkfähigen Titel. Leer lassen zum Leeren.', + untitledPlaceholder: 'Unbenannte Sitzung', + ageNow: 'Jetzt', + ageDay: 'T', + ageHour: 'S', + ageMin: 'M' + } + }, + + composer: { + message: 'Nachricht', + wakingProfile: profile => `${profile} wird geweckt…`, + placeholderStarting: 'Hermes wird gestartet...', + placeholderReconnecting: 'Verbindung zu Hermes wird wiederhergestellt…', + placeholderFollowUp: 'Folgenachricht senden', + newSessionPlaceholders: [ + 'Mit einem Ziel beginnen', + 'Mit einem Ziel beginnen', + "What's on your mind?", + 'Mit einem Ziel beginnen', + 'Mit einem Ziel beginnen', + 'Mit einem Ziel beginnen', + 'Mit einem Ziel beginnen' + ], + followUpPlaceholders: [ + 'Anpassen oder fortfahren', + 'Anpassen oder fortfahren', + 'Anpassen oder fortfahren', + "What's next?", + 'Anpassen oder fortfahren', + 'Anpassen oder fortfahren', + 'Anpassen oder fortfahren' + ], + startVoice: 'Sprachkonversation starten', + queueMessage: 'Nachricht queuen', + steer: 'Den aktuellen Lauf steuern', + stop: 'Stopp', + send: 'Senden', + speaking: 'Spricht', + transcribing: 'Transkribiere', + thinking: 'Denkt nach', + muted: 'Stumm', + listening: 'Hört zu', + muteMic: 'Mikrofon stummschalten', + unmuteMic: 'Mikrofon aktivieren', + stopListening: 'Aufnahme stoppen und senden', + stopShort: 'Stopp', + endConversation: 'Sprachkonversation beenden', + endShort: 'Beenden', + stopDictation: 'Diktat stoppen', + transcribingDictation: 'Diktat wird transkribiert', + voiceDictation: 'Sprachdiktat', + speakReplies: 'Antworten laut vorlesen', + stopSpeakingReplies: 'Lautes Vorlesen von Antworten stoppen', + lookupLoading: 'Suche…', + lookupNoMatches: 'Keine Treffer.', + lookupTry: 'Versuche', + lookupOr: 'oder', + commonCommands: 'Häufige Befehle', + hotkeys: 'Tastenkürzel', + helpFooter: 'Öffnet das vollständige Panel · Backspace schließt', + commandDescs: { + '/help': 'Vollständige Liste der Befehle + Tastenkürzel', + '/clear': 'Neue Sitzung starten', + '/resume': 'Frühere Sitzung fortsetzen', + '/details': 'Detailgrad des Transkripts steuern', + '/copy': 'Auswahl oder letzte Assistenten-Nachricht kopieren', + '/quit': 'Hermes beenden' + }, + hotkeyDescs: { + 'composer.mention': 'Dateien, Ordner, URLs, Git referenzieren', + 'composer.slash': 'Slash-Befehlspalette', + 'composer.help': 'Diese Schnellhilfe (löschen zum Ausblenden)', + 'composer.sendNewline': 'Senden · Umschalt+Eingabe für Zeilenumbruch', + 'composer.sendQueued': 'Nächsten wartenden Turn senden', + 'keybinds.openPanel': 'Alle Tastenkürzel', + 'composer.cancel': 'Popover schließen · Lauf abbrechen', + 'composer.history': 'Popover / Verlauf durchblättern' + }, + attachUrlTitle: 'Eine URL anhängen', + attachUrlDesc: 'Hermes ruft die Seite ab und fügt sie als Kontext für diesen Schritt hinzu.', + urlPlaceholder: 'https://example.com/post', + urlHintPre: 'Include the full URL, e.g.', + attach: 'Anhängen', + queued: count => `${count} in Warteschlange`, + attachmentOnly: 'Nur-Anhang-Schritt', + emptyTurn: 'Leerer Schritt', + attachments: count => `${count} ${count === 1 ? 'Anhang' : 'Anhänge'}`, + editingInComposer: 'In der Eingabe bearbeiten', + editingQueuedInComposer: 'Wartenden Schritt in der Eingabe bearbeiten', + queueEdit: 'Bearbeiten', + queueSendNext: 'Weiter', + queueSend: 'Senden', + queueDelete: 'Löschen', + queueStuckTitle: 'Wartende Nachricht nicht gesendet', + queueStuckBody: 'Ein wartender Schritt konnte wiederholt nicht gesendet werden. Er ist noch in der Warteschlange — versuche, ihn erneut zu senden.', + previewUnavailable: 'Vorschau nicht verfügbar', + previewLabel: label => `${label} Vorschau`, + couldNotPreview: label => `${label} konnte nicht vorab angesehen werden`, + removeAttachment: label => `${label} entfernen`, + dictating: 'Diktiert', + preparingAudio: 'Bereite Audio vor', + speakingResponse: 'Spreche Antwort', + readingAloud: 'Lese laut vor', + themeSuggestions: 'Desktop-Design-Vorschläge', + noMatchingThemes: 'Keine passenden Designs.', + themeTryPre: 'Try', + themeTryPost: '.', + attachLabel: 'Anhängen', + files: 'Dateien…', + folder: 'Ordner…', + images: 'Bilder…', + pasteImage: 'Bild einfügen', + url: 'URL…', + promptSnippets: 'Prompt-Schnipsel…', + tipPre: 'Tip: type', + tipPost: 'to reference files inline.', + snippetsTitle: 'Prompt-Schnipsel', + snippetsDesc: 'Wähle einen Start-Prompt, der in die Eingabe eingefügt wird.', + dropFiles: 'Dateien zum Anhängen ablegen', + dropSession: 'Ablegen, um diesen Chat zu verknüpfen', + snippets: { + codeReview: { + label: 'Code-Review', + description: 'Prüfe die aktuelle Änderung auf Regressionen, fehlende Randfälle und fehlende Tests.', + text: 'Bitte prüfe dies auf Bugs, Regressionen und fehlende Tests.' + }, + implementationPlan: { + label: 'Implementierungsplan', + description: 'Skizziere einen Ansatz, bevor du Code änderst, damit der Diff fokussiert bleibt.', + text: 'Bitte erstelle einen prägnanten Implementierungsplan, bevor du Code änderst.' + }, + explainThis: { + label: 'Dies erklären', + description: 'Gehe durch, wie der ausgewählte Code funktioniert, und verweise auf die Schlüsseldateien.', + text: 'Bitte erkläre, wie das funktioniert, und verweise mich auf die Schlüsseldateien.' + } + } + }, + + statusStack: { + agents: 'Agenten', + background: count => `${count} Hintergrund`, + subagents: count => `${count} ${count === 1 ? 'Subagent' : 'Subagenten'}`, + todos: (done, total) => `Aufgaben ${done}/${total}`, + running: 'Läuft', + stop: 'Stopp', + dismiss: 'Verwerfen', + exit: code => `Beenden ${code}`, + coding: { + title: 'Arbeitsbaum', + noBranch: 'Kein Zweig', + detached: 'Losgelöst', + clean: 'Sauber', + changed: count => `${count} geändert`, + ahead: count => `${count} voraus`, + behind: count => `${count} zurück`, + review: 'Review', + close: 'Schließen', + openChanges: 'Änderungen öffnen', + openFile: 'Datei öffnen', + stage: 'Stagen', + unstage: 'Unstagen', + stageAll: 'Alle stagen', + viewAsTree: 'Als Baum anzeigen', + viewAsList: 'Als Liste anzeigen', + revert: 'Zurücknehmen', + revertAll: 'Alle zurücknehmen', + revertConfirm: 'Änderungen an dieser Datei verwerfen und auf den committeten Zustand zurücksetzen? Dies kann nicht rückgängig gemacht werden.', + revertAllConfirm: 'Jede Änderung verwerfen und Dateien auf den committeten Zustand zurücksetzen? Dies kann nicht rückgängig gemacht werden.', + staged: 'Gestagt', + noChanges: 'Keine Änderungen', + notRepo: 'Kein Git-Repository', + noDiff: 'Kein Diff anzuzeigen', + scopeUncommitted: 'Nicht committet', + scopeBranch: 'Zweig', + scopeLastTurn: 'Letzter Schritt', + commit: 'Commit', + commitAndPush: 'Commit & Push', + commitPlaceholder: 'Nachricht (⌘↵ zum Committen)', + generateCommitMessage: 'Commit-Nachricht generieren', + stopGenerating: 'Generierung stoppen', + createPr: 'PR erstellen', + openPr: 'PR öffnen', + ghMissing: 'Installiere die GitHub-CLI (gh) und melde dich an, um PRs zu öffnen', + agentShip: 'Hermes bitten, den PR zu öffnen', + agentShipPrompt: + 'Prüfe die aktuellen Änderungen, committe sie mit einer klaren Conventional-Commit-Nachricht, pushe den Zweig und öffne einen Pull Request.', + newBranch: 'Neuer Zweig', + branchOffFrom: base => `Neuer Zweig von ${base}`, + switchTo: branch => `Zu ${branch} wechseln`, + switchFailed: branch => `Zu ${branch} konnte nicht gewechselt werden`, + worktrees: 'Worktrees' + } + }, + + updates: { + stages: { + idle: 'Wird vorbereitet…', + prepare: 'Wird vorbereitet…', + fetch: 'Lade herunter…', + pull: 'Fast geschafft…', + pydeps: 'Wird abgeschlossen…', + update: 'Hermes wird aktualisiert…', + rebuild: 'Desktop-App wird neu gebaut…', + restart: 'Hermes wird neu gestartet…', + done: 'Aktualisierung abgeschlossen', + manual: 'Aus deinem Terminal aktualisieren', + guiSkew: 'Desktop-App aktualisieren', + error: 'Aktualisierung pausiert' + }, + checking: 'Suche nach Updates…', + checkFailedTitle: 'Updates konnten nicht geprüft werden', + tryAgain: 'Erneut versuchen', + notAvailableTitle: 'Keine Aktualisierung verfügbar', + unsupportedMessage: 'Diese Hermes-Version kann sich nicht aus der App heraus aktualisieren.', + connectionRetry: 'Prüfe deine Verbindung und versuche es erneut.', + latestBody: 'Du verwendest die neueste Version.', + latestBodyBackend: 'Das Backend verwendet die neueste Version.', + allSetTitle: 'Alles bereit', + availableTitle: 'Neue Aktualisierung verfügbar', + availableBody: 'Eine neue Hermes-Version ist zur Installation bereit.', + availableTitleBackend: 'Backend-Aktualisierung verfügbar', + availableBodyBackend: 'Eine neuere Version des verbundenen Hermes-Backends ist zur Installation bereit.', + availableBodyNoChangelog: 'Eine neuere Version ist bereit. Versionshinweise sind für diesen Installationstyp nicht verfügbar.', + updateNow: 'Jetzt aktualisieren', + maybeLater: 'Vielleicht später', + moreChanges: count => `+ ${count} weitere ${count === 1 ? 'Änderung' : 'Änderungen'} enthalten.`, + manualTitle: 'Aus deinem Terminal aktualisieren', + manualBody: 'Du hast Hermes über die Befehlszeile installiert, daher laufen Aktualisierungen ebenfalls dort. Füge dies in dein Terminal ein:', + manualPickedUp: 'Hermes verwendet die neue Version beim nächsten Start.', + guiSkewTitle: 'Desktop-App aktualisieren', + guiSkewBody: + 'Das Backend wurde aktualisiert, aber dieses Desktop-App-Paket wurde nicht geändert. Aktualisiere oder installiere die Hermes-Desktop-App (dein AppImage / .deb / .rpm) neu, um sie anzugleichen.', + copy: 'Kopieren', + copied: 'Kopiert', + done: 'Fertig', + applyingBody: + 'Der Hermes-Updater übernimmt in einem eigenen Fenster und öffnet Hermes automatisch neu, wenn er fertig ist. Bitte öffne Hermes während der Aktualisierung nicht selbst.', + applyingBodyBackend: + 'Das Remote-Backend wendet die Aktualisierung an und wird neu starten. Hermes verbindet sich automatisch neu, sobald es zurück ist.', + applyingClose: 'Dieses Fenster schließt während der Aktualisierung, dann öffnet Hermes sich selbstständig neu.', + errorTitle: 'Aktualisierung nicht abgeschlossen', + errorBody: 'Keine Sorge — nichts ging verloren. Du kannst es jetzt erneut versuchen.', + notNow: 'Nicht jetzt', + applyStatus: { + preparing: 'Backend wird aktualisiert…', + pulling: 'Backend wird aktualisiert…', + restarting: 'Backend wird neu gestartet, um die Aktualisierung zu laden…', + notAvailable: 'Keine Aktualisierung für dieses Backend verfügbar.', + failed: 'Backend-Aktualisierung fehlgeschlagen.', + noReturn: 'Backend ist nicht wieder online gegangen. Die Aktualisierung wurde möglicherweise nicht abgeschlossen — prüfe den Backend-Host.' + } + }, + + install: { + stageStates: { + pending: 'Ausstehend', + running: 'Installiere', + succeeded: 'Fertig', + skipped: 'Übersprungen', + failed: 'Fehlgeschlagen' + }, + oneTimeTitle: 'Hermes benötigt eine einmalige Installation', + unsupportedDesc: platform => + `Die automatische Installation beim ersten Start ist auf ${platform} noch nicht verfügbar. Öffne das Terminal und führe den untenstehenden Befehl aus, dann starte diese App neu. Folgende Starts überspringen diesen Schritt.`, + installCommand: 'Installationsbefehl', + copyCommand: 'Befehl kopieren', + viewDocs: 'Installationsdokumentation anzeigen', + installTo: 'Wird installiert nach', + retryAfterRun: 'Ich habe es ausgeführt -- erneut versuchen', + failedTitle: 'Installation fehlgeschlagen', + settingUpTitle: 'Hermes Agent wird eingerichtet', + finishingTitle: 'Wird abgeschlossen', + failedDesc: + 'Einer der Installationsschritte ist fehlgeschlagen. Unter Windows kann dies passieren, wenn eine andere Hermes-CLI- oder Desktop-Instanz läuft. Stoppe alle laufenden Hermes-Instanzen und versuche es erneut. Prüfe die Details unten oder das Desktop-Protokoll für das vollständige Transkript.', + activeDesc: + 'Dies ist eine einmalige Einrichtung. Der Hermes-Installer lädt Abhängigkeiten herunter und konfiguriert deinen Rechner. Folgende Starts überspringen diesen Schritt.', + progress: (completed, total) => `${completed} von ${total} Schritten abgeschlossen`, + currentStage: stage => `-- now: ${stage}`, + fetchingManifest: 'Installer-Manifest wird abgerufen...', + error: 'Fehler', + hideOutput: 'Installer-Ausgabe ausblenden', + showOutput: 'Installer-Ausgabe anzeigen', + lines: count => `${count} ${count === 1 ? 'Zeile' : 'Zeilen'}`, + noOutput: 'Noch keine Ausgabe.', + cancelling: 'Abbrechen...', + cancelInstall: 'Installation abbrechen', + transcriptSaved: 'Vollständiges Transkript gespeichert unter', + copiedOutput: 'Kopiert!', + copyOutput: 'Ausgabe kopieren', + reloadRetry: 'Neu laden und erneut versuchen' + }, + + onboarding: { + headerTitle: 'Richten wir Hermes Agent für dich ein', + headerDesc: 'Verbinde einen Modell-Anbieter, um zu chatten. Die meisten Optionen brauchen einen Klick.', + preparingInstall: 'Hermes schließt die Installation ab. Das dauert beim ersten Start meist unter einer Minute.', + starting: 'Hermes wird gestartet…', + lookingUpProviders: 'Anbieter werden gesucht...', + collapse: 'Einklappen', + otherProviders: 'Andere Anbieter', + haveApiKey: 'Ich habe einen API-Schlüssel', + chooseLater: 'Ich wähle später einen Anbieter', + recommended: 'Empfohlen', + connected: 'Verbunden', + featuredPitch: 'Ein Abo, 300+ Frontier-Modelle — der empfohlene Weg, Hermes auszuführen', + openRouterPitch: 'Ein Schlüssel, hunderte Modelle — eine solide Standardeinstellung', + apiKeyOptions: { + fireworks: { + short: 'Direkte Modell-API', + description: 'Direkter Zugriff auf Modelle, die von Fireworks AI gehostet werden.' + }, + openrouter: { + short: 'Ein Schlüssel, viele Modelle', + description: 'Bietet hunderte Modelle hinter einem einzigen Schlüssel. Gute Standardeinstellung für neue Installationen.' + }, + openai: { short: 'GPT-Klasse-Modelle', description: 'Direct access to OpenAI models.' }, + gemini: { short: 'Gemini-Modelle', description: 'Direct access to Google Gemini models.' }, + xai: { short: 'Grok-Modelle', description: 'Direct access to xAI Grok models.' }, + local: { + short: 'Selbst gehostet', + description: 'Richte Hermes auf einen lokalen oder selbst gehosteten OpenAI-kompatiblen Endpunkt (vLLM, llama.cpp, Ollama usw.)' + } + }, + backToSignIn: 'Zurück zur Anmeldung', + getKey: 'Schlüssel abrufen', + replaceCurrent: 'Aktuellen Wert ersetzen', + pasteApiKey: 'API-Schlüssel einfügen', + localApiKeyPlaceholder: 'API-Schlüssel (optional — nur falls dein Endpunkt einen benötigt)', + couldNotSave: 'Anmeldedaten konnten nicht gespeichert werden.', + connecting: 'Verbinde…', + update: 'Aktualisieren', + flowSubtitles: { + pkce: 'Öffnet deinen Browser zur Anmeldung und setzt hier fort', + device_code: 'Öffnet eine Verifizierungsseite in deinem Browser — Hermes verbindet sich automatisch', + external: 'Melde dich einmal in deinem Terminal an, dann kehre zum Chat zurück' + }, + startingSignIn: provider => `Anmeldung für ${provider} wird gestartet...`, + verifyingCode: provider => `Dein Code wird mit ${provider} verifiziert...`, + connectedProvider: provider => `${provider} verbunden`, + connectedPicking: provider => `${provider} verbunden. Wähle ein Standardmodell...`, + signInFailed: 'Anmeldung fehlgeschlagen. Erneut versuchen.', + pickDifferentProvider: 'Anderen Anbieter wählen', + signInWith: provider => `Mit ${provider} anmelden`, + openedBrowser: provider => `Wir haben ${provider} in deinem Browser geöffnet.`, + authorizeThere: 'Autorisiere Hermes dort.', + copyAuthCode: 'Kopiere den Autorisierungscode und füge ihn unten ein.', + pasteAuthCode: 'Autorisierungscode einfügen', + reopenAuthPage: 'Autorisierungsseite erneut öffnen', + autoBrowser: provider => + `Wir haben ${provider} in deinem Browser geöffnet. Autorisiere Hermes dort und du wirst automatisch verbunden — nichts zu kopieren oder einzufügen.`, + reopenSignInPage: 'Anmeldeseite erneut öffnen', + waitingAuthorize: 'Warte auf deine Autorisierung...', + externalPending: provider => + `${provider} meldet sich über seine eigene CLI an. Führe diesen Befehl im Terminal aus, kehre dann zurück und wähle „Ich habe mich angemeldet":`, + signedIn: 'Ich habe mich angemeldet', + deviceCodeOpened: provider => `Wir haben ${provider} in deinem Browser geöffnet. Gib dort diesen Code ein:`, + reopenVerification: 'Verifizierungsseite erneut öffnen', + copy: 'Kopieren', + defaultModel: 'Standardmodell', + freeTier: 'Kostenlose Stufe', + pro: 'Pro', + free: 'Kostenlos', + price: (input, output) => `${input} ein / ${output} aus pro Mio. Tokens`, + change: 'Ändern', + startChatting: 'Beginnen', + docs: provider => `${provider}-Dokumentation` + }, + + modelPicker: { + title: 'Modell wechseln', + current: 'Aktuell:', + unknown: '(unbekannt)', + search: 'Anbieter und Modelle filtern...', + noModels: 'Keine Modelle gefunden.', + addProvider: 'Anbieter hinzufügen', + loadFailed: 'Modelle konnten nicht geladen werden', + noAuthenticatedProviders: 'Keine authentifizierten Anbieter.', + pro: 'Pro', + proNeedsSubscription: 'Pro-Modelle benötigen ein kostenpflichtiges Nous-Abo.', + free: 'Kostenlos', + freeTier: 'Kostenlose Stufe', + priceTitle: 'Eingabe-/Ausgabepreis pro Million Tokens' + }, + + modelVisibility: { + title: 'Modelle', + search: 'Modelle suchen', + noAuthenticatedProviders: 'Keine authentifizierten Anbieter.', + addProvider: 'Anbieter hinzufügen…' + }, + + shell: { + windowControls: 'Fenstersteuerung', + paneControls: 'Bereichssteuerung', + appControls: 'App-Steuerung', + modelMenu: { + search: 'Modelle suchen', + noModels: 'Keine Modelle gefunden', + editModels: 'Modelle bearbeiten…', + refreshModels: 'Modelle aktualisieren', + fast: 'Schnell', + medium: 'Mittel' + }, + modelOptions: { + noOptions: 'Keine Optionen für dieses Modell', + options: 'Optionen', + thinking: 'Denkt nach', + fast: 'Schnell', + effort: 'Aufwand', + minimal: 'Minimal', + low: 'Niedrig', + medium: 'Mittel', + high: 'Hoch', + max: 'Maximal', + updateFailed: 'Modelloptions-Aktualisierung fehlgeschlagen', + fastFailed: 'Schnellmodus-Aktualisierung fehlgeschlagen' + }, + gatewayMenu: { + gateway: 'Gateway', + connected: 'Verbunden', + connecting: 'Verbinde…', + offline: 'Offline', + inferenceReady: 'Inferenz bereit', + inferenceNotReady: 'Inferenz nicht bereit', + checkingInference: 'Inferenz wird geprüft', + disconnected: 'Getrennt', + openSystem: 'Systempanel öffnen', + connection: label => `Verbindung: ${label}`, + recentActivity: 'Letzte Aktivität', + viewAllLogs: 'Alle Protokolle ansehen →', + messagingPlatforms: 'Messaging-Plattformen' + }, + statusbar: { + unknown: 'Unbekannt', + restart: 'Neustart', + update: 'Aktualisieren', + updateInProgress: 'Aktualisierung läuft', + commitsBehind: (count, branch) => `${count} ${count === 1 ? 'Commit' : 'Commits'} hinter ${branch}`, + desktopVersion: version => `Hermes Desktop v${version}`, + backendVersion: version => `Backend v${version}`, + clientLabel: version => `Client v${version}`, + backendLabel: version => `Backend v${version}`, + commit: sha => `Commit ${sha}`, + branch: branch => `Zweig ${branch}`, + closeCommandCenter: 'Befehlszentrum schließen', + openCommandCenter: 'Befehlszentrum öffnen', + showTerminal: 'Terminal anzeigen', + hideTerminal: 'Terminal ausblenden', + gateway: 'Gateway', + gatewayReady: 'Bereit', + gatewayNeedsSetup: 'Einrichtung erforderlich', + gatewayChecking: 'Prüfe', + gatewayConnecting: 'Verbinde…', + gatewayOffline: 'Offline', + gatewayRestarting: 'Neustart…', + gatewayTitle: 'Hermes-Inferenz-Gateway-Status', + agents: 'Agenten', + closeAgents: 'Agenten schließen', + openAgents: 'Agenten öffnen', + subagents: count => `${count} ${count === 1 ? 'Subagent' : 'Subagenten'}`, + failed: count => `${count} fehlgeschlagen`, + running: count => `${count} läuft`, + cron: 'Cron', + openCron: 'Cron-Aufgaben öffnen', + starmap: 'Gedächtnisgraph', + openStarmap: 'Gedächtnisgraph öffnen', + turnRunning: 'Läuft', + currentTurnElapsed: 'Aktueller Schritt verstrichen', + contextUsage: 'Kontextnutzung', + contextUsagePanel: { + categories: { + conversation: 'Gespräch', + mcp: 'MCP', + memory: 'Gedächtnis', + rules: 'Regeln', + skills: 'Fähigkeiten', + subagent_definitions: 'Subagent-Definitionen', + system_prompt: 'System-Prompt', + tool_definitions: 'Werkzeug-Definitionen' + }, + empty: 'Noch keine Kontextdaten', + loading: 'Aufschlüsselung wird geladen…', + percentFull: percent => `${percent}% voll`, + title: 'Kontextnutzung', + tokenSummary: (used, max) => `${used} / ${max} Tokens` + }, + openContextUsage: 'Kontextnutzungs-Aufschlüsselung öffnen', + session: 'Sitzung', + runtimeSessionElapsed: 'Laufzeit-Sitzung verstrichen', + yoloOn: 'YOLO ein — gefährliche Befehle automatisch freigegeben. Klick zum Deaktivieren. Umschalt+Klick schaltet es global um.', + yoloOff: 'YOLO aus — klick, um gefährliche Befehle automatisch freizugeben. Umschalt+Klick schaltet es global um.', + modelNone: 'Keine', + noModel: 'Kein Modell', + switchModel: 'Modell wechseln', + openModelPicker: 'Modell-Auswahl öffnen', + modelTitle: (provider, model) => `Modell · ${provider}: ${model}`, + providerModelTitle: (provider, model) => `${provider} · ${model}` + } + }, + + rightSidebar: { + aria: 'Rechte Seitenleiste', + panelsAria: 'Rechte-Seitenleisten-Panels', + files: 'Dateisystem', + terminal: 'Terminal', + noFolderSelected: 'Kein Ordner ausgewählt', + changeCwdTitle: 'Arbeitsverzeichnis ändern', + remotePickerTitle: 'Remote-Ordner wählen', + remotePickerDescription: 'Durchsuche Ordner auf dem verbundenen Backend.', + remotePickerSelect: 'Ordner auswählen', + folderTip: cwd => `${cwd} — klicken, um Ordner zu ändern`, + openFolder: 'Ordner öffnen', + refreshTree: 'Baum aktualisieren', + collapseAll: 'Alle Ordner einklappen', + previewUnavailable: 'Vorschau nicht verfügbar', + couldNotPreview: path => `${path} konnte nicht vorab angesehen werden`, + noProjectTitle: 'Kein Projekt', + noProjectBody: 'Öffne ein Projekt, um seine Dateien zu durchsuchen und Änderungen zu prüfen.', + noProjectOpen: 'Kein Projekt geöffnet', + noDiffs: 'Keine Diffs', + unreadableTitle: 'Unlesbar', + unreadableBody: error => `Dieser Ordner konnte nicht gelesen werden (${error}).`, + emptyTitle: 'Leer', + emptyBody: 'Dieser Ordner ist leer.', + treeErrorTitle: 'Baumfehler', + treeErrorBody: 'Der Dateibaum stieß beim Rendern dieses Ordners auf einen Fehler.', + tryAgain: 'Erneut versuchen', + loadingTree: 'Dateibaum wird geladen', + loadingFiles: 'Dateien werden geladen', + terminalHide: 'Terminal ausblenden', + terminalsAria: 'Terminals', + terminalNew: 'Neues Terminal', + terminalCloseOthers: 'Andere schließen', + terminalCloseAll: 'Alle schließen', + addToChat: 'Zum Chat hinzufügen' + }, + + preview: { + tab: 'Vorschau', + closeTab: label => `${label} schließen`, + closeOthers: 'Andere schließen', + closeToRight: 'Rechts schließen', + closeAll: 'Alle schließen', + closePane: 'Vorschau-Bereich schließen', + loading: 'Vorschau wird geladen', + unavailable: 'Vorschau nicht verfügbar', + opening: 'Öffne...', + hide: 'Ausblenden', + openPreview: 'Vorschau öffnen', + openInBrowser: 'Im Browser öffnen', + linkHint: '⌘/Ctrl-Klick für Vorschau-Bereich', + sourceLineTitle: 'Klicken zum Auswählen · Umschalt-Klick zum Erweitern · Ziehen in die Eingabe', + source: 'QUELLE', + renderedPreview: 'Vorschau', + diff: 'DIFF', + unknownSize: 'Unbekannte Größe', + binaryTitle: 'Dies sieht nach einer Binärdatei aus', + binaryBody: label => `Die Vorschau von ${label} zeigt möglicherweise unlesbaren Text.`, + largeTitle: 'Diese Datei ist groß', + largeBody: (label, size) => `${label} ist ${size}. Hermes zeigt nur die ersten 512 KB.`, + previewAnyway: 'Trotzdem vorab ansehen', + truncated: 'Erste 512 KB werden angezeigt.', + noInlineTitle: 'Keine Inline-Vorschau', + noInlineBody: mimeType => `${mimeType || 'Dieser Dateityp'} kann dennoch als Kontext angehängt werden.`, + edit: 'Bearbeiten', + editing: 'Bearbeiten', + unsavedChanges: 'Nicht gespeicherte Änderungen', + saveFailed: message => `Speichern fehlgeschlagen: ${message}`, + diskChangedTitle: 'Datei auf der Festplatte geändert', + diskChangedBody: + 'Diese Datei wurde seit dem Öffnen geändert. Mit deiner Version überschreiben oder deine Bearbeitungen verwerfen und neu laden?', + overwrite: 'Überschreiben', + discardReload: 'Verwerfen & neu laden', + console: { + deselect: 'Eintrag abwählen', + select: 'Eintrag auswählen', + copyFailed: 'Konsolenausgabe konnte nicht kopiert werden', + copyEntry: 'Diesen Eintrag kopieren', + sendEntry: 'Diesen Eintrag an den Chat senden', + messages: count => `${count} Konsolennachrichten`, + resize: 'Vorschau-Konsole skalieren', + title: 'Preview Console', + selected: count => `${count} ausgewählt`, + sendToChat: 'An Chat senden', + copySelected: 'Ausgewählte in Zwischenablage kopieren', + copyAll: 'Alle in Zwischenablage kopieren', + copy: 'Kopieren', + clear: 'Leeren', + empty: 'Noch keine Konsolennachrichten.', + promptHeader: 'Vorschau-Konsole:', + sentTitle: 'An Chat gesendet', + sentMessage: count => `${count} ${count === 1 ? 'Protokolleintrag' : 'Protokolleinträge'} zur Eingabe hinzugefügt` + }, + web: { + appFailedToBoot: 'Vorschau-App konnte nicht starten', + serverNotFound: 'Server nicht gefunden', + failedToLoad: 'Vorschau konnte nicht geladen werden', + tryAgain: 'Erneut versuchen', + restarting: 'Hermes wird neu gestartet...', + askRestart: 'Hermes bitten, den Server neu zu starten', + lookingRestart: taskId => `Hermes sucht nach einem Vorschau-Server zum Neustart (${taskId})`, + restartingTitle: 'Vorschau-Server wird neu gestartet', + restartingMessage: 'Hermes arbeitet im Hintergrund. Beobachte die Vorschau-Konsole für den Fortschritt.', + startRestartFailed: message => `Server-Neustart konnte nicht gestartet werden: ${message}`, + restartFailed: 'Server-Neustart fehlgeschlagen', + hideConsole: 'Vorschau-Konsole ausblenden', + showConsole: 'Vorschau-Konsole anzeigen', + hideDevTools: 'Vorschau-DevTools ausblenden', + openDevTools: 'Vorschau-DevTools öffnen', + finishedRestarting: message => `Hermes finished restarting the preview server${message ?`: ${message}` : ''}`, + failedRestarting: message => `Server-Neustart fehlgeschlagen: ${message}`, + unknownError: 'Unbekannter Fehler', + restartedTitle: 'Vorschau-Server neu gestartet', + reloadingNow: 'Vorschau wird jetzt neu geladen.', + restartFailedTitle: 'Vorschau-Neustart fehlgeschlagen', + restartFailedMessage: 'Hermes konnte den Server nicht neu starten.', + stillWorking: + 'Hermes arbeitet noch, aber noch kein Neustart-Ergebnis eingetroffen. Der Server-Befehl läuft möglicherweise im Vordergrund.', + workspaceReloading: 'Arbeitsbereich geändert, Vorschau wird neu geladen', + fileChanged: url => `Datei geändert, Vorschau wird neu geladen: ${url}`, + filesChanged: (count, url) => `${count} Dateiänderungen, Vorschau wird neu geladen: ${url}`, + watchFailed: message => `Vorschau-Datei konnte nicht überwacht werden: ${message}`, + moduleMimeDescription: + 'Modul-Skripte werden mit dem falschen MIME-Typ ausgeliefert. Das bedeutet meist, dass ein statischer Dateiserver eine Vite/React-App statt des Projekt-Dev-Servers ausliefert.', + loadFailedConsole: (code, message) => `Load failed${code ?` (${code})` : ''}: ${message}`, + unreachableDescription: 'Die Vorschau-Seite konnte nicht erreicht werden.', + openTarget: url => `${url} öffnen`, + fallbackTitle: 'Vorschau' + } + }, + + assistant: { + thread: { + loadingSession: 'Sitzung wird geladen', + showEarlier: 'Frühere Nachrichten anzeigen', + loadingResponse: 'Hermes lädt eine Antwort', + resumeWhenBackgroundDone: count => + count === 1 + ? 'Wird fortgesetzt, wenn ${count} Hintergrundaufgaben abgeschlossen sind' + : `Wird fortgesetzt, wenn ${count} Hintergrundaufgaben abgeschlossen sind`, + thinking: 'Denkt nach', + today: time => `Heute, ${time}`, + yesterday: time => `Gestern, ${time}`, + copy: 'Kopieren', + refresh: 'Aktualisieren', + moreActions: 'Weitere Aktionen', + branchNewChat: 'Zweig in neuem Chat', + dismissError: 'Fehler verwerfen', + readAloudFailed: 'Vorlesen fehlgeschlagen', + preparingAudio: 'Bereite Audio vor...', + stopReading: 'Vorlesen stoppen', + readAloud: 'Laut vorlesen', + editMessage: 'Nachricht bearbeiten', + expandMessage: 'Nachricht erweitern', + scrollToBottom: 'Nach unten scrollen', + stop: 'Stopp', + restorePrevious: 'Vorherigen Checkpoint wiederherstellen', + restoreCheckpoint: 'Checkpoint wiederherstellen', + restoreFromHere: 'Checkpoint wiederherstellen — ab diesem Prompt erneut ausführen', + restoreTitle: 'Zu diesem Checkpoint wiederherstellen?', + restoreBody: + 'Alles nach diesem Prompt wird aus dem Gespräch entfernt und der Prompt wird von hier erneut ausgeführt.', + restoreConfirm: 'Wiederherstellen & erneut ausführen', + restoreNext: 'Nächsten Checkpoint wiederherstellen', + goForward: 'Vorwärts gehen', + sendEdited: 'Bearbeitete Nachricht senden', + attachingFile: 'Hänge an…' + }, + approval: { + gatewayDisconnected: 'Hermes-Gateway ist nicht verbunden', + sendFailed: 'Freigabe-Antwort konnte nicht gesendet werden', + run: 'Ausführen', + command: 'Befehl', + moreOptions: 'Weitere Freigabeoptionen', + allowSession: 'Diese Sitzung erlauben', + alwaysAllowMenu: 'Immer erlauben…', + jumpToApproval: 'Freigabe erforderlich', + reject: 'Ablehnen', + alwaysTitle: 'Diesen Befehl immer erlauben?', + alwaysDescription: pattern => + `Dies fügt das Muster „${pattern}" zu deiner dauerhaften Allowlist (~/.hermes/config.yaml) hinzu. Hermes fragt bei solchen Befehlen nicht erneut — in dieser oder künftigen Sitzungen.`, + alwaysAllow: 'Immer erlauben' + }, + clarify: { + notReady: 'Klärungsanfrage ist noch nicht bereit', + gatewayDisconnected: 'Hermes-Gateway ist nicht verbunden', + sendFailed: 'Klärungsantwort konnte nicht gesendet werden', + loadingQuestion: 'Frage wird geladen…', + other: 'Andere (gib deine Antwort ein)', + placeholder: 'Gib deine Antwort ein…', + skip: 'Überspringen', + continueLabel: 'Fortfahren' + }, + tool: { + code: 'Code', + copyCode: 'Code kopieren', + renderingImage: 'Bild wird gerendert', + copyOutput: 'Ausgabe kopieren', + copyCommand: 'Befehl kopieren', + copyContent: 'Inhalt kopieren', + copyUrl: 'URL kopieren', + copyResults: 'Ergebnisse kopieren', + copyQuery: 'Abfrage kopieren', + copyFile: 'Datei kopieren', + copyPath: 'Pfad kopieren', + outputAlt: 'Werkzeug-Ausgabe', + rawResponse: 'Rohe Antwort', + copyActivity: 'Aktivität kopieren', + recoveredOne: 'Nach 1 fehlgeschlagenem Schritt wiederhergestellt', + recoveredMany: count => `Nach ${count} fehlgeschlagenen Schritten wiederhergestellt`, + failedOne: '1 Schritt fehlgeschlagen', + failedMany: count => `${count} Schritte fehlgeschlagen`, + statusRunning: 'Läuft', + statusError: 'Fehler', + statusRecovered: 'Wiederhergestellt', + statusDone: 'Fertig', + actions: { + read: 'Gelesen', + reading: 'Lese', + opened: 'Geöffnet', + opening: 'Öffne', + failedToOpen: 'Öffnen fehlgeschlagen', + searched: 'Durchsucht', + searching: 'Durchsuche', + ran: 'Ausgeführt', + running: 'Läuft', + ranCode: 'Code ausgeführt', + runningCode: 'Skripten' + }, + prefixes: { + browser: 'Browser', + web: 'Web' + }, + titleTemplates: { + actionCommand: (action, command) => `${action} ${command}`, + actionQuoted: (action, value) => `${action} „${value}"`, + actionTarget: (action, target) => `${action} ${target}`, + prefixedDone: (prefix, action) => `${prefix} ${action}`, + runningPrefixedTool: (prefix, action) => `Führe ${prefix.toLowerCase()} ${action.toLowerCase()} aus`, + runningTool: action => `Führe ${action.toLowerCase()} aus` + }, + titles: { + browser_click: { done: 'Seitenelement angeklickt', pending: 'Seitenelement wird angeklickt', pendingAction: 'Klicke' }, + browser_fill: { done: 'Formularfeld ausgefüllt', pending: 'Formularfeld wird ausgefüllt', pendingAction: 'Fülle aus' }, + browser_navigate: { done: 'Seite geöffnet', pending: 'Seite wird geöffnet', pendingAction: 'Öffne' }, + browser_snapshot: { + done: 'Seiten-Snapshot erfasst', + pending: 'Seiten-Snapshot wird erfasst', + pendingAction: 'Erfasse' + }, + browser_take_screenshot: { + done: 'Screenshot erfasst', + pending: 'Screenshot wird erfasst', + pendingAction: 'Erfasse' + }, + browser_type: { done: 'Auf Seite getippt', pending: 'Auf Seite wird getippt', pendingAction: 'Tippe' }, + clarify: { done: 'Eine Frage gestellt', pending: 'Frage wird gestellt', pendingAction: 'Frage' }, + cronjob: { done: 'Cron-Aufgabe', pending: 'Cron-Aufgabe wird geplant', pendingAction: 'Plane' }, + edit_file: { done: 'Datei bearbeitet', pending: 'Datei wird bearbeitet', pendingAction: 'Bearbeite' }, + execute_code: { done: 'Code ausgeführt', pending: 'Skript wird ausgeführt', pendingAction: 'Führe aus' }, + image_generate: { done: 'Bild generiert', pending: 'Bild wird generiert', pendingAction: 'Generiere' }, + list_files: { done: 'Dateien aufgelistet', pending: 'Dateien werden aufgelistet', pendingAction: 'Liste auf' }, + patch: { done: 'Datei gepatcht', pending: 'Datei wird gepatcht', pendingAction: 'Patche' }, + read_file: { done: 'Datei gelesen', pending: 'Datei wird gelesen', pendingAction: 'Lese' }, + search_files: { done: 'Dateien durchsucht', pending: 'Dateien werden durchsucht', pendingAction: 'Durchsuche' }, + session_search_recall: { + done: 'Sitzungsverlauf durchsucht', + pending: 'Sitzungsverlauf wird durchsucht', + pendingAction: 'Durchsuche' + }, + terminal: { done: 'Befehl ausgeführt', pending: 'Befehl wird ausgeführt', pendingAction: 'Wird ausgeführt' }, + todo: { done: 'Aufgaben aktualisiert', pending: 'Aufgaben werden aktualisiert', pendingAction: 'Wird aktualisiert' }, + vision_analyze: { done: 'Bild analysiert', pending: 'Bild wird analysiert', pendingAction: 'Wird analysiert' }, + web_extract: { done: 'Webseite gelesen', pending: 'Webseite wird gelesen', pendingAction: 'Wird gelesen' }, + web_search: { done: 'Web durchsucht', pending: 'Web wird durchsucht', pendingAction: 'Durchsuche' }, + write_file: { done: 'Datei bearbeitet', pending: 'Datei wird bearbeitet', pendingAction: 'Bearbeite' } + } + } + }, + + prompts: { + gatewayDisconnected: 'Hermes-Gateway ist nicht verbunden', + sudoSendFailed: 'Sudo-Passwort konnte nicht gesendet werden', + secretSendFailed: 'Geheimnis konnte nicht gesendet werden', + sudoTitle: 'Administrator-Passwort', + sudoDesc: 'Hermes benötigt dein Sudo-Passwort, um einen privilegierten Befehl auszuführen. Es wird nur an deinen lokalen Agenten gesendet.', + sudoPlaceholder: 'Sudo-Passwort', + secretTitle: 'Geheimnis erforderlich', + secretDesc: 'Hermes benötigt eine Anmeldedaten, um fortzufahren.', + secretPlaceholder: 'Geheimnis-Wert' + }, + + desktop: { + audioReadFailed: 'Aufgenommenes Audio konnte nicht gelesen werden', + sessionUnavailable: 'Sitzung nicht verfügbar', + createSessionFailed: 'Neue Sitzung konnte nicht erstellt werden', + promptFailed: 'Prompt fehlgeschlagen', + providerCredentialRequired: 'Füge vor deiner ersten Nachricht eine Anbieter-Anmeldedaten hinzu.', + emptySlashCommand: 'Leerer Slash-Befehl', + desktopCommands: 'Desktop-Befehle', + skillCommandsAvailable: count => `${count} Fähigkeiten-Befehle verfügbar.`, + warningLine: message => `Warnung: ${message}`, + yoloArmed: 'YOLO für diesen Chat scharf', + yoloOff: 'YOLO aus', + yoloSystem: active => `YOLO ${active ? 'ein' : 'aus'} für diese Sitzung`, + yoloTitle: 'YOLO', + yoloToggleFailed: 'YOLO konnte nicht umgeschaltet werden', + profileStatus: current => + `Profil: ${current}. Nutze /profile oder die Auswahl „Neue Sitzung", um einen Chat in einem anderen Profil zu starten.`, + unknownProfile: 'Unbekanntes Profil', + noProfileNamed: (target, available) => `Kein Profil namens „${target}". Verfügbar: ${available}`, + newChatsProfile: name => `Neue Chats verwenden Profil ${name}.`, + setProfileFailed: 'Profil konnte nicht festgelegt werden', + sttDisabled: 'Sprache-zu-Text ist in den Einstellungen deaktiviert.', + stopFailed: 'Stopp fehlgeschlagen', + regenerateFailed: 'Neu generieren fehlgeschlagen', + editFailed: 'Bearbeiten fehlgeschlagen', + resumeFailed: 'Fortsetzen fehlgeschlagen', + resumeStrandedTitle: 'Diese Sitzung konnte nicht geladen werden', + resumeStrandedBody: + 'Die Verbindung zu dieser Sitzung ist fehlgeschlagen und automatische Wiederholungen wurden aufgegeben. Prüfe, dass das Gateway läuft, und versuche es erneut.', + resumeRetry: 'Erneut versuchen', + nothingToBranch: 'Nichts zu verzweigen', + branchNeedsChat: 'Starte oder setze einen Chat fort, bevor du verzweigst.', + sessionBusy: 'Sitzung beschäftigt', + branchStopCurrent: 'Stoppe den aktuellen Schritt, bevor du diesen Chat verzweigst.', + branchNoText: 'Diese Nachricht hat keinen Text, von dem verzweigt werden kann.', + branchTitle: n => `Entwurf: Zweig #${n}`, + branchFailed: 'Verzweigung fehlgeschlagen', + deleteFailed: 'Löschen fehlgeschlagen', + archived: 'Archiviert', + archiveFailed: 'Archivieren fehlgeschlagen', + cwdChangeFailed: 'Arbeitsverzeichnis-Änderung fehlgeschlagen', + cwdStagedTitle: 'Arbeitsverzeichnis vorgemerkt', + cwdStagedMessage: 'Starte das Desktop-Backend neu, um cwd-Änderungen auf diese aktive Sitzung anzuwenden.', + modelSwitchFailed: 'Modellwechsel fehlgeschlagen', + sessionExported: 'Sitzung exportiert', + sessionExportFailed: 'Sitzung konnte nicht exportiert werden', + imageSaved: 'Bild gespeichert', + downloadStarted: 'Download gestartet', + restartToUseSaveImage: 'Starte Hermes Desktop neu, um Bild speichern zu nutzen.', + restartToSaveImages: 'Starte Hermes Desktop neu, um Bilder zu speichern', + imageDownloadFailed: 'Bild-Download fehlgeschlagen', + openImage: 'Bild öffnen', + downloadImage: 'Bild herunterladen', + savingImage: 'Bild wird gespeichert', + imagePreviewFailed: 'Bildvorschau fehlgeschlagen', + imageAttach: 'Bild anhängen', + imageWriteFailed: 'Bild konnte nicht auf die Festplatte geschrieben werden.', + imageAttachFailed: 'Bildanhang fehlgeschlagen', + attachImages: 'Bilder anhängen', + clipboard: 'Zwischenablage', + noClipboardImage: 'Kein Bild in der Zwischenablage gefunden', + clipboardPasteFailed: 'Zwischenablage-Einfügen fehlgeschlagen', + dropFiles: 'Dateien ablegen', + handoff: { + pickPlatform: 'Ziel wählen', + success: platform => `Übergeben an ${platform}. Jederzeit hier fortsetzen.`, + systemNote: platform => `↻ Übergeben an ${platform} — jederzeit hier fortsetzen.`, + failed: error => `Übergabe fehlgeschlagen: ${error}`, + timedOut: 'Zeitüberschreitung beim Warten auf das Gateway. Läuft `hermes gateway`?' + } + }, + + errors: { + genericFailure: 'Etwas ist schief gelaufen', + boundaryTitle: 'Etwas ging in der Oberfläche kaputt', + boundaryDesc: 'Die Ansicht stieß auf einen unerwarteten Fehler. Deine Chats und Einstellungen sind sicher.', + reloadWindow: 'Fenster neu laden', + openLogs: 'Protokolle öffnen' + }, + + ui: { + search: { + clear: 'Suche leeren' + }, + pagination: { + label: 'Paginierung', + previous: 'Zurück', + previousAria: 'Zur vorherigen Seite gehen', + next: 'Weiter', + nextAria: 'Zur nächsten Seite gehen' + }, + sidebar: { + title: 'Seitenleiste', + description: 'Zeigt die mobile Seitenleiste an.', + toggle: 'Seitenleiste umschalten' + } + } +}) \ No newline at end of file diff --git a/apps/desktop/src/i18n/languages.test.ts b/apps/desktop/src/i18n/languages.test.ts index 792aad5f586b2..5e3d1ed1fbdf4 100644 --- a/apps/desktop/src/i18n/languages.test.ts +++ b/apps/desktop/src/i18n/languages.test.ts @@ -15,23 +15,28 @@ describe('desktop i18n languages', () => { expect(normalizeLocale('zh_HK')).toBe('zh-hant') expect(normalizeLocale('ja')).toBe('ja') expect(normalizeLocale('ja-JP')).toBe('ja') + expect(normalizeLocale('de')).toBe('de') + expect(normalizeLocale('de-DE')).toBe('de') + expect(normalizeLocale('deutsch')).toBe('de') + expect(normalizeLocale('german')).toBe('de') }) it('falls back to English for empty or unsupported values', () => { expect(normalizeLocale(null)).toBe(DEFAULT_LOCALE) expect(normalizeLocale('')).toBe(DEFAULT_LOCALE) - expect(normalizeLocale('de')).toBe(DEFAULT_LOCALE) }) it('distinguishes exact locale ids from supported config aliases', () => { expect(isSupportedLocaleValue('zh-CN')).toBe(true) expect(isSupportedLocaleValue('zh-TW')).toBe(true) expect(isSupportedLocaleValue('ja-JP')).toBe(true) - expect(isSupportedLocaleValue('de')).toBe(false) + expect(isSupportedLocaleValue('de')).toBe(true) + expect(isSupportedLocaleValue('de-DE')).toBe(true) expect(isLocale('zh-CN')).toBe(false) expect(isLocale('zh')).toBe(true) expect(isLocale('zh-hant')).toBe(true) expect(isLocale('ja')).toBe(true) + expect(isLocale('de')).toBe(true) }) it('returns the persisted config value for supported locales', () => { @@ -39,5 +44,6 @@ describe('desktop i18n languages', () => { expect(localeConfigValue('zh')).toBe('zh') expect(localeConfigValue('zh-hant')).toBe('zh-hant') expect(localeConfigValue('ja')).toBe('ja') + expect(localeConfigValue('de')).toBe('de') }) }) diff --git a/apps/desktop/src/i18n/languages.ts b/apps/desktop/src/i18n/languages.ts index 96daf9b598dd9..ebb598972e29e 100644 --- a/apps/desktop/src/i18n/languages.ts +++ b/apps/desktop/src/i18n/languages.ts @@ -11,6 +11,12 @@ export const LOCALE_OPTIONS = [ englishName: 'English', configValue: 'en' }, + { + id: 'de', + name: 'Deutsch', + englishName: 'German', + configValue: 'de' + }, { id: 'zh', name: '简体中文', @@ -43,6 +49,11 @@ const LOCALE_ALIASES: Record = { en: 'en', 'en-us': 'en', en_us: 'en', + de: 'de', + 'de-de': 'de', + de_de: 'de', + deutsch: 'de', + german: 'de', zh: 'zh', 'zh-cn': 'zh', zh_cn: 'zh', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 851c794c20913..d26310023862e 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -5,7 +5,7 @@ // partial locales should use `defineLocale()` so missing desktop-only strings // fall back to English while new keys remain type-checked. -export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja' +export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja' | 'de' export type ToolTitleKey = | 'browser_click'