df828daea9
Wer claude schon benutzt, hat einen fertigen Login in ~/.claude. Bisher begann ai-control trotzdem bei null: jeder Pool war ein neu angelegtes Verzeichnis, der erste Sessionstart verlangte /login. Ein Pool kann jetzt stattdessen auf ein bestehendes Verzeichnis zeigen. pool.json trägt dafür optional `dir` (home-kontrahiert). Der Pool-Ordner ist dann nur die Hülle für diese Datei; pool_config_dir() löst auf und ist die einzige Stelle, über die Sessionstart, Verbrauchsstatistik, Panel-Provisionierung und Keychain-Name gehen. Zeigt ein Pool auf claudes Default-Verzeichnis, bleibt CLAUDE_CONFIG_DIR ungesetzt: claude nimmt dieses Verzeichnis von sich aus und damit den unsuffixierten Keychain-Eintrag, an dem der bestehende Login hängt (am Keychain gegengeprüft — "Claude Code-credentials" ohne Suffix neben den suffixierten Pool-Einträgen). Ein Verweis auf ein anderes Verzeichnis setzt die Variable wie gewohnt. Das verwiesene Verzeichnis gehört der App nicht: dort entsteht nur die Panel-Ausstattung (Skill, MCP-Eintrag, Freigabe-Ergänzung, alles additiv), keine settings.json-Grundausstattung. Löschen trifft nur die Hülle; Runtime-Symlinks und das Zurücksetzen des Keychain-Eintrags sind gesperrt. Ein Verzeichnis kann nur ein Pool belegen, Ziele innerhalb von pools/ sind ausgeschlossen. In der UI erscheint der Knopf nur, wenn ~/.claude existiert und noch kein Pool darauf zeigt; die Liste zeigt den Pfad unter dem Namen und statt "Zurücksetzen" den Hinweis, dass die Anmeldung claude gehört. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
// Ü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",
|
|
"System",
|
|
"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");
|
|
});
|
|
});
|