feat(i18n): Community, Startseite und Meine Karten auf Englisch

Task 11: alle sichtbaren Texte von community/, pages/home-page.ts, pages/community-page.ts, pages/cards-page.ts und pages/format.ts
(Texte, aria-label, title, Platzhalter, Alt-Texte, Rückfragen, Meldungen) gehen durch t()/msg(); en.ts bekommt 82 Einträge (214 → 296).

- Konstanten bleiben deutsche Strings per msg() und gehen beim Anzeigen durch t() (publish.ts, publish-dialog.ts, gallery.ts und die Seiten);
  geworfene Fehler tragen weiter den deutschen Text, damit die Vergleiche gehen. Funktionen, die Text für Nutzer liefern, übersetzen beim
  Zurückgeben (validatePublishForm, publishFailureText, formatDate, reportsLabel). Template-Literale sind Platzhalter geworden ({max},
  {reason}, {name}, {title}, {query}, {count}); der deutsche Text bleibt Zeichen für Zeichen derselbe.
- API-Fehler: jeder ApiError wird mit apiErrorText() angezeigt (Veröffentlichen-Dialog, Detailseite, Meine Karten), nach dem Code des Dienstes
  statt nach dem deutschen Servertext. Logik, die err.message vergleicht (publishCard, 404 beim Zurückziehen), bleibt unverändert.
- Bekannte Reste aus Task 10 behoben: LOGIN_NEEDED ging in publish-dialog.ts, community-page.ts und cards-page.ts ohne t() an die Oberfläche;
  die Meldungen zu Bild lesen/Pixelgrenze/fehlendem Logo (cards-page.ts, GERMAN_MESSAGES in publish-dialog.ts) erscheinen jetzt übersetzt;
  die Zahl der Meldungen in der Logo-Infozeile („3 reports“).
- pages/format.ts: formatDate liefert Deutsch dd.mm.yyyy und Englisch „Oct 4, 2026“ (Intl.DateTimeFormat en-US), reportsLabel „1 report“ /
  „3 reports“. „Herausgeber“ heißt als Bank „Issuer / bank“.
- logos/prepare-logo.ts: der Helfer problem() ist weg, die sechs Stellen schreiben new LogoError(t(MELDUNG)) selbst (gleiches Verhalten);
  so sieht der neue Prüfer unten jede Verwendung einer Konstante.
- i18n/coverage.test.ts: Die Heuristik für deutsche Literale deckt jetzt die ganze Oberfläche ab (account/, card/, community/, editor/, logos/,
  pages/, shell/, main.ts, i18n/api-messages.ts). Sie erkennt zusätzlich Wörter mit Klammer („Ausgeblendet (gemeldet)“) und nimmt rel und
  Namen in Klammerschreibweise (headers['X-Aircard-Jwt']) aus. Acht begründete Ausnahmen kommen dazu (zwei AppwriteException-Meldungen,
  vier interne Logo-Fehler, der Sprachknopf in der Zielsprache). Neu: ein Prüfer über den Syntaxbaum, der verlangt, dass jede Konstante
  mit msg() nur in t(), new Error(), Vergleichen, case und Listen/Mengen vorkommt (auch über Importe und Umbenennungen); ohne Ausnahmen.
- Tests: format.test.ts (beide Sprachen), Englisch-Blöcke in publish-dialog, gallery-grid, home-page, community-page, cards-page und
  logo-info (Texte, Fehlerwege, „Publishing failed – This card is already published.“, kein deutscher Wörterbuchtext auf der Seite). Die
  Hilfen texts()/germanLeft() liegen jetzt in i18n/english-check.ts (vorher in english-ui.test.ts). Der bestehende Englisch-Test in
  cards-page.test.ts sucht den Knopf „Download“ statt „Herunterladen“.
- Vitest 53 Dateien, 1812 Tests grün (vorher 1779), tsc und Build sauber.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 00:34:10 +00:00
1 parent 1ff1873b27
commit 7e66cd0f0f
20 files changed
+1164 -211

No files matched your search

+4
View File
@@ -663,6 +663,10 @@ describe('Admin', () => {
await mount(logo({ reportCount: 1, ownerId: 'u1' }));
expect(buttons()).toEqual(['Report', 'Delete', 'Hide']);
expect(reasons()).toEqual(['Spam']);
expect(reportCount()).toBe('1 report');
row = logo({ hidden: true, reportCount: 3 });
info.update();
expect(reportCount()).toBe('3 reports');
row = logo({ hidden: true, reportCount: 1 });
info.update();
expect(buttons()).toEqual(['Delete', 'Unhide']);
+7 -12
View File
@@ -23,7 +23,7 @@ export const LOGO_SOURCE_MAX_BYTES = 5_242_880;
/** Dieselbe Grenze wie beim Foto im Editor (40 Megapixel): `decodeImage` prüft sie nach dem Laden der Maße, bevor es dekodiert. */
export const LOGO_SOURCE_MAX_PIXELS = MAX_IMAGE_PIXELS;
// Die Meldungen (Spec § 4 und § 11); übersetzt wird beim Auslösen, siehe `problem`.
// Die Meldungen (Spec § 4 und § 11); übersetzt wird beim Auslösen: `new LogoError(t(MELDUNG))`.
export const LOGO_TYPE_MESSAGE = msg('Bitte ein PNG-, JPG-, WebP- oder SVG-Bild wählen.');
export const LOGO_SIZE_MESSAGE = msg('Die Datei ist größer als 5 MB.');
export const LOGO_PIXELS_MESSAGE = msg('Das Bild ist zu groß (max. 40 Megapixel).');
@@ -54,11 +54,6 @@ export interface Size {
/** Ein Fehler mit fertigem, übersetztem Text für die Oberfläche; jeder andere Fehler beim Verarbeiten gilt als „nicht lesbar“. */
class LogoError extends Error {}
/** Ein `LogoError` mit der Meldung in der Sprache der Seite. */
function problem(text: string): LogoError {
return new LogoError(t(text));
}
const RASTER_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
const SVG_TYPE = 'image/svg+xml';
@@ -127,9 +122,9 @@ export function hasTransparency(data: Uint8ClampedArray): boolean {
*/
export function planLogo(data: Uint8ClampedArray, w: number, h: number): { box: Box; size: Size } {
const box = trimBox(data, w, h);
if (!box) throw problem(LOGO_EMPTY_MESSAGE);
if (!box) throw new LogoError(t(LOGO_EMPTY_MESSAGE));
const size = fitWithin(box.w, box.h, LOGO_MAX_W, LOGO_MAX_H);
if (size.w < LOGO_MIN || size.h < LOGO_MIN) throw problem(LOGO_SMALL_MESSAGE);
if (size.w < LOGO_MIN || size.h < LOGO_MIN) throw new LogoError(t(LOGO_SMALL_MESSAGE));
return { box, size };
}
@@ -208,7 +203,7 @@ function newCanvas(w: number, h: number): { canvas: HTMLCanvasElement; ctx: Canv
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) throw problem(LOGO_READ_MESSAGE);
if (!ctx) throw new LogoError(t(LOGO_READ_MESSAGE));
return { canvas, ctx };
}
@@ -228,7 +223,7 @@ async function decode(blob: Blob): Promise<CardImage> {
try {
return await decodeImage(blob);
} catch (err) {
if (err instanceof Error && err.message === PIXELS_MESSAGE) throw problem(LOGO_PIXELS_MESSAGE);
if (err instanceof Error && err.message === PIXELS_MESSAGE) throw new LogoError(t(LOGO_PIXELS_MESSAGE));
throw err;
}
}
@@ -248,7 +243,7 @@ async function drawSvg(file: File): Promise<Drawn> {
function toPng(canvas: HTMLCanvasElement): Promise<Blob> {
return new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(problem(LOGO_READ_MESSAGE))), 'image/png');
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new LogoError(t(LOGO_READ_MESSAGE)))), 'image/png');
});
}
@@ -273,6 +268,6 @@ export async function prepareLogo(file: File): Promise<PreparedLogo> {
hasTransparency: hasTransparency(out.ctx.getImageData(0, 0, size.w, size.h).data),
};
} catch (err) {
throw err instanceof LogoError ? err : problem(LOGO_READ_MESSAGE);
throw err instanceof LogoError ? err : new LogoError(t(LOGO_READ_MESSAGE));
}
}