i18n bis in die Terminal- und Panel-Fenster

Uebersetzt waren bisher nur die Vue-Fenster; Terminal, Panel und Popup hatten
ihre Beschriftungen hart auf Deutsch im Markup und in den Views -- ein
englisches UI blieb zur Haelfte deutsch.

Die Tabellen ziehen nach messages.ts um, dazu ein Vue-freier t() und ein
applyI18n(), das statisches Markup ueber data-i18n-Marker beschriftet.
i18n.ts bleibt der vue-i18n-Wrapper fuer die Vue-Fenster. So kommt kein Vue
in die Terminal- und Panel-Bundles (Terminal 657,02 -> 657,06 kB).
Neue Namensraeume: panel, popup, commands, archiveForm, search, wiki.
toLocaleTimeString folgt der Sprache statt fest 'de'.

Zwei Funde nebenbei:
- window.localStorage wird ueber try/catch gelesen. Gesperrter Storage
  (privater Modus) wirft schon beim Lesen, und unter Node 22 ueberschattet
  ein undefiniertes globales localStorage das der Testumgebung.
- Die Frontend-Tests liefen unbemerkt gegen happy-doms en-US.
  test-setup.ts nagelt die Sprache fest.

49 Tests (39 bestehende, 10 neue fuer Tabellen-Paritaet, t() und applyI18n).
This commit is contained in:
marcusH
2026-07-20 22:36:19 +02:00
parent 92f34cb64d
commit 1fa9ffcf1b
17 changed files with 746 additions and 386 deletions
+7 -308
View File
@@ -1,320 +1,19 @@
/// vue-i18n-Wrapper für die Vue-Fenster (index.html). Die Tabellen liegen in
/// messages.ts und werden von der Terminal-/Panel-Schicht ohne Vue genutzt.
import { createI18n } from "vue-i18n";
const de = {
app: {
projects: "Projekte",
pools: "Pools",
usage: "Verbrauch",
autostart: "Autostart",
},
usage: {
days7: "7 Tage",
days30: "30 Tage",
pool: "Pool",
project: "Projekt",
input: "Input",
output: "Output",
cacheWrite: "Cache ↑",
cacheRead: "Cache ↓",
cost: "≈ Kosten",
total: "Summe",
empty: "Keine Verbrauchsdaten im Zeitraum.",
estimateNote:
"API-Gegenwert aus den lokalen Transcripts — keine Abrechnung, Historie nur solange claude sie vorhält.",
},
projects: {
project: "Projekt",
pool: "Pool",
noPool: " kein Pool ",
start: "Starten",
stop: "Beenden",
terminalSettings: "Terminal-Einstellungen",
stillRunning: "{name} läuft noch",
poolChangeSaved:
"Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
restartHint:
"Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei.",
keepRunning: "Weiterlaufen lassen",
restartNow: "Jetzt neu starten",
restarting: "Starte neu …",
terminal: "Terminal — {name}",
title: "Titel",
theme: "Theme",
dockIcon: "Dock-Icon",
chooseFile: "Datei wählen …",
remove: "Entfernen",
defaultIcon: "Standard-Icon",
appliesNextStart: "Gilt ab dem nächsten Terminal-Start.",
cancel: "Abbrechen",
save: "Speichern",
noPoolAssigned: "kein Pool",
needsPool: "Kein Pool zugeordnet",
needsKey: "Pool hat keinen API-Key",
newProject: "+ Projekt",
addFolder: "Projekt importieren …",
addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
addWorkDir: "Ordner hinzufügen …",
archive: "Archiv",
archiveNone: "kein Archiv — Panel ohne Wiki und Suche",
changeDir: "Ändern …",
wizardTitle: "Neues Projekt",
name: "Name",
projectDir: "Projektordner",
projectDirCustom: "Ordner wählen …",
workDir: "Arbeitsordner",
workDirNone: "keiner",
workDirCustom: "vorhandenen Ordner verknüpfen",
chooseFolder: "Ordner wählen …",
create: "Anlegen",
wizardHint:
"Angelegt werden Projektordner mit memory/, Sentinel-.gitignore, settings.json (Memory-Verzeichnis + Berechtigungen) und die Pool-/Terminal-Zuordnung.",
delete: "Projekt löschen",
deleteRunning: "Läuft noch — erst beenden",
deleteTitle: "{name} löschen",
deleteConfirm: "Entfernen",
deleteWorkDirs: "Arbeitsordner mitlöschen:",
scopeIntegration: "Nur Integration entfernen",
scopeIntegrationDesc:
"ai-control-Spuren werden entfernt; Projektordner, memory/ und Todoliste bleiben.",
scopeArchive: "Integration & Archiv",
scopeArchiveDesc: "zusätzlich {path} — endgültig.",
scopeFull: "Projekt komplett löschen",
scopeFullDesc: "zusätzlich der Projektordner {path} — endgültig.",
deletePreviewTitle: "Wird entfernt:",
docCount: "kein Dokument | 1 Dokument | {count} Dokumente",
artRegistry: "Registry-Eintrag",
artAiControl: ".ai-control/ (Config + Icon)",
artArchivePerm: "Archiv-Berechtigung in .claude/settings.json",
artTodoHook: "Todo-Hook in .claude/settings.json",
artPanelFiles: "Panel-Kanaldatei | {count} Panel-Kanaldateien",
artDesktop: ".desktop-Starter",
artArchive: "Archiv {path}",
artProjectDir: "Projektordner {path}",
artWorkDir: "Arbeitsordner {path}",
todo: "Todoliste",
todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
archiveChangeTitle: "Archiv wechseln",
archiveChangeText: "Das Archiv wechselt von {old} nach {neu}.",
archiveMigrate: "Dokumente ins neue Archiv verschieben",
archiveMigrateHint:
"Ohne Haken bleibt das bisherige Archiv unverändert liegen; die App verweist künftig nur noch auf das neue.",
archiveChangeConfirm: "Wechseln",
groupAppearance: "Darstellung",
groupFolders: "Ordner",
groupSession: "Session",
writesImmediately: "Änderungen hier schreiben sofort — ohne Speichern.",
},
pools: {
pool: "Pool",
type: "Typ",
actions: "Aktionen",
newOauth: "+ oAuth",
newApikey: "+ apiKey",
relogin: "Zurücksetzen",
rename: "Umbenennen",
renameTitle: "{name} umbenennen",
changeKey: "Key ändern",
insertKey: "Key eintragen",
delete: "Löschen",
assigned: "kein Projekt | 1 Projekt | {count} Projekte",
empty: "Noch keine Pools angelegt.",
newOauthPool: "Neuen oAuth-Pool anlegen",
newApikeyPool: "Neuen API-Key-Pool anlegen",
editKey: "API-Key ändern {name}",
name: "Name",
apiKey: "API-Key",
oauthHint:
"Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
cancel: "Abbrechen",
createPool: "Anlegen",
reset: "Zurücksetzen",
save: "Speichern",
deletePool: "Pool löschen",
deleteWarning:
"Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
deleteBlockedTooltip:
"Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
reloginTitle: "{name} zurücksetzen",
reloginWarning:
"Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
reloginNoEntry:
"Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
reloginBlocked:
"Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
keychainUnavailableTitle: "Keine sichere Ablage",
keychainUnavailable:
"Keychain/Keyring ist nicht verfügbar. Der Key kann stattdessen ungesichert als Datei im Pool-Ordner liegen (0600).",
storeAsFile: "Als Datei ablegen",
},
};
const en: typeof de = {
app: {
projects: "Projects",
pools: "Pools",
usage: "Usage",
autostart: "Autostart",
},
usage: {
days7: "7 days",
days30: "30 days",
pool: "Pool",
project: "Project",
input: "Input",
output: "Output",
cacheWrite: "Cache ↑",
cacheRead: "Cache ↓",
cost: "≈ cost",
total: "Total",
empty: "No usage data in this period.",
estimateNote:
"API equivalent from local transcripts — not a bill; history only as long as claude keeps it.",
},
projects: {
project: "Project",
pool: "Pool",
noPool: " no pool ",
start: "Start",
stop: "Stop",
terminalSettings: "Terminal settings",
stillRunning: "{name} is still running",
poolChangeSaved:
"The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
restartHint:
"Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped.",
keepRunning: "Keep running",
restartNow: "Restart now",
restarting: "Restarting …",
terminal: "Terminal — {name}",
title: "Title",
theme: "Theme",
dockIcon: "Dock icon",
chooseFile: "Choose file …",
remove: "Remove",
defaultIcon: "Default icon",
appliesNextStart: "Applies from the next terminal start.",
cancel: "Cancel",
save: "Save",
noPoolAssigned: "no pool",
needsPool: "No pool assigned",
needsKey: "Pool has no API key",
newProject: "+ Project",
addFolder: "Import project …",
addFolderTitle: "Add an existing folder as a project",
addWorkDir: "Add folder …",
archive: "Archive",
archiveNone: "no archive — panel without wiki and search",
changeDir: "Change …",
wizardTitle: "New project",
name: "Name",
projectDir: "Project folder",
projectDirCustom: "Choose folder …",
workDir: "Working folder",
workDirNone: "none",
workDirCustom: "link existing folder",
chooseFolder: "Choose folder …",
create: "Create",
wizardHint:
"Creates the project folder with memory/, sentinel .gitignore, settings.json (memory dir + permissions), and the pool/terminal assignment.",
delete: "Delete project",
deleteRunning: "Still running — stop it first",
deleteTitle: "Delete {name}",
deleteConfirm: "Remove",
deleteWorkDirs: "Also delete working folders:",
scopeIntegration: "Remove integration only",
scopeIntegrationDesc:
"ai-control traces are removed; project folder, memory/ and todo list stay.",
scopeArchive: "Integration & archive",
scopeArchiveDesc: "additionally {path} — permanent.",
scopeFull: "Delete project completely",
scopeFullDesc: "additionally the project folder {path} — permanent.",
deletePreviewTitle: "Will be removed:",
docCount: "no documents | 1 document | {count} documents",
artRegistry: "registry entry",
artAiControl: ".ai-control/ (config + icon)",
artArchivePerm: "archive permission in .claude/settings.json",
artTodoHook: "todo hook in .claude/settings.json",
artPanelFiles: "panel channel file | {count} panel channel files",
artDesktop: ".desktop launcher",
artArchive: "archive {path}",
artProjectDir: "project folder {path}",
artWorkDir: "working folder {path}",
todo: "Todo list",
todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
archiveChangeTitle: "Change archive",
archiveChangeText: "The archive changes from {old} to {neu}.",
archiveMigrate: "Move documents to the new archive",
archiveMigrateHint:
"Unchecked, the previous archive stays untouched; the app just points to the new one.",
archiveChangeConfirm: "Change",
groupAppearance: "Appearance",
groupFolders: "Folders",
groupSession: "Session",
writesImmediately: "Changes here are written immediately — no save step.",
},
pools: {
pool: "Pool",
type: "Type",
actions: "Actions",
newOauth: "+ oAuth",
newApikey: "+ apiKey",
relogin: "Reset",
rename: "Rename",
renameTitle: "Rename {name}",
changeKey: "Change key",
insertKey: "Insert key",
delete: "Delete",
assigned: "no projects | 1 project | {count} projects",
empty: "No pools yet.",
newOauthPool: "Create oAuth pool",
newApikeyPool: "Create API key pool",
editKey: "Change API key {name}",
name: "Name",
apiKey: "API key",
oauthHint:
"The pool is created; claude signs in itself on first start via /login (browser against your subscription).",
cancel: "Cancel",
createPool: "Create",
reset: "Reset",
save: "Save",
deletePool: "Delete pool",
deleteWarning:
"Pool {name} and its credential file will be deleted. This cannot be undone.",
deleteUnassigns: "These projects lose their pool assignment:",
deleteBlockedTooltip:
"Deletion blocked — active: {projects}. Stop the session first.",
reloginTitle: "Reset {name}",
reloginWarning:
"The access token for {name} stored in the keychain will be deleted. On the next start claude asks for /login again.",
reloginNoEntry:
"No keychain entry present — claude signs in via /login on the next start anyway.",
reloginBlocked:
"Resetting requires an unused pool. These projects are still running:",
keychainUnavailableTitle: "No secure storage",
keychainUnavailable:
"Keychain/keyring is unavailable. The key can instead be stored unprotected as a file in the pool folder (0600).",
storeAsFile: "Store as file",
},
};
const stored = localStorage.getItem("lang");
const initial =
stored === "de" || stored === "en"
? stored
: navigator.language.startsWith("de")
? "de"
: "en";
import { messages, storedLocale, type Locale } from "./messages";
export const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: initial,
locale: storedLocale(),
fallbackLocale: "en",
messages: { de, en },
messages,
});
export function setLocale(lang: "de" | "en") {
export function setLocale(lang: Locale) {
i18n.global.locale.value = lang;
localStorage.setItem("lang", lang);
}