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:
1 parent
1ff1873b27
commit
7e66cd0f0f
20 files changed
+1164
-211
No files matched your search
@@ -2,6 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { User } from '../account/auth';
|
||||
import { ApiError, type ReportReason } from '../community/api';
|
||||
import type { CheckedImage, GalleryCard, GalleryReport } from '../community/gallery';
|
||||
import { germanLeft } from '../i18n/english-check';
|
||||
import { setLang } from '../i18n/i18n';
|
||||
|
||||
// gallery.ts, api.ts und auth.ts hängen am Appwrite-Client; die Seite bekommt alles über ihre Abhängigkeiten.
|
||||
vi.mock('../account/appwrite', () => ({
|
||||
@@ -817,3 +819,172 @@ describe('Detailseite: Admin', () => {
|
||||
expect(q('img')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Detailseite auf Englisch', () => {
|
||||
beforeEach(() => setLang('en', { persist: false }));
|
||||
|
||||
it('Ansichten ohne Karte: nicht verfügbar, Laden, Ladefehler', async () => {
|
||||
getCard.mockResolvedValue(null);
|
||||
await mount();
|
||||
expect(q('h1')?.textContent).toBe('This card is no longer available.');
|
||||
expect(q('a[href="/#community"]')?.textContent).toBe('Back to gallery');
|
||||
expect(document.title).toBe('Community – AirCard-IOS');
|
||||
expect(germanLeft(root)).toEqual([]);
|
||||
unmount?.();
|
||||
|
||||
getCard.mockReturnValue(new Promise(() => {}));
|
||||
await mount();
|
||||
expect(q('[role="status"]')?.textContent).toBe('Loading card …');
|
||||
unmount?.();
|
||||
|
||||
getCard.mockReset();
|
||||
getCard.mockRejectedValueOnce(new TypeError('offline'));
|
||||
await mount();
|
||||
expect(errorText()).toBe('The card couldn’t be loaded right now.');
|
||||
expect(buttonByText('Try again')).toBeDefined();
|
||||
expect(germanLeft(root)).toEqual([]);
|
||||
});
|
||||
|
||||
it('Karte eines Gastes: Datum, Verfasser, Alt-Text, Knöpfe, Bildfehler und Herunterladen', async () => {
|
||||
getCard.mockResolvedValue(card({ displayName: '' }));
|
||||
loadImage.mockReturnValueOnce(new Promise(() => {}));
|
||||
await mount();
|
||||
expect(q('.detail-card')?.textContent).toBe('Loading image …');
|
||||
unmount?.();
|
||||
|
||||
loadImage.mockImplementation(async () => image());
|
||||
await mount();
|
||||
expect(q('.detail-meta')?.textContent).toBe('by Anonymous · Oct 4, 2026');
|
||||
expect(q('.detail-card img')?.getAttribute('alt')).toBe('Community card “Mitternachtsblau”');
|
||||
expect(q('.detail-tag')?.getAttribute('title')).toBe('Show cards with this tag');
|
||||
expect(q('.detail-back')?.textContent).toBe('← Back to gallery');
|
||||
expect(buttonTexts()).toEqual(['Download', 'Use as template', 'Report']);
|
||||
expect(germanLeft(root)).toEqual([]);
|
||||
|
||||
// Bild nicht darstellbar, Herunterladen holt es nach: unterwegs „Downloading …“, danach der Fehler
|
||||
q('.detail-card img')!.dispatchEvent(new Event('error'));
|
||||
expect(q('.detail-card')?.textContent).toContain('The image couldn’t be loaded.');
|
||||
loadImage.mockReturnValueOnce(new Promise(() => {}));
|
||||
buttonByText('Download')!.click();
|
||||
expect(buttonTexts()).toContain('Downloading …');
|
||||
unmount?.();
|
||||
|
||||
await mount();
|
||||
q('.detail-card img')!.dispatchEvent(new Event('error'));
|
||||
loadImage.mockRejectedValueOnce(new TypeError('offline'));
|
||||
buttonByText('Download')!.click();
|
||||
await settle();
|
||||
expect(errorText()).toBe('The image couldn’t be loaded.');
|
||||
});
|
||||
|
||||
it('Melden-Dialog: Hinweis, wenn er sich nicht öffnen lässt, und der Dialog selbst auf Englisch', async () => {
|
||||
reportCard.mockRejectedValue(new ApiError(429, 'Zu viele Meldungen – bitte später nochmal.', undefined, 'tooManyReports'));
|
||||
await mount();
|
||||
buttonByText('Report')!.click();
|
||||
await settle();
|
||||
expect([...reportEl()!.querySelectorAll('.report-reason')].map((l) => l.textContent)).toEqual([
|
||||
'Inappropriate',
|
||||
'Real card data',
|
||||
'Copyright/trademark',
|
||||
'Spam',
|
||||
'Other',
|
||||
]);
|
||||
const radio = reportEl()!.querySelector<HTMLInputElement>('input[value="spam"]')!;
|
||||
radio.checked = true;
|
||||
reportEl()!.querySelector('form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
await settle();
|
||||
expect(reportEl()!.querySelector('[role="alert"]')!.textContent).toBe('Too many reports – please try again later.');
|
||||
unmount?.();
|
||||
reportEl()?.remove();
|
||||
|
||||
const spy = vi.spyOn(HTMLDialogElement.prototype, 'showModal').mockImplementation(() => {
|
||||
throw new Error('nicht darstellbar');
|
||||
});
|
||||
try {
|
||||
await mount();
|
||||
buttonByText('Report')!.click();
|
||||
await settle();
|
||||
expect(errorText()).toBe('The report dialog couldn’t be opened.');
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('Autor: Zurückziehen mit Rückfrage und die Fehler als Text der Seite (Code, Anmeldung, Netz)', async () => {
|
||||
user = owner;
|
||||
await mount();
|
||||
expect(buttonTexts()).toEqual(['Download', 'Use as template', 'Report', 'Withdraw']);
|
||||
buttonByText('Withdraw')!.click();
|
||||
await settle();
|
||||
expect(confirmEl()!.querySelector('h2')!.textContent).toBe('Withdraw card “Mitternachtsblau”?');
|
||||
expect(confirmEl()!.querySelector('p')!.textContent).toBe(
|
||||
'The card disappears from the gallery, and its image and reports are deleted. Your private card is kept.',
|
||||
);
|
||||
expect([...confirmEl()!.querySelectorAll('button')].map((b) => b.textContent)).toEqual(['Cancel', 'Withdraw']);
|
||||
withdrawCard.mockRejectedValueOnce(new ApiError(503, 'Der Dienst ist gerade nicht erreichbar.', undefined, 'unavailable'));
|
||||
confirmButton('Withdraw').click();
|
||||
await settle();
|
||||
expect(errorText()).toBe('The service is currently unavailable.');
|
||||
|
||||
// ohne bekannten Code zeigt Englisch die allgemeine Meldung zum Status
|
||||
withdrawCard.mockRejectedValueOnce(new ApiError(403, 'Nur für Administratoren.'));
|
||||
buttonByText('Withdraw')!.click();
|
||||
await settle();
|
||||
confirmButton('Withdraw').click();
|
||||
await settle();
|
||||
expect(errorText()).toBe('You are not allowed to do that.');
|
||||
|
||||
withLogin.mockRejectedValueOnce(new LoginCancelled());
|
||||
buttonByText('Withdraw')!.click();
|
||||
await settle();
|
||||
confirmButton('Withdraw').click();
|
||||
await settle();
|
||||
expect(errorText()).toBe('Please sign in to continue.');
|
||||
|
||||
withLogin.mockRejectedValueOnce(new TypeError('offline'));
|
||||
buttonByText('Withdraw')!.click();
|
||||
await settle();
|
||||
confirmButton('Withdraw').click();
|
||||
await settle();
|
||||
expect(errorText()).toBe('Can’t reach the server.');
|
||||
expect(germanLeft(root)).toEqual([]);
|
||||
});
|
||||
|
||||
it('Admin: Moderation, Meldungen mit Grund und Datum, Ausblenden/Einblenden mit Hinweis, Löschen mit Rückfrage', async () => {
|
||||
user = admin;
|
||||
getCard.mockResolvedValue(card({ reportCount: 3 }));
|
||||
listReports.mockResolvedValue([report(1, 'spam'), report(2, 'kartendaten'), report(3, 'unbekannt')]);
|
||||
await mount();
|
||||
expect(q('.detail-admin-title')?.textContent).toBe('Moderation');
|
||||
expect(q('.detail-reports-count')?.textContent).toBe('3 reports');
|
||||
expect(qa('.detail-reasons li').map((li) => li.textContent)).toEqual([
|
||||
'Spam · Oct 5, 2026',
|
||||
'Real card data · Oct 5, 2026',
|
||||
'unbekannt · Oct 5, 2026', // unbekannte Werte der API bleiben unverändert
|
||||
]);
|
||||
expect(buttonTexts()).toEqual(['Download', 'Use as template', 'Report', 'Hide', 'Delete']);
|
||||
expect(germanLeft(root)).toEqual([]);
|
||||
|
||||
buttonByText('Hide')!.click();
|
||||
await settle();
|
||||
expect(q('.detail-hidden')?.textContent).toBe('Hidden – This card isn’t visible to others.');
|
||||
expect(statusText()).toBe('Card hidden.');
|
||||
buttonByText('Unhide')!.click();
|
||||
await settle();
|
||||
expect(statusText()).toBe('Card unhidden.');
|
||||
|
||||
buttonByText('Delete')!.click();
|
||||
await settle();
|
||||
expect(confirmEl()!.querySelector('h2')!.textContent).toBe('Delete card “Mitternachtsblau”?');
|
||||
expect(confirmEl()!.querySelector('p')!.textContent).toBe('The card, its image and all reports will be deleted permanently.');
|
||||
expect([...confirmEl()!.querySelectorAll('button')].map((b) => b.textContent)).toEqual(['Cancel', 'Delete']);
|
||||
confirmButton('Cancel').click();
|
||||
await settle();
|
||||
|
||||
// die Gründe lassen sich nicht laden
|
||||
unmount?.();
|
||||
listReports.mockRejectedValue(new TypeError('offline'));
|
||||
await mount();
|
||||
expect(q('.detail-admin')?.textContent).toContain('The reasons couldn’t be loaded.');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user