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:
+17
-17
@@ -2,7 +2,7 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Dokument</title>
|
||||
<title data-i18n="panel.windowDocument">Dokument</title>
|
||||
<style>
|
||||
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
||||
:root {
|
||||
@@ -375,33 +375,33 @@
|
||||
<body>
|
||||
<div class="panel-topbar" data-tauri-drag-region>
|
||||
<div class="panel-tabs" id="panel-tabs">
|
||||
<button class="panel-btn" data-mode="commands" title="Befehls-History">Befehle</button>
|
||||
<button class="panel-btn" data-mode="commands" data-i18n-title="panel.tabCommandsTitle" data-i18n="panel.tabCommands">Befehle</button>
|
||||
<span class="tab-sep"></span>
|
||||
<button class="panel-btn" data-mode="draft" title="Dokument">Dokument</button>
|
||||
<button class="panel-btn" data-mode="wiki" title="Archiv-Wiki">Wiki</button>
|
||||
<button class="panel-btn" data-mode="search" title="Suchtreffer">Suche</button>
|
||||
<button class="panel-btn" data-mode="draft" data-i18n-title="panel.tabDraftTitle" data-i18n="panel.tabDraft">Dokument</button>
|
||||
<button class="panel-btn" data-mode="wiki" data-i18n-title="panel.tabWikiTitle" data-i18n="panel.tabWiki">Wiki</button>
|
||||
<button class="panel-btn" data-mode="search" data-i18n-title="panel.tabSearchTitle" data-i18n="panel.tabSearch">Suche</button>
|
||||
<span class="tab-sep"></span>
|
||||
</div>
|
||||
<button class="winbtn" id="panel-dock" title="Wieder andocken">
|
||||
<button class="winbtn" id="panel-dock" data-i18n-title="panel.dock">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5" /><path d="M9.5 2.5v11" /><path d="M4 8h3.4M6 6.2 7.8 8 6 9.8" /></svg>
|
||||
</button>
|
||||
<button class="winbtn osctl" id="win-min" aria-label="Minimieren">
|
||||
<button class="winbtn osctl" id="win-min" data-i18n-aria="panel.minimize">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="2" y1="6.5" x2="10" y2="6.5" /></svg>
|
||||
</button>
|
||||
<button class="winbtn osctl" id="win-max" aria-label="Maximieren">
|
||||
<button class="winbtn osctl" id="win-max" data-i18n-aria="panel.maximize">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><rect x="2.5" y="2.5" width="7" height="7" rx="1" /></svg>
|
||||
</button>
|
||||
<button class="winbtn close" id="panel-close" title="Panel schließen">
|
||||
<button class="winbtn close" id="panel-close" data-i18n-title="panel.closePanel">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-head">
|
||||
<div class="panel-titlerow" data-tauri-drag-region>
|
||||
<span class="panel-title">Dokument</span>
|
||||
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
||||
<span class="panel-title" data-i18n="panel.tabDraft">Dokument</span>
|
||||
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" data-i18n-title="panel.editTitle">✎</button>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<select class="panel-lang draft-only" id="panel-lang" title="Sprache der Rechtschreibprüfung">
|
||||
<select class="panel-lang draft-only" id="panel-lang" data-i18n-title="panel.spellcheckLang">
|
||||
<option value="de">DE</option>
|
||||
<option value="en">EN</option>
|
||||
<option value="en-GB">EN-GB</option>
|
||||
@@ -410,17 +410,17 @@
|
||||
<option value="it">IT</option>
|
||||
<option value="nl">NL</option>
|
||||
</select>
|
||||
<button class="panel-btn draft-only" id="panel-wiki-jump" title="Im Wiki öffnen">
|
||||
<button class="panel-btn draft-only" id="panel-wiki-jump" data-i18n-title="panel.openInWiki">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M8 3.5C6.6 2.4 4.6 2 2.5 2v11c2.1 0 4.1.4 5.5 1.5 1.4-1.1 3.4-1.5 5.5-1.5V2c-2.1 0-4.1.4-5.5 1.5z" /><path d="M8 3.5v11" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
||||
<button class="panel-btn draft-only" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
||||
<button class="panel-btn draft-only" id="panel-mode" data-i18n-title="panel.toggleRaw" data-i18n="panel.rendered">MD</button>
|
||||
<button class="panel-btn draft-only" id="panel-content-edit" data-i18n-title="panel.editDraft">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn draft-only" id="panel-copy" title="In die Zwischenablage kopieren">
|
||||
<button class="panel-btn draft-only" id="panel-copy" data-i18n-title="panel.copy">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn draft-only" id="panel-archive" title="In den Archiv-Ordner speichern">
|
||||
<button class="panel-btn draft-only" id="panel-archive" data-i18n-title="panel.archive">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Projekte</title>
|
||||
<title data-i18n="panel.windowProjects">Projekte</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
||||
+6
-4
@@ -3,6 +3,8 @@
|
||||
/// archivieren, Escape schließt. Gemeinsam für das angedockte Panel und das
|
||||
/// abgelöste Fenster.
|
||||
|
||||
import { t } from "./messages";
|
||||
|
||||
export interface ArchiveFormMeta {
|
||||
folder?: string;
|
||||
description?: string;
|
||||
@@ -23,12 +25,12 @@ export function initArchiveForm(
|
||||
const form = document.createElement("div");
|
||||
form.className = "archive-form";
|
||||
form.hidden = true;
|
||||
const folder = field("Ordner — z. B. konzepte/panel");
|
||||
const desc = field("Beschreibung");
|
||||
const tags = field("Schlagwörter, kommagetrennt");
|
||||
const folder = field(t("archiveForm.folder"));
|
||||
const desc = field(t("archiveForm.description"));
|
||||
const tags = field(t("archiveForm.tags"));
|
||||
const submit = document.createElement("button");
|
||||
submit.className = "archive-form-submit";
|
||||
submit.textContent = "Archivieren";
|
||||
submit.textContent = t("archiveForm.submit");
|
||||
form.append(folder, desc, tags, submit);
|
||||
document.body.append(form);
|
||||
|
||||
|
||||
+14
-12
@@ -6,6 +6,7 @@
|
||||
/// Fremdtext und gehen nie durch innerHTML.
|
||||
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { storedLocale, t } from "./messages";
|
||||
|
||||
|
||||
/// Entfernt Bidi- und Zero-Width-Steuerzeichen (U+200B–200F, U+202A–202E,
|
||||
@@ -34,10 +35,11 @@ export interface CommandsView {
|
||||
|
||||
function fmtTime(ts: number): string {
|
||||
const d = new Date(ts * 1000);
|
||||
const hm = d.toLocaleTimeString("de", { hour: "2-digit", minute: "2-digit" });
|
||||
const loc = storedLocale();
|
||||
const hm = d.toLocaleTimeString(loc, { hour: "2-digit", minute: "2-digit" });
|
||||
return d.toDateString() === new Date().toDateString()
|
||||
? hm
|
||||
: `${d.toLocaleDateString("de")} ${hm}`;
|
||||
: `${d.toLocaleDateString(loc)} ${hm}`;
|
||||
}
|
||||
|
||||
/// Kurzes visuelles Feedback (copied/error) — der eine Flash-Helper fürs
|
||||
@@ -59,7 +61,7 @@ export function panelToast(msg: string) {
|
||||
function copyBtn(text: () => string): HTMLButtonElement {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "panel-btn cmd-copy";
|
||||
btn.title = "Befehl kopieren";
|
||||
btn.title = t("commands.copyOne");
|
||||
btn.innerHTML =
|
||||
'<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>';
|
||||
btn.addEventListener("click", async () => {
|
||||
@@ -72,7 +74,7 @@ function copyBtn(text: () => string): HTMLButtonElement {
|
||||
function deleteBtn(onClick: () => void): HTMLButtonElement {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "panel-btn cmd-del";
|
||||
btn.title = "Aus der History entfernen";
|
||||
btn.title = t("commands.removeOne");
|
||||
btn.innerHTML =
|
||||
'<svg width="14" height="14" viewBox="0 0 16 16"><path d="M2.5 4.5h11" /><path d="M5.5 4.5V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1.5" /><path d="M4 4.5l.7 8.6a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9l.7-8.6" /></svg>';
|
||||
btn.addEventListener("click", onClick);
|
||||
@@ -96,7 +98,7 @@ export function initCommandsView(
|
||||
if (rec.session) {
|
||||
const sep = document.createElement("div");
|
||||
sep.className = "cmd-sep";
|
||||
sep.textContent = `Session · ${fmtTime(rec.ts)}`;
|
||||
sep.textContent = `${t("commands.session")} · ${fmtTime(rec.ts)}`;
|
||||
container.append(sep);
|
||||
continue;
|
||||
}
|
||||
@@ -113,7 +115,7 @@ export function initCommandsView(
|
||||
if (cmds.length > 1) {
|
||||
const all = document.createElement("button");
|
||||
all.className = "cmd-all";
|
||||
all.textContent = "Alle kopieren";
|
||||
all.textContent = t("commands.copyAll");
|
||||
all.addEventListener("click", async () => {
|
||||
await writeText(cmds.map((c) => stripInvisibles(c.cmd)).join("\n"));
|
||||
flash(all, "copied");
|
||||
@@ -166,10 +168,10 @@ export function initCommandsView(
|
||||
export type PanelMode = "draft" | "commands" | "search" | "wiki";
|
||||
|
||||
const LABEL: { [m in PanelMode]: string } = {
|
||||
draft: "Dokument",
|
||||
commands: "Befehle",
|
||||
search: "Suche",
|
||||
wiki: "Wiki",
|
||||
draft: t("panel.tabDraft"),
|
||||
commands: t("panel.tabCommands"),
|
||||
search: t("panel.tabSearch"),
|
||||
wiki: t("panel.tabWiki"),
|
||||
};
|
||||
|
||||
/// Vier Tabs Entwurf / Befehle / Suche / Wiki: blenden Entwurfs-Inhalt samt
|
||||
@@ -202,7 +204,7 @@ export function initPanelMode(opts: {
|
||||
function to(m: PanelMode) {
|
||||
if (m === mode) return;
|
||||
opts.flush();
|
||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || "Dokument";
|
||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || t("panel.tabDraft");
|
||||
mode = m;
|
||||
apply();
|
||||
if (m === "draft") opts.titleEl.textContent = draftTitle;
|
||||
@@ -211,7 +213,7 @@ export function initPanelMode(opts: {
|
||||
/// Auswahl aufheben (Panel zugeklappt) — der nächste to()-Aufruf oder
|
||||
/// Tab-Klick wählt wieder aus.
|
||||
function clear() {
|
||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || "Dokument";
|
||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || t("panel.tabDraft");
|
||||
mode = null;
|
||||
for (const t of tabs) t.classList.remove("active");
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ onUnmounted(() => {
|
||||
</ul>
|
||||
|
||||
<footer class="ft">
|
||||
<button class="fbtn" @click="openMain">Öffnen</button>
|
||||
<button class="fbtn danger" @click="quit">Beenden</button>
|
||||
<button class="fbtn" @click="openMain">{{ $t("popup.open") }}</button>
|
||||
<button class="fbtn danger" @click="quit">{{ $t("popup.quit") }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+7
-308
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Übersetzungstabellen und der Vue-freie t()/applyI18n()-Helfer.
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { applyI18n, messages, storedLocale, t } from "./messages";
|
||||
|
||||
/// Alle Punkt-Pfade eines Nachrichtenbaums, für den Vollständigkeitsabgleich.
|
||||
function keys(obj: object, prefix = ""): string[] {
|
||||
return Object.entries(obj).flatMap(([k, v]) =>
|
||||
typeof v === "object" && v !== null
|
||||
? keys(v, `${prefix}${k}.`)
|
||||
: [`${prefix}${k}`],
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
window.localStorage.removeItem("lang");
|
||||
} catch {
|
||||
/* Storage in der Testumgebung nicht verfügbar */
|
||||
}
|
||||
document.body.textContent = "";
|
||||
});
|
||||
|
||||
describe("Nachrichtentabellen", () => {
|
||||
it("führen dieselben Schlüssel in beiden Sprachen", () => {
|
||||
expect(keys(messages.en).sort()).toEqual(keys(messages.de).sort());
|
||||
});
|
||||
|
||||
it("lassen keinen Text unübersetzt stehen", () => {
|
||||
// Gleiche Texte sind fast immer eine vergessene Übersetzung. Erlaubt sind
|
||||
// nur Eigennamen und Zeichen ohne Sprache.
|
||||
const erlaubt = new Set([
|
||||
"Pool",
|
||||
"Pools",
|
||||
"Name",
|
||||
"Theme",
|
||||
"Wiki",
|
||||
"MD",
|
||||
"Autostart",
|
||||
"Input",
|
||||
"Output",
|
||||
"Session",
|
||||
"Cache ↑",
|
||||
"Cache ↓",
|
||||
"+ oAuth",
|
||||
"+ apiKey",
|
||||
"Terminal",
|
||||
"Terminal — {name}",
|
||||
".desktop-Starter",
|
||||
".ai-control/ (Config + Icon)",
|
||||
"Archiv-Berechtigung in .claude/settings.json",
|
||||
"Todo-Hook in .claude/settings.json",
|
||||
"OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
|
||||
]);
|
||||
const gleich = keys(messages.de).filter((k) => {
|
||||
const de = pick(messages.de, k);
|
||||
return de === pick(messages.en, k) && !erlaubt.has(de);
|
||||
});
|
||||
expect(gleich).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function pick(obj: object, key: string): string {
|
||||
return key
|
||||
.split(".")
|
||||
.reduce<never>((o, part) => (o as Record<string, never>)[part], obj as never);
|
||||
}
|
||||
|
||||
describe("t()", () => {
|
||||
it("löst Punkt-Pfade auf", () => {
|
||||
expect(t("panel.tabDraft")).toBe("Dokument");
|
||||
});
|
||||
|
||||
it("setzt Platzhalter ein", () => {
|
||||
expect(t("search.noHits", { scope: "„x“" })).toBe("Keine Treffer für „x“");
|
||||
});
|
||||
|
||||
it("lässt unbekannte Platzhalter stehen", () => {
|
||||
expect(t("wiki.emptyTag", { falsch: "x" })).toBe("Keine Dokumente mit #{tag}.");
|
||||
});
|
||||
|
||||
it("gibt bei fehlendem Schlüssel den Pfad zurück", () => {
|
||||
expect(t("panel.gibtEsNicht")).toBe("panel.gibtEsNicht");
|
||||
expect(t("nichts.da.tief")).toBe("nichts.da.tief");
|
||||
});
|
||||
|
||||
it("folgt der gespeicherten Sprache", () => {
|
||||
window.localStorage.setItem("lang", "en");
|
||||
expect(storedLocale()).toBe("en");
|
||||
expect(t("panel.tabDraft")).toBe("Document");
|
||||
});
|
||||
|
||||
it("fällt bei unbekannter Sprache auf die Browsersprache zurück", () => {
|
||||
window.localStorage.setItem("lang", "kli");
|
||||
expect(storedLocale()).toBe("de"); // test-setup.ts pinnt navigator.language
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyI18n()", () => {
|
||||
it("beschriftet Text und Attribute", () => {
|
||||
document.body.innerHTML = `
|
||||
<button data-i18n="panel.tabWiki" data-i18n-title="panel.tabWikiTitle">alt</button>
|
||||
<button data-i18n-aria="panel.close"></button>
|
||||
<input data-i18n-placeholder="search.placeholder" />`;
|
||||
applyI18n();
|
||||
const btn = document.querySelector("button")!;
|
||||
expect(btn.textContent).toBe("Wiki");
|
||||
expect(btn.title).toBe("Archiv-Wiki");
|
||||
expect(document.querySelectorAll("button")[1].getAttribute("aria-label")).toBe(
|
||||
"Schließen",
|
||||
);
|
||||
expect(document.querySelector("input")!.placeholder).toBe(
|
||||
"Archiv durchsuchen — #tag filtert",
|
||||
);
|
||||
expect(document.documentElement.lang).toBe("de");
|
||||
});
|
||||
|
||||
it("lässt Markup ohne Marker unangetastet", () => {
|
||||
document.body.innerHTML = `<span>unberührt</span>`;
|
||||
applyI18n();
|
||||
expect(document.querySelector("span")!.textContent).toBe("unberührt");
|
||||
});
|
||||
});
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
/// Übersetzungstabellen für alle Fenster plus ein Vue-freier `t()`-Helfer.
|
||||
/// Die Vue-Fenster (index.html) ziehen die Tabellen über i18n.ts in vue-i18n;
|
||||
/// Terminal-, Panel- und Popup-Fenster haben kein Vue und nutzen `t()` und
|
||||
/// `applyI18n()` direkt — sonst landete vue-i18n samt Vue in ihren Bundles.
|
||||
|
||||
export type Locale = "de" | "en";
|
||||
|
||||
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",
|
||||
},
|
||||
panel: {
|
||||
windowTerminal: "Terminal",
|
||||
windowDocument: "Dokument",
|
||||
windowProjects: "Projekte",
|
||||
tabCommands: "Befehle",
|
||||
tabCommandsTitle: "Befehls-History",
|
||||
tabDraft: "Dokument",
|
||||
tabDraftTitle: "Dokument",
|
||||
tabWiki: "Wiki",
|
||||
tabWikiTitle: "Archiv-Wiki",
|
||||
tabSearch: "Suche",
|
||||
tabSearchTitle: "Suchtreffer",
|
||||
minimize: "Minimieren",
|
||||
maximize: "Maximieren",
|
||||
close: "Schließen",
|
||||
closePanel: "Panel schließen",
|
||||
hidePanel: "Panel ausblenden",
|
||||
detach: "In eigenes Fenster ablösen",
|
||||
dock: "Wieder andocken",
|
||||
editTitle: "Titel bearbeiten",
|
||||
spellcheckLang: "Sprache der Rechtschreibprüfung",
|
||||
openInWiki: "Im Wiki öffnen",
|
||||
toggleRaw: "Rohtext / gerendert",
|
||||
rendered: "MD",
|
||||
raw: "Roh",
|
||||
editDraft: "Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)",
|
||||
copy: "In die Zwischenablage kopieren",
|
||||
archive: "In den Archiv-Ordner speichern",
|
||||
chooseArchiveDir: "Archiv-Ordner wählen",
|
||||
processEnded: "[Prozess beendet]",
|
||||
},
|
||||
popup: {
|
||||
open: "Öffnen",
|
||||
quit: "Beenden",
|
||||
},
|
||||
commands: {
|
||||
copyOne: "Befehl kopieren",
|
||||
removeOne: "Aus der History entfernen",
|
||||
copyAll: "Alle kopieren",
|
||||
session: "Session",
|
||||
},
|
||||
archiveForm: {
|
||||
folder: "Ordner — z. B. konzepte/panel",
|
||||
description: "Beschreibung",
|
||||
tags: "Schlagwörter, kommagetrennt",
|
||||
submit: "Archivieren",
|
||||
},
|
||||
search: {
|
||||
placeholder: "Archiv durchsuchen — #tag filtert",
|
||||
minChars: "Mindestens 3 Zeichen — oder Enter.",
|
||||
hits: "{count} Treffer {scope}",
|
||||
noHits: "Keine Treffer für {scope}",
|
||||
},
|
||||
wiki: {
|
||||
archive: "Archiv",
|
||||
docOne: "{count} Dokument",
|
||||
docMany: "{count} Dokumente",
|
||||
all: "Alle",
|
||||
backlinks: "Verweise hierher",
|
||||
backlinksLabel: "Verweise hierher: ",
|
||||
emptyTag: "Keine Dokumente mit #{tag}.",
|
||||
emptyArchive: "Das Archiv ist leer.",
|
||||
emptyHint:
|
||||
"Archivieren: Archiv-Button im Entwurf oder „archiviere das“ im Chat — mit Ordner, Beschreibung und Schlagwörtern.",
|
||||
recent: "Zuletzt",
|
||||
root: "Wurzel",
|
||||
back: "‹ Archiv",
|
||||
noPage: "Keine Wiki-Seite geladen.",
|
||||
openOverview: "Archiv-Übersicht öffnen",
|
||||
},
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
panel: {
|
||||
windowTerminal: "Terminal",
|
||||
windowDocument: "Document",
|
||||
windowProjects: "Projects",
|
||||
tabCommands: "Commands",
|
||||
tabCommandsTitle: "Command history",
|
||||
tabDraft: "Document",
|
||||
tabDraftTitle: "Document",
|
||||
tabWiki: "Wiki",
|
||||
tabWikiTitle: "Archive wiki",
|
||||
tabSearch: "Search",
|
||||
tabSearchTitle: "Search hits",
|
||||
minimize: "Minimize",
|
||||
maximize: "Maximize",
|
||||
close: "Close",
|
||||
closePanel: "Close panel",
|
||||
hidePanel: "Hide panel",
|
||||
detach: "Detach into its own window",
|
||||
dock: "Dock again",
|
||||
editTitle: "Edit title",
|
||||
spellcheckLang: "Spell-check language",
|
||||
openInWiki: "Open in wiki",
|
||||
toggleRaw: "Raw text / rendered",
|
||||
rendered: "MD",
|
||||
raw: "Raw",
|
||||
editDraft: "Edit draft (Cmd/Ctrl+Enter saves, Esc discards)",
|
||||
copy: "Copy to clipboard",
|
||||
archive: "Save to the archive folder",
|
||||
chooseArchiveDir: "Choose archive folder",
|
||||
processEnded: "[process ended]",
|
||||
},
|
||||
popup: {
|
||||
open: "Open",
|
||||
quit: "Quit",
|
||||
},
|
||||
commands: {
|
||||
copyOne: "Copy command",
|
||||
removeOne: "Remove from history",
|
||||
copyAll: "Copy all",
|
||||
session: "Session",
|
||||
},
|
||||
archiveForm: {
|
||||
folder: "Folder — e.g. concepts/panel",
|
||||
description: "Description",
|
||||
tags: "Tags, comma-separated",
|
||||
submit: "Archive",
|
||||
},
|
||||
search: {
|
||||
placeholder: "Search the archive — #tag filters",
|
||||
minChars: "At least 3 characters — or press Enter.",
|
||||
hits: "{count} hits {scope}",
|
||||
noHits: "No hits for {scope}",
|
||||
},
|
||||
wiki: {
|
||||
archive: "Archive",
|
||||
docOne: "{count} document",
|
||||
docMany: "{count} documents",
|
||||
all: "All",
|
||||
backlinks: "Links here",
|
||||
backlinksLabel: "Links here: ",
|
||||
emptyTag: "No documents tagged #{tag}.",
|
||||
emptyArchive: "The archive is empty.",
|
||||
emptyHint:
|
||||
"To archive: the archive button in the draft, or “archive this” in the chat — with folder, description and tags.",
|
||||
recent: "Recent",
|
||||
root: "Root",
|
||||
back: "‹ Archive",
|
||||
noPage: "No wiki page loaded.",
|
||||
openOverview: "Open archive overview",
|
||||
},
|
||||
};
|
||||
|
||||
export const messages = { de, en };
|
||||
|
||||
/// Gewählte Sprache: der Schalter aus dem Hauptfenster (localStorage), sonst
|
||||
/// die Browser-Sprache. Beide Wege teilen sich den Schlüssel `lang` mit
|
||||
/// setLocale() in i18n.ts.
|
||||
export function storedLocale(): Locale {
|
||||
// Über window und in try/catch: gesperrter Storage (privater Modus) wirft
|
||||
// schon beim Lesen, und unter Node schattet ein undefiniertes Global das
|
||||
// localStorage der Testumgebung.
|
||||
let stored: string | null = null;
|
||||
try {
|
||||
stored = window.localStorage.getItem("lang");
|
||||
} catch {
|
||||
stored = null;
|
||||
}
|
||||
if (stored === "de" || stored === "en") return stored;
|
||||
return navigator.language.startsWith("de") ? "de" : "en";
|
||||
}
|
||||
|
||||
/// Übersetzt einen Punkt-Pfad (`panel.tabDraft`) und setzt `{platzhalter}` ein.
|
||||
/// Fehlt der Schlüssel, kommt der Pfad selbst zurück — im UI sichtbar statt
|
||||
/// still leer.
|
||||
export function t(key: string, params?: Record<string, string | number>): string {
|
||||
const table: unknown = messages[storedLocale()];
|
||||
const hit = key
|
||||
.split(".")
|
||||
.reduce<unknown>((o, part) => (o as Record<string, unknown>)?.[part], table);
|
||||
if (typeof hit !== "string") return key;
|
||||
return params
|
||||
? hit.replace(/\{(\w+)\}/g, (m, name) => String(params[name] ?? m))
|
||||
: hit;
|
||||
}
|
||||
|
||||
/// Beschriftet statisches Markup: `data-i18n` setzt den Textinhalt (auch am
|
||||
/// <title> im Kopf), `data-i18n-title`/`-aria`/`-placeholder` das jeweilige
|
||||
/// Attribut. Einmal beim Fensterstart aufgerufen — die Sprache wechselt erst
|
||||
/// mit dem nächsten Start, es gibt in diesen Fenstern keinen Umschalter.
|
||||
export function applyI18n(root: ParentNode = document) {
|
||||
for (const el of root.querySelectorAll<HTMLElement>("[data-i18n]")) {
|
||||
el.textContent = t(el.dataset.i18n!);
|
||||
}
|
||||
const attrs: [string, string][] = [
|
||||
["data-i18n-title", "title"],
|
||||
["data-i18n-aria", "aria-label"],
|
||||
["data-i18n-placeholder", "placeholder"],
|
||||
];
|
||||
for (const [marker, attr] of attrs) {
|
||||
for (const el of root.querySelectorAll<HTMLElement>(`[${marker}]`)) {
|
||||
el.setAttribute(attr, t(el.getAttribute(marker)!));
|
||||
}
|
||||
}
|
||||
document.documentElement.lang = storedLocale();
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { renderMarkdown } from "./markdown";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { flash } from "./commands-view";
|
||||
import { t } from "./messages";
|
||||
|
||||
export interface PanelView {
|
||||
set(text: string): void;
|
||||
@@ -37,7 +38,7 @@ function firstLine(text: string): string {
|
||||
if (t.startsWith("#") && h) return stripInlineMd(h);
|
||||
if (!fallback) fallback = t;
|
||||
}
|
||||
return fallback ? stripInlineMd(fallback) : "Dokument";
|
||||
return fallback ? stripInlineMd(fallback) : t("panel.tabDraft");
|
||||
}
|
||||
|
||||
/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
|
||||
@@ -129,7 +130,7 @@ export function initPanelView(opts: {
|
||||
opts.content.className = "raw";
|
||||
opts.content.textContent = rawText;
|
||||
}
|
||||
opts.modeBtn.textContent = rendered ? "MD" : "Roh";
|
||||
opts.modeBtn.textContent = rendered ? t("panel.rendered") : t("panel.raw");
|
||||
}
|
||||
|
||||
opts.modeBtn.addEventListener("click", () => {
|
||||
|
||||
+3
-1
@@ -8,10 +8,12 @@ import { wirePanel } from "./panel-wiring";
|
||||
import { flash, panelToast } from "./commands-view";
|
||||
import { initArchiveForm } from "./archive-form";
|
||||
import { applyTheme, THEMES } from "./themes";
|
||||
import { applyI18n, t } from "./messages";
|
||||
|
||||
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
||||
// danach denselben Update-Events wie das angedockte Panel; startet in
|
||||
// „Befehle", wenn es (noch) keinen Entwurf gibt.
|
||||
applyI18n();
|
||||
const project = new URLSearchParams(location.search).get("project")!;
|
||||
const win = getCurrentWebviewWindow();
|
||||
|
||||
@@ -110,7 +112,7 @@ const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
|
||||
let dir: string | undefined;
|
||||
if (!configured) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const chosen = await open({ directory: true, title: "Archiv-Ordner wählen" });
|
||||
const chosen = await open({ directory: true, title: t("panel.chooseArchiveDir") });
|
||||
if (!chosen) return;
|
||||
dir = chosen as string;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { createApp } from "vue";
|
||||
import Popup from "./components/Popup.vue";
|
||||
import { i18n } from "./i18n";
|
||||
|
||||
createApp(Popup).mount("#app");
|
||||
createApp(Popup).use(i18n).mount("#app");
|
||||
|
||||
+6
-4
@@ -4,6 +4,8 @@
|
||||
/// gehen nie durch innerHTML; die `**…**`-Marker im Snippet werden per
|
||||
/// Split in <mark>-Elemente übersetzt.
|
||||
|
||||
import { t } from "./messages";
|
||||
|
||||
interface Hit {
|
||||
relpath: string;
|
||||
title: string;
|
||||
@@ -34,7 +36,7 @@ export function initSearchView(
|
||||
bar.className = "hit-search";
|
||||
const input = document.createElement("input");
|
||||
input.type = "search";
|
||||
input.placeholder = "Archiv durchsuchen — #tag filtert";
|
||||
input.placeholder = t("search.placeholder");
|
||||
// Live-Suche ab 3 Zeichen, entprellt (300 ms Debounce); endet die Eingabe
|
||||
// mitten im Wort, wird das letzte Wort als Präfix gesucht (arch → arch*) —
|
||||
// außer es ist ein #tag-Token. Enter sucht sofort und wörtlich — für exakte
|
||||
@@ -50,7 +52,7 @@ export function initSearchView(
|
||||
if (q) {
|
||||
const head = document.createElement("div");
|
||||
head.className = "hit-head";
|
||||
head.textContent = "Mindestens 3 Zeichen — oder Enter.";
|
||||
head.textContent = t("search.minChars");
|
||||
results.append(head);
|
||||
}
|
||||
return;
|
||||
@@ -93,8 +95,8 @@ export function initSearchView(
|
||||
const tag = run.tag ? `#${run.tag}` : "";
|
||||
const scope = [what, tag].filter(Boolean).join(" · ");
|
||||
head.textContent = run.hits.length
|
||||
? `${run.hits.length} Treffer ${scope}`
|
||||
: `Keine Treffer für ${scope}`;
|
||||
? t("search.hits", { count: run.hits.length, scope })
|
||||
: t("search.noHits", { scope });
|
||||
results.append(head);
|
||||
for (const hit of run.hits) {
|
||||
const tile = document.createElement("div");
|
||||
|
||||
+6
-2
@@ -14,6 +14,7 @@ import { wirePanel } from "./panel-wiring";
|
||||
import { applyTheme, THEMES } from "./themes";
|
||||
import { flash, panelToast } from "./commands-view";
|
||||
import { initArchiveForm } from "./archive-form";
|
||||
import { applyI18n, t } from "./messages";
|
||||
|
||||
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
||||
function showError(msg: string) {
|
||||
@@ -31,6 +32,7 @@ window.addEventListener("unhandledrejection", (e) =>
|
||||
showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
|
||||
);
|
||||
|
||||
applyI18n();
|
||||
const project = new URLSearchParams(location.search).get("project")!;
|
||||
const win = getCurrentWebviewWindow();
|
||||
|
||||
@@ -173,7 +175,9 @@ function b64ToBytes(b64: string): Uint8Array {
|
||||
}
|
||||
|
||||
await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
|
||||
await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));
|
||||
await win.listen("pty-exit", () =>
|
||||
term.write(`\r\n\x1b[90m${t("panel.processEnded")}\x1b[0m\r\n`),
|
||||
);
|
||||
|
||||
term.onData((data) => invoke("term_write", { data }));
|
||||
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
|
||||
@@ -258,7 +262,7 @@ const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
|
||||
let dir: string | undefined;
|
||||
if (!configured) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const chosen = await open({ directory: true, title: "Archiv-Ordner wählen" });
|
||||
const chosen = await open({ directory: true, title: t("panel.chooseArchiveDir") });
|
||||
if (!chosen) return;
|
||||
dir = chosen as string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/// Testsprache festnageln: ohne das entscheidet `navigator.language` der
|
||||
/// Testumgebung über die erwarteten Texte (happy-dom liefert en-US), und die
|
||||
/// Erwartungen im Test hingen an einem Default, den keiner gesetzt hat.
|
||||
|
||||
Object.defineProperty(window.navigator, "language", {
|
||||
value: "de",
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
/// Node 22 stellt ein globales `localStorage` bereit, das ohne
|
||||
/// --localstorage-file undefiniert ist und das der Testumgebung überschattet.
|
||||
/// Ein Speicher im Arbeitsspeicher reicht hier vollauf.
|
||||
const speicher = new Map<string, string>();
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (k: string) => speicher.get(k) ?? null,
|
||||
setItem: (k: string, v: string) => void speicher.set(k, String(v)),
|
||||
removeItem: (k: string) => void speicher.delete(k),
|
||||
clear: () => speicher.clear(),
|
||||
key: (i: number) => [...speicher.keys()][i] ?? null,
|
||||
get length() {
|
||||
return speicher.size;
|
||||
},
|
||||
},
|
||||
});
|
||||
+13
-14
@@ -7,6 +7,7 @@
|
||||
|
||||
import { renderMarkdown } from "./markdown";
|
||||
import { linkWikiRefs } from "./panel-view";
|
||||
import { t } from "./messages";
|
||||
|
||||
interface DocEntry {
|
||||
name: string;
|
||||
@@ -72,14 +73,14 @@ export function initWikiView(
|
||||
left.className = "wiki-head-left";
|
||||
const title = document.createElement("div");
|
||||
title.className = "wiki-head-title";
|
||||
title.textContent = p.tag ? `#${p.tag}` : "Archiv";
|
||||
title.textContent = p.tag ? `#${p.tag}` : t("wiki.archive");
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "wiki-head-sub";
|
||||
sub.textContent = p.home;
|
||||
left.append(title, sub);
|
||||
const count = document.createElement("div");
|
||||
count.className = "wiki-head-right";
|
||||
count.textContent = `${p.total} ${p.total === 1 ? "Dokument" : "Dokumente"}`;
|
||||
count.textContent = t(p.total === 1 ? "wiki.docOne" : "wiki.docMany", { count: p.total });
|
||||
head.append(left, count);
|
||||
return head;
|
||||
}
|
||||
@@ -89,7 +90,7 @@ export function initWikiView(
|
||||
function tagBar(p: Page): HTMLElement {
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "wiki-chips";
|
||||
bar.append(chip("Alle", "tag:", !p.tag));
|
||||
bar.append(chip(t("wiki.all"), "tag:", !p.tag));
|
||||
for (const t of p.tags) {
|
||||
bar.append(chip(`#${t.name} ${t.count}`, `tag:${t.name}`, p.tag === t.name));
|
||||
}
|
||||
@@ -108,7 +109,7 @@ export function initWikiView(
|
||||
if (doc.backlinks) {
|
||||
const back = document.createElement("div");
|
||||
back.className = "wiki-doc-back";
|
||||
back.title = "Verweise hierher";
|
||||
back.title = t("wiki.backlinks");
|
||||
back.textContent = `↩ ${doc.backlinks}`;
|
||||
line.append(back);
|
||||
}
|
||||
@@ -142,12 +143,10 @@ export function initWikiView(
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "wiki-empty";
|
||||
const line = document.createElement("strong");
|
||||
line.textContent = p.tag ? `Keine Dokumente mit #${p.tag}.` : "Das Archiv ist leer.";
|
||||
line.textContent = p.tag ? t("wiki.emptyTag", { tag: p.tag }) : t("wiki.emptyArchive");
|
||||
empty.append(line);
|
||||
if (!p.tag) {
|
||||
empty.append(
|
||||
"Archivieren: Archiv-Button im Entwurf oder „archiviere das“ im Chat — mit Ordner, Beschreibung und Schlagwörtern.",
|
||||
);
|
||||
empty.append(t("wiki.emptyHint"));
|
||||
}
|
||||
container.append(empty);
|
||||
return;
|
||||
@@ -155,7 +154,7 @@ export function initWikiView(
|
||||
if (p.recent.length) {
|
||||
const eyebrow = document.createElement("div");
|
||||
eyebrow.className = "wiki-folder";
|
||||
eyebrow.textContent = "Zuletzt";
|
||||
eyebrow.textContent = t("wiki.recent");
|
||||
container.append(eyebrow);
|
||||
for (const doc of p.recent) container.append(docRow(doc));
|
||||
}
|
||||
@@ -168,7 +167,7 @@ export function initWikiView(
|
||||
} else if (p.recent.length) {
|
||||
const eyebrow = document.createElement("div");
|
||||
eyebrow.className = "wiki-folder";
|
||||
eyebrow.textContent = "Wurzel";
|
||||
eyebrow.textContent = t("wiki.root");
|
||||
container.append(eyebrow);
|
||||
}
|
||||
for (const doc of folder.docs) container.append(docRow(doc));
|
||||
@@ -180,7 +179,7 @@ export function initWikiView(
|
||||
head.className = "wiki-doc-head";
|
||||
const back = document.createElement("button");
|
||||
back.className = "wiki-back";
|
||||
back.textContent = "‹ Archiv";
|
||||
back.textContent = t("wiki.back");
|
||||
back.addEventListener("click", () => onLink("tag:"));
|
||||
const path = document.createElement("div");
|
||||
path.className = "wiki-head-sub";
|
||||
@@ -201,7 +200,7 @@ export function initWikiView(
|
||||
if (d.backlinks.length) {
|
||||
const back = document.createElement("div");
|
||||
back.className = "wiki-backlinks";
|
||||
back.append("Verweise hierher: ");
|
||||
back.append(t("wiki.backlinksLabel"));
|
||||
d.backlinks.forEach((name, i) => {
|
||||
if (i) back.append(" · ");
|
||||
const a = document.createElement("a");
|
||||
@@ -223,9 +222,9 @@ export function initWikiView(
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "wiki-empty";
|
||||
const line = document.createElement("strong");
|
||||
line.textContent = "Keine Wiki-Seite geladen.";
|
||||
line.textContent = t("wiki.noPage");
|
||||
empty.append(line);
|
||||
empty.append(chip("Archiv-Übersicht öffnen", "tag:"));
|
||||
empty.append(chip(t("wiki.openOverview"), "tag:"));
|
||||
container.append(empty);
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -2,7 +2,7 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Terminal</title>
|
||||
<title data-i18n="panel.windowTerminal">Terminal</title>
|
||||
<style>
|
||||
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
||||
:root {
|
||||
@@ -457,22 +457,22 @@
|
||||
<img id="project-icon" hidden data-tauri-drag-region />
|
||||
<span id="project-name" data-tauri-drag-region></span>
|
||||
<div class="panel-tabs" id="panel-tabs">
|
||||
<button class="panel-btn" data-mode="commands" title="Befehls-History">Befehle</button>
|
||||
<button class="panel-btn" data-mode="commands" data-i18n-title="panel.tabCommandsTitle" data-i18n="panel.tabCommands">Befehle</button>
|
||||
<span class="tab-sep"></span>
|
||||
<button class="panel-btn" data-mode="draft" title="Dokument">Dokument</button>
|
||||
<button class="panel-btn" data-mode="wiki" title="Archiv-Wiki">Wiki</button>
|
||||
<button class="panel-btn" data-mode="search" title="Suchtreffer">Suche</button>
|
||||
<button class="panel-btn" data-mode="draft" data-i18n-title="panel.tabDraftTitle" data-i18n="panel.tabDraft">Dokument</button>
|
||||
<button class="panel-btn" data-mode="wiki" data-i18n-title="panel.tabWikiTitle" data-i18n="panel.tabWiki">Wiki</button>
|
||||
<button class="panel-btn" data-mode="search" data-i18n-title="panel.tabSearchTitle" data-i18n="panel.tabSearch">Suche</button>
|
||||
<span class="tab-sep"></span>
|
||||
</div>
|
||||
<span id="pool"></span>
|
||||
<div id="winbtns">
|
||||
<button class="winbtn" id="win-min" aria-label="Minimieren">
|
||||
<button class="winbtn" id="win-min" data-i18n-aria="panel.minimize">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="2" y1="6.5" x2="10" y2="6.5" /></svg>
|
||||
</button>
|
||||
<button class="winbtn" id="win-max" aria-label="Maximieren">
|
||||
<button class="winbtn" id="win-max" data-i18n-aria="panel.maximize">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><rect x="2.5" y="2.5" width="7" height="7" rx="1" /></svg>
|
||||
</button>
|
||||
<button class="winbtn close" id="win-close" aria-label="Schließen">
|
||||
<button class="winbtn close" id="win-close" data-i18n-aria="panel.close">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="2.7" y1="2.7" x2="9.3" y2="9.3" /><line x1="9.3" y1="2.7" x2="2.7" y2="9.3" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -483,17 +483,17 @@
|
||||
<aside id="panel" hidden>
|
||||
<div class="panel-head">
|
||||
<div class="panel-titlerow">
|
||||
<span class="panel-title">Dokument</span>
|
||||
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
||||
<button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">
|
||||
<span class="panel-title" data-i18n="panel.tabDraft">Dokument</span>
|
||||
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" data-i18n-title="panel.editTitle">✎</button>
|
||||
<button class="panel-btn" id="panel-detach" data-i18n-title="panel.detach">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M12.5 8.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1h4.5" /><path d="M10 2h4v4" /><path d="M14 2 8 8" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn" id="panel-hide" title="Panel ausblenden">
|
||||
<button class="panel-btn" id="panel-hide" data-i18n-title="panel.hidePanel">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<select class="panel-lang draft-only" id="panel-lang" title="Sprache der Rechtschreibprüfung">
|
||||
<select class="panel-lang draft-only" id="panel-lang" data-i18n-title="panel.spellcheckLang">
|
||||
<option value="de">DE</option>
|
||||
<option value="en">EN</option>
|
||||
<option value="en-GB">EN-GB</option>
|
||||
@@ -502,17 +502,17 @@
|
||||
<option value="it">IT</option>
|
||||
<option value="nl">NL</option>
|
||||
</select>
|
||||
<button class="panel-btn draft-only" id="panel-wiki-jump" title="Im Wiki öffnen">
|
||||
<button class="panel-btn draft-only" id="panel-wiki-jump" data-i18n-title="panel.openInWiki">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M8 3.5C6.6 2.4 4.6 2 2.5 2v11c2.1 0 4.1.4 5.5 1.5 1.4-1.1 3.4-1.5 5.5-1.5V2c-2.1 0-4.1.4-5.5 1.5z" /><path d="M8 3.5v11" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
||||
<button class="panel-btn draft-only" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
||||
<button class="panel-btn draft-only" id="panel-mode" data-i18n-title="panel.toggleRaw" data-i18n="panel.rendered">MD</button>
|
||||
<button class="panel-btn draft-only" id="panel-content-edit" data-i18n-title="panel.editDraft">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn draft-only" id="panel-copy" title="In die Zwischenablage kopieren">
|
||||
<button class="panel-btn draft-only" id="panel-copy" data-i18n-title="panel.copy">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
|
||||
</button>
|
||||
<button class="panel-btn draft-only" id="panel-archive" title="In den Archiv-Ordner speichern">
|
||||
<button class="panel-btn draft-only" id="panel-archive" data-i18n-title="panel.archive">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,5 +4,6 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: "happy-dom",
|
||||
include: ["src/**/*.test.ts"],
|
||||
setupFiles: ["src/test-setup.ts"],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user