feat(logos): Datenschicht, API-Aufrufe und übersetzbare API-Fehler

- logos/logos.ts (Muster community/gallery.ts): CommunityLogo, toLogo, logoQueries (Beliebt/Neu, Admin-Filter Gemeldet und
  Ausgeblendet, Cursor), listLogos (nextCursor nur bei voller Seite von 24), getLogo (ungültige ID oder 404 -> null),
  myHearts (eigene Herzen einer Seite, ohne IDs keine Anfrage), listLogoReports (ohne Melder-Hash), loadLogoBlob.
- logos/logo-api.ts: uploadLogo, setHeart, reportLogo (ohne Anmeldung), deleteLogo, setLogoVisibility über apiPost.
  uploadLogo behandelt das Limit (429 tooManyLogos) wie publishCard als eindeutige Ablehnung (die hochgeladene Datei darf
  aufgeräumt werden); eine Antwort ohne id (und bei setHeart ohne Zustand und Zähler) ist 503 mit offenem Ausgang.
- account/appwrite.ts: IDS um logos, logoLikes, logoReports und logoBucket.
- community/api.ts: ApiError.serverCode (der Schlüssel aus {error, code}; code bleibt der HTTP-Status, message der deutsche
  Text). Client-Fehler tragen unavailable (auch 502/503/504 vom Proxy, vor dem Lesen des Bodys), generic, jwtUnsupported,
  alreadyPublished, tooManyPublications. Neu: unavailableError() und apiErrorText(err). Die deutschen Texte stehen einmal
  in api-messages.ts, die Vergleiche in publishCard laufen unverändert über err.message.
- i18n/api-messages.ts: API_MESSAGES (alle 33 Texte von MSG plus die Client-Codes generic und jwtUnsupported) und
  apiMessage: bekannter Code -> t(Text); unbekannt: Deutsch der Servertext, Englisch eine Meldung je Status (400, 403, 404,
  sonst allgemein). Der Code badRequest trägt auch die dynamischen Prüftexte des Dienstes, Deutsch zeigt dann den
  genaueren Servertext.
- i18n/en.ts: englische Texte zu allen API_MESSAGES.
- Tests: api-messages.test.ts lädt MSG aus deploy/api/common.js (createRequire) und prüft den Gleichlauf in beide
  Richtungen; logos.test.ts, logo-api.test.ts und neue Fälle in api.test.ts (der Helfer freshApi stellt die frische
  i18n-Instanz auf Deutsch). 156 neue Tests, die bisherigen unverändert grün (1285 gesamt).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 19:59:08 +00:00
1 parent fc9e5f5577
commit 3051b3b3f9
10 files changed
+1491 -21

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
// Übersetzbare Fehlertexte der API: Der Dienst `aircard-api` (deploy/api) antwortet auf Fehler mit `{error, code}`. `error` ist der
// deutsche Text, `code` sein Schlüssel in MSG (deploy/api/common.js). Hier steht jeder Text noch einmal unter seinem Schlüssel
// (der Browser lädt den Dienst nicht); api-messages.test.ts lädt MSG und prüft den Gleichlauf. Dazu kommen zwei Client-Codes.
// Übersetzt wird beim Anzeigen (`apiMessage`); `ApiError.message` bleibt der deutsche Text, damit Vergleiche damit weiter gehen.
import { currentLang, msg, t } from './i18n';
export const API_MESSAGES: Readonly<Record<string, string>> = {
// Die Schlüssel und Texte von MSG, in derselben Reihenfolge.
unauthorized: msg('Bitte melde dich an.'),
notFound: msg('Nicht gefunden.'),
cardGone: msg('Diese Karte ist nicht (mehr) verfügbar.'),
forbidden: msg('Dazu hast du keine Berechtigung.'),
adminOnly: msg('Nur für Admins.'),
foreignFile: msg('Diese Datei gehört dir nicht.'),
fileMissing: msg('Bild nicht gefunden.'),
fileType: msg('Das Bild muss eine PNG- oder WebP-Datei sein.'),
fileSize: msg('Das Bild ist größer als 5 MB.'),
alreadyPublished: msg('Diese Karte ist schon veröffentlicht.'),
filesTaken: msg('Diese Bilder sind bereits veröffentlicht oder gesperrt.'),
imageFormat: msg('Das Bild hat nicht das Kartenformat.'),
notYourCard: msg('Diese Karte gehört nicht zu deinem Konto.'),
tooManyPublications: msg('Du hast schon 20 Karten veröffentlicht. Zieh eine zurück, um eine neue zu veröffentlichen.'),
tooManyReports: msg('Zu viele Meldungen – bitte später nochmal.'),
badRequest: msg('Ungültige Anfrage.'),
badCardId: msg('Ungültige Karten-ID.'),
badReason: msg('Bitte wähle einen gültigen Grund.'),
badVisibility: msg('Ungültige Angabe zur Sichtbarkeit.'),
logoGone: msg('Dieses Logo ist nicht (mehr) verfügbar.'),
badLogoId: msg('Ungültige Logo-ID.'),
logoNameMissing: msg('Bitte gib einen Namen an.'),
logoFileType: msg('Das Logo muss eine PNG-Datei sein.'),
logoFileSize: msg('Das Logo ist größer als 1 MB.'),
logoFormat: msg('Das Logo hat ungültige Maße (8 × 8 bis 720 × 250 Pixel).'),
logoTaken: msg('Diese Datei wird schon für ein Logo verwendet.'),
tooManyLogos: msg('Du hast schon 30 Logos hochgeladen. Lösche eins, um ein neues hochzuladen.'),
badHeart: msg('Ungültige Angabe zum Herz.'),
badMono: msg('Ungültige Angabe zu „einfarbig“.'),
tooLarge: msg('Die Anfrage ist zu groß.'),
jsonOnly: msg('Nur application/json wird unterstützt.'),
unavailable: msg('Der Dienst ist gerade nicht erreichbar.'),
internal: msg('Interner Fehler.'),
// Nur im Client (kein Schlüssel im Dienst): allgemeiner Fehler und „Anmeldung per Token ausgeschaltet“.
generic: msg('Etwas ist schiefgelaufen. Bitte nochmal versuchen.'),
jwtUnsupported: msg('Anmeldung per Token ist auf dem Server ausgeschaltet – bitte den Betreiber informieren.'),
};
/** Die allgemeine Meldung zu einem Status, wenn der Code unbekannt ist (Spec § 7): 400, 403, 404, sonst „Etwas ist schiefgelaufen“. */
function fallbackText(status: number): string {
if (status === 400) return API_MESSAGES.badRequest;
if (status === 403) return API_MESSAGES.forbidden;
if (status === 404) return API_MESSAGES.notFound;
return API_MESSAGES.generic;
}
/**
* Der Text zu einem Fehler des Dienstes (oder einem Fehler, den der Client mit denselben Codes erzeugt) in der Sprache der Seite.
* Ein bekannter Code wird über das Wörterbuch übersetzt. Ein unbekannter oder fehlender Code (älterer Dienst, Proxy) zeigt auf
* Deutsch den Text des Dienstes, auf Englisch eine allgemeine Meldung je Status.
*/
export function apiMessage(serverCode: string | null, status: number, serverText: string): string {
const english = currentLang() === 'en';
if (serverCode !== null && Object.hasOwn(API_MESSAGES, serverCode)) {
// `badRequest` tragen auch die dynamischen Prüftexte des Dienstes („Der Titel ist zu lang (höchstens 60 Zeichen).“):
// Deutsch zeigt dann den genaueren Servertext, Englisch den allgemeinen Text des Codes.
if (!english && serverCode === 'badRequest') return serverText;
return t(API_MESSAGES[serverCode]);
}
return english ? t(fallbackText(status)) : serverText;
}