diff --git a/BEFUNDE.md b/BEFUNDE.md index 8f6fab5..6a16ef5 100644 --- a/BEFUNDE.md +++ b/BEFUNDE.md @@ -1,5 +1,21 @@ # Bestandsaufnahme, 12.09.2026 +> **Stand 24.09.2026.** Dieses Dokument ist die Aufnahme vom 12.09. Was seither +> erledigt wurde, steht an Ort und Stelle durchgestrichen oder vermerkt; die +> Kurzfassung: +> +> | | | +> |---|---| +> | Punkte 1, 2, 2b, 3 (teilweise) | ✅ erledigt | +> | Go-Tests `documents`, `tags` | ✅ 12 Testfunktionen | +> | Go-Tests `sharing`, `rescan`, `audit`, `thumbnails` | **offen** | +> | Frontend-DOM-Tests | ✅ jsdom seit 18.09., 46 Tests | +> +> Dazwischen lag eine Woche Oberflächenarbeit, die hier nicht auftaucht, weil +> sie keine Befunde abarbeitete: Rückmeldungen mit Rückgängig, Platzhalter, +> Leerzustände, Sprache vereinheitlicht, `Intl`. Steht in +> [TESTING.md](TESTING.md). + Einmal durch die ganze Cloud: Code, Sicherheit, offene Enden. Geordnet nach dem, was passieren kann — nicht nach Aufwand. @@ -160,14 +176,14 @@ Zwei Tests in `pkg/middleware/logger_test.go` halten es fest. **Schwere: mittel. Nicht was heute kaputt ist, sondern was morgen still kaputtgeht.** -| Paket | Zeilen | Test | Aussenpruefung | +| Paket | Zeilen | Test (24.09.) | Aussenpruefung | |---|---:|---:|---| -| `internal/tags` | 1132 | **0** | keine | -| `internal/documents` (OCR, Volltext) | 1008 | **0** | keine | -| `internal/sharing` | 382 | **0** | Python-Selbsttest, 24 Prüfungen | -| `internal/rescan` | 479 | **0** | Python-Selbsttest, 20 Prüfungen | -| `internal/audit` | 207 | **0** | keine | -| `internal/thumbnails` | 113 | **0** | keine | +| `internal/tags` | 1338 | **4** ✅ | keine | +| `internal/documents` (OCR, Volltext) | 1243 | **8** ✅ | keine | +| `internal/sharing` | 347 | **0** | Python-Selbsttest, 24 Prüfungen | +| `internal/rescan` | 435 | **0** | Python-Selbsttest, 20 Prüfungen | +| `internal/audit` | 180 | **0** | keine | +| `internal/thumbnails` | 95 | **0** | keine | Bei `sharing` und `rescan` deckt wenigstens ein Selbsttest die Außenseite ab. Bei **`documents` und `tags` gar nichts** — und `documents` ist das Stück, das @@ -251,8 +267,16 @@ gibt: * **Die Fehlversuchssperren liegen im Arbeitsspeicher.** Ein Neustart des Containers hebt jede Sperre auf. Für den Hausgebrauch vertretbar; wer es anders will, braucht eine Tabelle. -* **Frontend ohne Komponententests.** `vitest` läuft jetzt (37 Tests), aber nur - über reine Funktionen — für DOM-Tests fehlt `jsdom` in den devDependencies. +* ~~**Frontend ohne Komponententests.**~~ ✅ **18.09.2026:** `jsdom` ist in den + devDependencies, die Umgebung bleibt `node` (schnell); wer ein DOM braucht, + schreibt `// @vitest-environment jsdom` an den Anfang seiner Datei. 46 Tests, + davon vier gegen ein DOM. Der erste davon hat sofort einen Fehler gefunden – + in der Behebung vom Vortag (`useBackClose`). + + **Was er nicht leistet, steht dabei:** Den ursprünglichen Fehler bildet jsdom + nicht ab; es verwirft einen Verlaufssprung, sobald `pushState` folgt. Dafür + bräuchte es einen echten Browser (Playwright). Nachgeprüft, nicht vermutet: + mit der alten, kaputten Fassung blieben alle Tests grün. --- diff --git a/TESTING.md b/TESTING.md index 71220ff..345cd51 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1562,6 +1562,46 @@ bleibt `node` (schnell); wer ein DOM braucht, schreibt --- +## Ein Wächter für die Sprache (24.09.2026) + +Sechs Tage nach der Übersetzung fielen beim Nachsehen **fünf Reste** auf: +`label="Ganztägig"`, `aria-label="Kalendermenü"`, `label="Bearbeiten"` und +zweimal `"Löschen"`. Alle in **doppelt** gequoteten JSX-Attributen – die +Ersetzungsliste hatte nur die einfach gequoteten, und das damalige Prüfskript +hielt Einzelwörter ohne Leerzeichen für Bezeichner. + +Deshalb steht die Prüfung jetzt als Test (`src/sprache.test.ts`) statt als +Skript, das man einmal laufen lässt. Sie läuft bei jedem `npm test` mit. + +**Der erste Wächter taugte nichts, und das kam nur durch die Mutation heraus:** +Er prüfte allein auf Umlaute und liess `"Papierkorb ist leer"` anstandslos +durch. Wer einen Wächter baut, muss ihn einmal angreifen. + +Jetzt: Umlaute **plus** eine Liste häufiger deutscher Wörter mit Wortgrenzen +(sonst schlägt "ist" in "list" an). Kommentare werden vorher entfernt – und +zwar richtig, mit einem kleinen Durchlauf statt zeilenweise: Blockkommentare +haben Fortsetzungszeilen ohne führenden Stern, die beim ersten Versuch fünf +Fehlalarme erzeugt haben. + +Nachgewiesen an vier Mutationen, alle werden rot: + +| eingeschleuster Text | erkannt an | +|---|---| +| `Papierkorb ist leer` | "ist", "leer" | +| `Loeschen fehlgeschlagen` | "Loeschen", "fehlgeschlagen" | +| `Ganztägig` | Umlaut | +| `Bitte warten` | "bitte" | + +> **Ein Netz, kein Beweis.** Ein deutscher Satz ohne Umlaut und ohne eines der +> gelisteten Wörter käme durch – "Datum bereits vergeben" etwa. Der Test +> fängt, was erfahrungsgemäß passiert: ein vergessenes Attribut beim +> Übersetzen. Wer die Liste ergänzt, weil einmal etwas durchgerutscht ist, tut +> genau das Richtige. + +- [x] Gebautes Bündel enthält keine deutschen Oberflächentexte mehr – + gegengeprüft, dass die englischen drin sind (sonst prüfte der Grep nur + sich selbst) + ## Wochentage und Monate aus `Intl` (20.09.2026) Sechs handgeschriebene Arrays in drei Dateien, und `Intl` kam im ganzen Projekt diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3cdf2d3..c3e40c7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@tailwindcss/vite": "^4.3.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^26.6.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "jsdom": "^26.1.0", @@ -2914,6 +2915,16 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -6371,6 +6382,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index ad228b3..2f19de9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@tailwindcss/vite": "^4.3.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^26.6.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "jsdom": "^26.1.0", diff --git a/frontend/src/components/calendar/EventDialog.tsx b/frontend/src/components/calendar/EventDialog.tsx index 47941a6..b589856 100644 --- a/frontend/src/components/calendar/EventDialog.tsx +++ b/frontend/src/components/calendar/EventDialog.tsx @@ -221,7 +221,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId )}
- +
diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx index 6a52f73..92062e6 100644 --- a/frontend/src/pages/CalendarPage.tsx +++ b/frontend/src/pages/CalendarPage.tsx @@ -582,7 +582,7 @@ export default function CalendarPage() { setDeleteCalendar(null)} /> @@ -642,7 +642,7 @@ function CalMenu({ onRename, onExport, onImport, onDelete, onShare, own, canWrit }, [open]) return (
- {open && ( @@ -651,11 +651,11 @@ function CalMenu({ onRename, onExport, onImport, onDelete, onShare, own, canWrit background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: 8, boxShadow: '0 4px 14px rgba(0,0,0,0.25)', overflow: 'hidden', }}> - {own && } label="Bearbeiten" onClick={() => { setOpen(false); onRename() }} />} + {own && } label="Edit" onClick={() => { setOpen(false); onRename() }} />} {own && } label="Freigeben…" onClick={() => { setOpen(false); onShare() }} />} } label="Exportieren (.ics)" onClick={() => { setOpen(false); onExport() }} /> {canWrite && { setOpen(false); onImport() }} />} - {own && } label="Löschen" danger onClick={() => { setOpen(false); onDelete() }} />} + {own && } label="Delete" danger onClick={() => { setOpen(false); onDelete() }} />}
)}
diff --git a/frontend/src/sprache.test.ts b/frontend/src/sprache.test.ts new file mode 100644 index 0000000..5a9c034 --- /dev/null +++ b/frontend/src/sprache.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +/** + * Die Oberfläche spricht Englisch – dieser Test hält sie dabei. + * + * Am 18.09.2026 wurde der Kalender übersetzt, 80 Stellen. Sechs Tage später + * fielen beim Nachsehen **vier Reste** auf: `label="Ganztägig"`, + * `aria-label="Kalendermenü"`, zweimal `confirmLabel="Löschen"`. Alle in + * doppelt gequoteten JSX-Attributen – meine Ersetzungsliste hatte nur die + * einfach gequoteten, und mein damaliges Prüfskript hielt Einzelwörter ohne + * Leerzeichen für Bezeichner und übersprang sie. + * + * Genau deshalb steht die Prüfung jetzt hier statt in einem Skript, das man + * einmal laufen lässt: Sie läuft bei jedem `npm test` mit. + * + * **Kommentare bleiben deutsch** und werden übersprungen. Erkannt wird nur, + * was in Anführungszeichen oder zwischen Tags steht. + */ + +const hier = dirname(fileURLToPath(import.meta.url)) + +/** + * Woran man Deutsch erkennt. + * + * **Umlaute allein genügen nicht.** Die erste Fassung dieses Tests prüfte nur + * darauf – und liess „Papierkorb ist leer" anstandslos durch. Aufgefallen ist + * das, weil der Test mutiert wurde: Wer einen Wächter baut, muss ihn einmal + * angreifen, sonst bewacht er nichts. + * + * Deshalb zusätzlich eine Liste häufiger deutscher Wörter, mit Wortgrenzen + * geprüft – sonst schlüge „ist" in „list" an. Bewusst **nicht** darin: Wörter, + * die es im Englischen genauso gibt (Name, Start, Status, Import, Server) und + * solche, die zu leicht danebengreifen (man, war). + * + * **Das ist ein Netz, kein Beweis.** Ein deutscher Satz ohne Umlaut und ohne + * eines dieser Wörter käme durch – „Datum bereits vergeben" etwa. Der Test + * fängt, was erfahrungsgemäß passiert (ein vergessenes Attribut beim + * Übersetzen), nicht jeden denkbaren Fall. Wer hier etwas ergänzt, weil es + * einmal durchgerutscht ist, tut genau das Richtige. + */ +const umlaute = /[äöüßÄÖÜ]/ +const deutscheWoerter = new RegExp( + '\\b(' + [ + // Funktionswörter + 'der', 'die', 'das', 'den', 'dem', 'des', 'ein', 'eine', 'einen', 'einem', + 'und', 'oder', 'nicht', 'kein', 'keine', 'keinen', 'ist', 'sind', 'wird', + 'werden', 'wurde', 'mit', 'vom', 'zum', 'zur', 'aus', 'bei', 'nach', + 'durch', 'ohne', 'gegen', 'noch', 'schon', 'auch', 'nur', 'sehr', 'mehr', + 'alle', 'alles', 'sich', 'wenn', 'dann', 'hier', 'wie', + // Wörter der Oberfläche + 'Datei', 'Dateien', 'Ordner', 'Termin', 'Termine', 'Kalender', + 'Papierkorb', 'Einstellungen', 'Freigabe', 'Etikett', 'Absender', + 'Speichern', 'Abbrechen', 'Bearbeiten', 'Hochladen', 'Herunterladen', + 'Suchen', 'Anmelden', 'Abmelden', 'Benutzer', 'Nutzer', 'Passwort', + 'Fehler', 'Hinweis', 'Warnung', 'leer', 'neu', + // Wörter, an denen typische Meldungen hängen – auch in der + // umlautfreien Schreibweise, die beim Tippen gern entsteht. + 'fehlgeschlagen', 'erfolgreich', 'gespeichert', 'geladen', 'konnte', + 'bitte', 'entfernen', 'aendern', 'hinzufuegen', 'Loeschen', 'Schliessen', + 'Groesse', 'Aendern', + ].join('|') + ')\\b', 'i') + +function istDeutsch(text: string): boolean { + return umlaute.test(text) || deutscheWoerter.test(text) +} + +/** Einzelne Stellen, an denen Deutsch bleiben darf. Bisher keine. */ +const erlaubt: string[] = [] + +function quellen(ordner: string): string[] { + return readdirSync(ordner).flatMap(name => { + const pfad = join(ordner, name) + if (statSync(pfad).isDirectory()) return quellen(pfad) + if (!/\.tsx?$/.test(name) || name.includes('.test.')) return [] + return [pfad] + }) +} + +/** + * Kommentare unkenntlich machen, Zeilenumbrüche behalten. + * + * Zeilenweise „fängt die Zeile mit // an?" reicht nicht: Ein Blockkommentar + * hat Fortsetzungszeilen ohne führenden Stern, und genau die wurden als + * Zeichenketten gelesen – fünf Fehlalarme beim ersten Versuch. Also einmal + * richtig durchgehen und dabei mitzählen, ob man gerade in einem Kommentar + * oder in einer Zeichenkette steht. + * + * Ersetzt wird durch Leerzeichen statt zu löschen, damit Zeilennummern und + * Spalten stimmen bleiben. + */ +function ohneKommentare(quelle: string): string { + const out: string[] = [] + let i = 0 + let stringZeichen = '' + while (i < quelle.length) { + const c = quelle[i] + const naechste = quelle[i + 1] + if (stringZeichen) { + if (c === '\\') { out.push(c, naechste ?? ''); i += 2; continue } + if (c === stringZeichen) stringZeichen = '' + out.push(c); i++; continue + } + if (c === '/' && naechste === '/') { + while (i < quelle.length && quelle[i] !== '\n') { out.push(' '); i++ } + continue + } + if (c === '/' && naechste === '*') { + while (i < quelle.length && !(quelle[i] === '*' && quelle[i + 1] === '/')) { + out.push(quelle[i] === '\n' ? '\n' : ' '); i++ + } + out.push(' ', ' '); i += 2; continue + } + if (c === "'" || c === '"' || c === '`') stringZeichen = c + out.push(c); i++ + } + return out.join('') +} + +function deutscheTexte(pfad: string): string[] { + const treffer: string[] = [] + ohneKommentare(readFileSync(pfad, 'utf8')).split('\n').forEach((zeile, i) => { + for (const m of zeile.matchAll(/(['"])((?:(?!\1)[^\\\n])*)\1/g)) { + if (istDeutsch(m[2]) && !erlaubt.includes(m[2])) { + treffer.push(`${pfad}:${i + 1} ${m[0]}`) + } + } + for (const m of zeile.matchAll(/>\s*([^<>{}\n]*?)\s*${m[1]}<`) + } + } + }) + return treffer +} + +describe('Sprache der Oberfläche', () => { + it('enthält keinen deutschen Text mehr', () => { + const gefunden = quellen(hier).flatMap(deutscheTexte) + expect(gefunden, `Deutscher Oberflächentext:\n${gefunden.join('\n')}`).toEqual([]) + }) + + it('erkennt Deutsch überhaupt – auch in Attributen', () => { + // Gegenprobe: Ohne sie wäre der Test oben grün, wenn das Muster kaputt + // ist. Genau dieser Fehler ist am 18.09. passiert. + const probe = '/tmp/aether-sprachprobe.tsx' + const inhalt = [ + '// Ein Kommentar mit Umlaut: Größe – darf nicht auffallen', + 'const a = ', + "const b = 'Löschen'", + 'const c =

Keine Termine für heute

', + ].join('\n') + writeFileSync(probe, inhalt) + const gefunden = deutscheTexte(probe) + expect(gefunden).toHaveLength(3) + expect(gefunden.join(' ')).toContain('Ganztägig') + expect(gefunden.join(' ')).not.toContain('Kommentar') + }) +}) diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index aab8a43..e2ceb49 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -4,7 +4,9 @@ "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], // Vites Zusaetze zum Import: ?url, ?raw und der Seiteneffekt-Import von CSS. - "types": ["vite/client"], + // "node" kam am 24.09.2026 dazu: sprache.test.ts liest den Quelltext von + // der Platte, um deutschen Oberflaechentext zu finden. + "types": ["vite/client", "node"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler",