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">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Dokument</title>
|
<title data-i18n="panel.windowDocument">Dokument</title>
|
||||||
<style>
|
<style>
|
||||||
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
||||||
:root {
|
:root {
|
||||||
@@ -375,33 +375,33 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="panel-topbar" data-tauri-drag-region>
|
<div class="panel-topbar" data-tauri-drag-region>
|
||||||
<div class="panel-tabs" id="panel-tabs">
|
<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>
|
<span class="tab-sep"></span>
|
||||||
<button class="panel-btn" data-mode="draft" title="Dokument">Dokument</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" title="Archiv-Wiki">Wiki</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" title="Suchtreffer">Suche</button>
|
<button class="panel-btn" data-mode="search" data-i18n-title="panel.tabSearchTitle" data-i18n="panel.tabSearch">Suche</button>
|
||||||
<span class="tab-sep"></span>
|
<span class="tab-sep"></span>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
<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>
|
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="2" y1="6.5" x2="10" y2="6.5" /></svg>
|
||||||
</button>
|
</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>
|
<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>
|
||||||
<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>
|
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div class="panel-titlerow" data-tauri-drag-region>
|
<div class="panel-titlerow" data-tauri-drag-region>
|
||||||
<span class="panel-title">Dokument</span>
|
<span class="panel-title" data-i18n="panel.tabDraft">Dokument</span>
|
||||||
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" data-i18n-title="panel.editTitle">✎</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<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="de">DE</option>
|
||||||
<option value="en">EN</option>
|
<option value="en">EN</option>
|
||||||
<option value="en-GB">EN-GB</option>
|
<option value="en-GB">EN-GB</option>
|
||||||
@@ -410,17 +410,17 @@
|
|||||||
<option value="it">IT</option>
|
<option value="it">IT</option>
|
||||||
<option value="nl">NL</option>
|
<option value="nl">NL</option>
|
||||||
</select>
|
</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>
|
<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>
|
||||||
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
<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" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Projekte</title>
|
<title data-i18n="panel.windowProjects">Projekte</title>
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|||||||
+6
-4
@@ -3,6 +3,8 @@
|
|||||||
/// archivieren, Escape schließt. Gemeinsam für das angedockte Panel und das
|
/// archivieren, Escape schließt. Gemeinsam für das angedockte Panel und das
|
||||||
/// abgelöste Fenster.
|
/// abgelöste Fenster.
|
||||||
|
|
||||||
|
import { t } from "./messages";
|
||||||
|
|
||||||
export interface ArchiveFormMeta {
|
export interface ArchiveFormMeta {
|
||||||
folder?: string;
|
folder?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -23,12 +25,12 @@ export function initArchiveForm(
|
|||||||
const form = document.createElement("div");
|
const form = document.createElement("div");
|
||||||
form.className = "archive-form";
|
form.className = "archive-form";
|
||||||
form.hidden = true;
|
form.hidden = true;
|
||||||
const folder = field("Ordner — z. B. konzepte/panel");
|
const folder = field(t("archiveForm.folder"));
|
||||||
const desc = field("Beschreibung");
|
const desc = field(t("archiveForm.description"));
|
||||||
const tags = field("Schlagwörter, kommagetrennt");
|
const tags = field(t("archiveForm.tags"));
|
||||||
const submit = document.createElement("button");
|
const submit = document.createElement("button");
|
||||||
submit.className = "archive-form-submit";
|
submit.className = "archive-form-submit";
|
||||||
submit.textContent = "Archivieren";
|
submit.textContent = t("archiveForm.submit");
|
||||||
form.append(folder, desc, tags, submit);
|
form.append(folder, desc, tags, submit);
|
||||||
document.body.append(form);
|
document.body.append(form);
|
||||||
|
|
||||||
|
|||||||
+14
-12
@@ -6,6 +6,7 @@
|
|||||||
/// Fremdtext und gehen nie durch innerHTML.
|
/// Fremdtext und gehen nie durch innerHTML.
|
||||||
|
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
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,
|
/// Entfernt Bidi- und Zero-Width-Steuerzeichen (U+200B–200F, U+202A–202E,
|
||||||
@@ -34,10 +35,11 @@ export interface CommandsView {
|
|||||||
|
|
||||||
function fmtTime(ts: number): string {
|
function fmtTime(ts: number): string {
|
||||||
const d = new Date(ts * 1000);
|
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()
|
return d.toDateString() === new Date().toDateString()
|
||||||
? hm
|
? hm
|
||||||
: `${d.toLocaleDateString("de")} ${hm}`;
|
: `${d.toLocaleDateString(loc)} ${hm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kurzes visuelles Feedback (copied/error) — der eine Flash-Helper fürs
|
/// 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 {
|
function copyBtn(text: () => string): HTMLButtonElement {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.className = "panel-btn cmd-copy";
|
btn.className = "panel-btn cmd-copy";
|
||||||
btn.title = "Befehl kopieren";
|
btn.title = t("commands.copyOne");
|
||||||
btn.innerHTML =
|
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>';
|
'<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 () => {
|
btn.addEventListener("click", async () => {
|
||||||
@@ -72,7 +74,7 @@ function copyBtn(text: () => string): HTMLButtonElement {
|
|||||||
function deleteBtn(onClick: () => void): HTMLButtonElement {
|
function deleteBtn(onClick: () => void): HTMLButtonElement {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.className = "panel-btn cmd-del";
|
btn.className = "panel-btn cmd-del";
|
||||||
btn.title = "Aus der History entfernen";
|
btn.title = t("commands.removeOne");
|
||||||
btn.innerHTML =
|
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>';
|
'<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);
|
btn.addEventListener("click", onClick);
|
||||||
@@ -96,7 +98,7 @@ export function initCommandsView(
|
|||||||
if (rec.session) {
|
if (rec.session) {
|
||||||
const sep = document.createElement("div");
|
const sep = document.createElement("div");
|
||||||
sep.className = "cmd-sep";
|
sep.className = "cmd-sep";
|
||||||
sep.textContent = `Session · ${fmtTime(rec.ts)}`;
|
sep.textContent = `${t("commands.session")} · ${fmtTime(rec.ts)}`;
|
||||||
container.append(sep);
|
container.append(sep);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -113,7 +115,7 @@ export function initCommandsView(
|
|||||||
if (cmds.length > 1) {
|
if (cmds.length > 1) {
|
||||||
const all = document.createElement("button");
|
const all = document.createElement("button");
|
||||||
all.className = "cmd-all";
|
all.className = "cmd-all";
|
||||||
all.textContent = "Alle kopieren";
|
all.textContent = t("commands.copyAll");
|
||||||
all.addEventListener("click", async () => {
|
all.addEventListener("click", async () => {
|
||||||
await writeText(cmds.map((c) => stripInvisibles(c.cmd)).join("\n"));
|
await writeText(cmds.map((c) => stripInvisibles(c.cmd)).join("\n"));
|
||||||
flash(all, "copied");
|
flash(all, "copied");
|
||||||
@@ -166,10 +168,10 @@ export function initCommandsView(
|
|||||||
export type PanelMode = "draft" | "commands" | "search" | "wiki";
|
export type PanelMode = "draft" | "commands" | "search" | "wiki";
|
||||||
|
|
||||||
const LABEL: { [m in PanelMode]: string } = {
|
const LABEL: { [m in PanelMode]: string } = {
|
||||||
draft: "Dokument",
|
draft: t("panel.tabDraft"),
|
||||||
commands: "Befehle",
|
commands: t("panel.tabCommands"),
|
||||||
search: "Suche",
|
search: t("panel.tabSearch"),
|
||||||
wiki: "Wiki",
|
wiki: t("panel.tabWiki"),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Vier Tabs Entwurf / Befehle / Suche / Wiki: blenden Entwurfs-Inhalt samt
|
/// Vier Tabs Entwurf / Befehle / Suche / Wiki: blenden Entwurfs-Inhalt samt
|
||||||
@@ -202,7 +204,7 @@ export function initPanelMode(opts: {
|
|||||||
function to(m: PanelMode) {
|
function to(m: PanelMode) {
|
||||||
if (m === mode) return;
|
if (m === mode) return;
|
||||||
opts.flush();
|
opts.flush();
|
||||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || "Dokument";
|
if (mode === "draft") draftTitle = opts.titleEl.textContent || t("panel.tabDraft");
|
||||||
mode = m;
|
mode = m;
|
||||||
apply();
|
apply();
|
||||||
if (m === "draft") opts.titleEl.textContent = draftTitle;
|
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
|
/// Auswahl aufheben (Panel zugeklappt) — der nächste to()-Aufruf oder
|
||||||
/// Tab-Klick wählt wieder aus.
|
/// Tab-Klick wählt wieder aus.
|
||||||
function clear() {
|
function clear() {
|
||||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || "Dokument";
|
if (mode === "draft") draftTitle = opts.titleEl.textContent || t("panel.tabDraft");
|
||||||
mode = null;
|
mode = null;
|
||||||
for (const t of tabs) t.classList.remove("active");
|
for (const t of tabs) t.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ onUnmounted(() => {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<footer class="ft">
|
<footer class="ft">
|
||||||
<button class="fbtn" @click="openMain">Öffnen</button>
|
<button class="fbtn" @click="openMain">{{ $t("popup.open") }}</button>
|
||||||
<button class="fbtn danger" @click="quit">Beenden</button>
|
<button class="fbtn danger" @click="quit">{{ $t("popup.quit") }}</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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";
|
import { createI18n } from "vue-i18n";
|
||||||
|
|
||||||
const de = {
|
import { messages, storedLocale, type Locale } from "./messages";
|
||||||
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";
|
|
||||||
|
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
globalInjection: true,
|
globalInjection: true,
|
||||||
locale: initial,
|
locale: storedLocale(),
|
||||||
fallbackLocale: "en",
|
fallbackLocale: "en",
|
||||||
messages: { de, en },
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setLocale(lang: "de" | "en") {
|
export function setLocale(lang: Locale) {
|
||||||
i18n.global.locale.value = lang;
|
i18n.global.locale.value = lang;
|
||||||
localStorage.setItem("lang", 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 { renderMarkdown } from "./markdown";
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { flash } from "./commands-view";
|
import { flash } from "./commands-view";
|
||||||
|
import { t } from "./messages";
|
||||||
|
|
||||||
export interface PanelView {
|
export interface PanelView {
|
||||||
set(text: string): void;
|
set(text: string): void;
|
||||||
@@ -37,7 +38,7 @@ function firstLine(text: string): string {
|
|||||||
if (t.startsWith("#") && h) return stripInlineMd(h);
|
if (t.startsWith("#") && h) return stripInlineMd(h);
|
||||||
if (!fallback) fallback = t;
|
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`.
|
/// 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.className = "raw";
|
||||||
opts.content.textContent = rawText;
|
opts.content.textContent = rawText;
|
||||||
}
|
}
|
||||||
opts.modeBtn.textContent = rendered ? "MD" : "Roh";
|
opts.modeBtn.textContent = rendered ? t("panel.rendered") : t("panel.raw");
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.modeBtn.addEventListener("click", () => {
|
opts.modeBtn.addEventListener("click", () => {
|
||||||
|
|||||||
+3
-1
@@ -8,10 +8,12 @@ import { wirePanel } from "./panel-wiring";
|
|||||||
import { flash, panelToast } from "./commands-view";
|
import { flash, panelToast } from "./commands-view";
|
||||||
import { initArchiveForm } from "./archive-form";
|
import { initArchiveForm } from "./archive-form";
|
||||||
import { applyTheme, THEMES } from "./themes";
|
import { applyTheme, THEMES } from "./themes";
|
||||||
|
import { applyI18n, t } from "./messages";
|
||||||
|
|
||||||
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
||||||
// danach denselben Update-Events wie das angedockte Panel; startet in
|
// danach denselben Update-Events wie das angedockte Panel; startet in
|
||||||
// „Befehle", wenn es (noch) keinen Entwurf gibt.
|
// „Befehle", wenn es (noch) keinen Entwurf gibt.
|
||||||
|
applyI18n();
|
||||||
const project = new URLSearchParams(location.search).get("project")!;
|
const project = new URLSearchParams(location.search).get("project")!;
|
||||||
const win = getCurrentWebviewWindow();
|
const win = getCurrentWebviewWindow();
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
|
|||||||
let dir: string | undefined;
|
let dir: string | undefined;
|
||||||
if (!configured) {
|
if (!configured) {
|
||||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
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;
|
if (!chosen) return;
|
||||||
dir = chosen as string;
|
dir = chosen as string;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import Popup from "./components/Popup.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
|
/// gehen nie durch innerHTML; die `**…**`-Marker im Snippet werden per
|
||||||
/// Split in <mark>-Elemente übersetzt.
|
/// Split in <mark>-Elemente übersetzt.
|
||||||
|
|
||||||
|
import { t } from "./messages";
|
||||||
|
|
||||||
interface Hit {
|
interface Hit {
|
||||||
relpath: string;
|
relpath: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -34,7 +36,7 @@ export function initSearchView(
|
|||||||
bar.className = "hit-search";
|
bar.className = "hit-search";
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "search";
|
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
|
// 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*) —
|
// 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
|
// 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) {
|
if (q) {
|
||||||
const head = document.createElement("div");
|
const head = document.createElement("div");
|
||||||
head.className = "hit-head";
|
head.className = "hit-head";
|
||||||
head.textContent = "Mindestens 3 Zeichen — oder Enter.";
|
head.textContent = t("search.minChars");
|
||||||
results.append(head);
|
results.append(head);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -93,8 +95,8 @@ export function initSearchView(
|
|||||||
const tag = run.tag ? `#${run.tag}` : "";
|
const tag = run.tag ? `#${run.tag}` : "";
|
||||||
const scope = [what, tag].filter(Boolean).join(" · ");
|
const scope = [what, tag].filter(Boolean).join(" · ");
|
||||||
head.textContent = run.hits.length
|
head.textContent = run.hits.length
|
||||||
? `${run.hits.length} Treffer ${scope}`
|
? t("search.hits", { count: run.hits.length, scope })
|
||||||
: `Keine Treffer für ${scope}`;
|
: t("search.noHits", { scope });
|
||||||
results.append(head);
|
results.append(head);
|
||||||
for (const hit of run.hits) {
|
for (const hit of run.hits) {
|
||||||
const tile = document.createElement("div");
|
const tile = document.createElement("div");
|
||||||
|
|||||||
+6
-2
@@ -14,6 +14,7 @@ import { wirePanel } from "./panel-wiring";
|
|||||||
import { applyTheme, THEMES } from "./themes";
|
import { applyTheme, THEMES } from "./themes";
|
||||||
import { flash, panelToast } from "./commands-view";
|
import { flash, panelToast } from "./commands-view";
|
||||||
import { initArchiveForm } from "./archive-form";
|
import { initArchiveForm } from "./archive-form";
|
||||||
|
import { applyI18n, t } from "./messages";
|
||||||
|
|
||||||
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
||||||
function showError(msg: string) {
|
function showError(msg: string) {
|
||||||
@@ -31,6 +32,7 @@ window.addEventListener("unhandledrejection", (e) =>
|
|||||||
showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
|
showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
applyI18n();
|
||||||
const project = new URLSearchParams(location.search).get("project")!;
|
const project = new URLSearchParams(location.search).get("project")!;
|
||||||
const win = getCurrentWebviewWindow();
|
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<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.onData((data) => invoke("term_write", { data }));
|
||||||
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
|
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
|
||||||
@@ -258,7 +262,7 @@ const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
|
|||||||
let dir: string | undefined;
|
let dir: string | undefined;
|
||||||
if (!configured) {
|
if (!configured) {
|
||||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
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;
|
if (!chosen) return;
|
||||||
dir = chosen as string;
|
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 { renderMarkdown } from "./markdown";
|
||||||
import { linkWikiRefs } from "./panel-view";
|
import { linkWikiRefs } from "./panel-view";
|
||||||
|
import { t } from "./messages";
|
||||||
|
|
||||||
interface DocEntry {
|
interface DocEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -72,14 +73,14 @@ export function initWikiView(
|
|||||||
left.className = "wiki-head-left";
|
left.className = "wiki-head-left";
|
||||||
const title = document.createElement("div");
|
const title = document.createElement("div");
|
||||||
title.className = "wiki-head-title";
|
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");
|
const sub = document.createElement("div");
|
||||||
sub.className = "wiki-head-sub";
|
sub.className = "wiki-head-sub";
|
||||||
sub.textContent = p.home;
|
sub.textContent = p.home;
|
||||||
left.append(title, sub);
|
left.append(title, sub);
|
||||||
const count = document.createElement("div");
|
const count = document.createElement("div");
|
||||||
count.className = "wiki-head-right";
|
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);
|
head.append(left, count);
|
||||||
return head;
|
return head;
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ export function initWikiView(
|
|||||||
function tagBar(p: Page): HTMLElement {
|
function tagBar(p: Page): HTMLElement {
|
||||||
const bar = document.createElement("div");
|
const bar = document.createElement("div");
|
||||||
bar.className = "wiki-chips";
|
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) {
|
for (const t of p.tags) {
|
||||||
bar.append(chip(`#${t.name} ${t.count}`, `tag:${t.name}`, p.tag === t.name));
|
bar.append(chip(`#${t.name} ${t.count}`, `tag:${t.name}`, p.tag === t.name));
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ export function initWikiView(
|
|||||||
if (doc.backlinks) {
|
if (doc.backlinks) {
|
||||||
const back = document.createElement("div");
|
const back = document.createElement("div");
|
||||||
back.className = "wiki-doc-back";
|
back.className = "wiki-doc-back";
|
||||||
back.title = "Verweise hierher";
|
back.title = t("wiki.backlinks");
|
||||||
back.textContent = `↩ ${doc.backlinks}`;
|
back.textContent = `↩ ${doc.backlinks}`;
|
||||||
line.append(back);
|
line.append(back);
|
||||||
}
|
}
|
||||||
@@ -142,12 +143,10 @@ export function initWikiView(
|
|||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "wiki-empty";
|
empty.className = "wiki-empty";
|
||||||
const line = document.createElement("strong");
|
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);
|
empty.append(line);
|
||||||
if (!p.tag) {
|
if (!p.tag) {
|
||||||
empty.append(
|
empty.append(t("wiki.emptyHint"));
|
||||||
"Archivieren: Archiv-Button im Entwurf oder „archiviere das“ im Chat — mit Ordner, Beschreibung und Schlagwörtern.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
container.append(empty);
|
container.append(empty);
|
||||||
return;
|
return;
|
||||||
@@ -155,7 +154,7 @@ export function initWikiView(
|
|||||||
if (p.recent.length) {
|
if (p.recent.length) {
|
||||||
const eyebrow = document.createElement("div");
|
const eyebrow = document.createElement("div");
|
||||||
eyebrow.className = "wiki-folder";
|
eyebrow.className = "wiki-folder";
|
||||||
eyebrow.textContent = "Zuletzt";
|
eyebrow.textContent = t("wiki.recent");
|
||||||
container.append(eyebrow);
|
container.append(eyebrow);
|
||||||
for (const doc of p.recent) container.append(docRow(doc));
|
for (const doc of p.recent) container.append(docRow(doc));
|
||||||
}
|
}
|
||||||
@@ -168,7 +167,7 @@ export function initWikiView(
|
|||||||
} else if (p.recent.length) {
|
} else if (p.recent.length) {
|
||||||
const eyebrow = document.createElement("div");
|
const eyebrow = document.createElement("div");
|
||||||
eyebrow.className = "wiki-folder";
|
eyebrow.className = "wiki-folder";
|
||||||
eyebrow.textContent = "Wurzel";
|
eyebrow.textContent = t("wiki.root");
|
||||||
container.append(eyebrow);
|
container.append(eyebrow);
|
||||||
}
|
}
|
||||||
for (const doc of folder.docs) container.append(docRow(doc));
|
for (const doc of folder.docs) container.append(docRow(doc));
|
||||||
@@ -180,7 +179,7 @@ export function initWikiView(
|
|||||||
head.className = "wiki-doc-head";
|
head.className = "wiki-doc-head";
|
||||||
const back = document.createElement("button");
|
const back = document.createElement("button");
|
||||||
back.className = "wiki-back";
|
back.className = "wiki-back";
|
||||||
back.textContent = "‹ Archiv";
|
back.textContent = t("wiki.back");
|
||||||
back.addEventListener("click", () => onLink("tag:"));
|
back.addEventListener("click", () => onLink("tag:"));
|
||||||
const path = document.createElement("div");
|
const path = document.createElement("div");
|
||||||
path.className = "wiki-head-sub";
|
path.className = "wiki-head-sub";
|
||||||
@@ -201,7 +200,7 @@ export function initWikiView(
|
|||||||
if (d.backlinks.length) {
|
if (d.backlinks.length) {
|
||||||
const back = document.createElement("div");
|
const back = document.createElement("div");
|
||||||
back.className = "wiki-backlinks";
|
back.className = "wiki-backlinks";
|
||||||
back.append("Verweise hierher: ");
|
back.append(t("wiki.backlinksLabel"));
|
||||||
d.backlinks.forEach((name, i) => {
|
d.backlinks.forEach((name, i) => {
|
||||||
if (i) back.append(" · ");
|
if (i) back.append(" · ");
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -223,9 +222,9 @@ export function initWikiView(
|
|||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "wiki-empty";
|
empty.className = "wiki-empty";
|
||||||
const line = document.createElement("strong");
|
const line = document.createElement("strong");
|
||||||
line.textContent = "Keine Wiki-Seite geladen.";
|
line.textContent = t("wiki.noPage");
|
||||||
empty.append(line);
|
empty.append(line);
|
||||||
empty.append(chip("Archiv-Übersicht öffnen", "tag:"));
|
empty.append(chip(t("wiki.openOverview"), "tag:"));
|
||||||
container.append(empty);
|
container.append(empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-18
@@ -2,7 +2,7 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Terminal</title>
|
<title data-i18n="panel.windowTerminal">Terminal</title>
|
||||||
<style>
|
<style>
|
||||||
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
||||||
:root {
|
:root {
|
||||||
@@ -457,22 +457,22 @@
|
|||||||
<img id="project-icon" hidden data-tauri-drag-region />
|
<img id="project-icon" hidden data-tauri-drag-region />
|
||||||
<span id="project-name" data-tauri-drag-region></span>
|
<span id="project-name" data-tauri-drag-region></span>
|
||||||
<div class="panel-tabs" id="panel-tabs">
|
<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>
|
<span class="tab-sep"></span>
|
||||||
<button class="panel-btn" data-mode="draft" title="Dokument">Dokument</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" title="Archiv-Wiki">Wiki</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" title="Suchtreffer">Suche</button>
|
<button class="panel-btn" data-mode="search" data-i18n-title="panel.tabSearchTitle" data-i18n="panel.tabSearch">Suche</button>
|
||||||
<span class="tab-sep"></span>
|
<span class="tab-sep"></span>
|
||||||
</div>
|
</div>
|
||||||
<span id="pool"></span>
|
<span id="pool"></span>
|
||||||
<div id="winbtns">
|
<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>
|
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="2" y1="6.5" x2="10" y2="6.5" /></svg>
|
||||||
</button>
|
</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>
|
<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>
|
||||||
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -483,17 +483,17 @@
|
|||||||
<aside id="panel" hidden>
|
<aside id="panel" hidden>
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div class="panel-titlerow">
|
<div class="panel-titlerow">
|
||||||
<span class="panel-title">Dokument</span>
|
<span class="panel-title" data-i18n="panel.tabDraft">Dokument</span>
|
||||||
<button class="panel-btn panel-edit draft-only" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
<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" title="In eigenes Fenster ablösen">
|
<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>
|
<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>
|
||||||
<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>
|
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<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="de">DE</option>
|
||||||
<option value="en">EN</option>
|
<option value="en">EN</option>
|
||||||
<option value="en-GB">EN-GB</option>
|
<option value="en-GB">EN-GB</option>
|
||||||
@@ -502,17 +502,17 @@
|
|||||||
<option value="it">IT</option>
|
<option value="it">IT</option>
|
||||||
<option value="nl">NL</option>
|
<option value="nl">NL</option>
|
||||||
</select>
|
</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>
|
<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>
|
||||||
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
<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" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
environment: "happy-dom",
|
environment: "happy-dom",
|
||||||
include: ["src/**/*.test.ts"],
|
include: ["src/**/*.test.ts"],
|
||||||
|
setupFiles: ["src/test-setup.ts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user