feat(i18n): Konto, Editor und Karte auf Englisch
Task 10: alle sichtbaren Texte von account/, editor/, card/ und der Seite „Passwort neu setzen“ (Texte, aria-label, title, Meldungen,
Rückfragen) gehen durch t()/msg(); en.ts bekommt 87 Einträge.
- account/: Konto-Menü, Login-Dialog (Titel, Reiter, Felder, Meldungen) und errors.ts (errorMessage übersetzt beim Zurückgeben).
validatePassword nutzt {min}/{max}. Der Login-Dialog wird nur einmal gebaut: die Beschriftung des Schließen-Knopfs wird bei jedem
Öffnen in der aktuellen Sprache gesetzt. LOGIN_NEEDED und DEFAULT_CARD_NAME bleiben deutsche Konstanten (msg), angezeigt werden sie
mit t(); der Standardname einer neuen Karte folgt damit der Sprache („My card“).
- editor/: Vorlagen-, Options- und Vorschauspalte, Bild-Upload, Speichern, Download, Rückfragen. Konstanten (upload.ts, download.ts,
editor-page.ts) bleiben deutsche Strings per msg(); validateImageFile, pngSizeWarning und das Speichern („Speichern fehlgeschlagen –
{reason}“) liefern den Text schon übersetzt, Fehler tragen weiter die deutsche Konstante (Vergleiche mit READ_MESSAGE/PIXELS_MESSAGE
gehen weiter) und werden beim Anzeigen mit t(error.message) übersetzt. Zoom „100 %“ / „100%“ und „Bearbeitet: {name}“ mit Platzhaltern.
- card/: Bezeichnungen der Marken (msg) und die Meldungen des Renderers (msg, Anzeige im Editor über t()).
- pages/reset-page.ts, account/login-dialog.ts: Die Meldung „Die Passwörter stimmen nicht überein.“ ist nicht mehr der Schlüssel zum
Fokus: es zählt die Bedingung selbst, nicht der Vergleich mit dem (nun übersetzten) Text.
- i18n/coverage.test.ts: Heuristik für deutsche Text-Literale außerhalb von t()/msg() (Umlaut/ß, mindestens zwei Wörter, einzelnes
großgeschriebenes Wort; gelesen wird der Syntaxbaum) für account/, editor/, card/ und pages/reset-page.ts; Ausnahmen mit Grund, jede
muss noch zutreffen; Scanner mit eigenen Tests. Die Liste der Bereiche ist eine Konstante für Task 11.
- i18n/english-ui.test.ts (neu): Stichproben auf Englisch (Konto-Menü, Login-Dialog, Passwort-Seite, Editor inkl. Fehlerwege) und
„kein deutscher Wörterbuchtext auf der englischen Seite“.
- test-setup.ts: Wer Module mit vi.resetModules() frisch lädt, bekam eine neue Kopie von i18n.ts mit der Browsersprache (happy-dom: en-US);
der Browser meldet im Test jetzt Deutsch, damit auch diese Tests deutsch beginnen. main.test.ts: nach dem Wechsel auf Englisch heißt
der Knopf des Konto-Menüs „Sign in“.
- Tests: Vitest 53 Dateien, 1779 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a75be2d44c
commit
1ff1873b27
23 files changed
+857
-169
No files matched your search
@@ -1,19 +1,20 @@
|
||||
// Login-Dialog: ein gemeinsames <dialog> mit Anmelden, Registrieren und „Passwort vergessen“.
|
||||
import './account.css';
|
||||
import { el } from '../editor/dom';
|
||||
import { msg, t } from '../i18n/i18n';
|
||||
import { login, register, requestPasswordReset, validatePassword } from './auth';
|
||||
import { errorMessage, errorType, type ErrorContext } from './errors';
|
||||
|
||||
export type LoginView = 'login' | 'register' | 'forgot';
|
||||
|
||||
const TITLES: Record<LoginView, string> = {
|
||||
login: 'Anmelden',
|
||||
register: 'Registrieren',
|
||||
forgot: 'Passwort vergessen',
|
||||
login: msg('Anmelden'),
|
||||
register: msg('Registrieren'),
|
||||
forgot: msg('Passwort vergessen'),
|
||||
};
|
||||
|
||||
const MISMATCH = 'Die Passwörter stimmen nicht überein.';
|
||||
const RESET_SENT = 'Wir haben dir einen Link geschickt. Er gilt eine Stunde.';
|
||||
const MISMATCH = msg('Die Passwörter stimmen nicht überein.');
|
||||
const RESET_SENT = msg('Wir haben dir einen Link geschickt. Er gilt eine Stunde.');
|
||||
|
||||
type Field = { label: HTMLLabelElement; input: HTMLInputElement };
|
||||
|
||||
@@ -37,7 +38,7 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
|
||||
const closeButton = el(
|
||||
'button',
|
||||
{ type: 'button', class: 'auth-close', 'aria-label': 'Schließen' },
|
||||
{ type: 'button', class: 'auth-close', 'aria-label': t('Schließen') },
|
||||
el('span', { 'aria-hidden': 'true' }, '×'),
|
||||
);
|
||||
const title = el('h2', { id: 'auth-title', class: 'auth-title' });
|
||||
@@ -135,7 +136,7 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
'aria-controls': 'auth-panel',
|
||||
tabindex: selected ? 0 : -1,
|
||||
},
|
||||
TITLES[tabView],
|
||||
t(TITLES[tabView]),
|
||||
);
|
||||
tab.addEventListener('click', () => select(tabView));
|
||||
tab.addEventListener('keydown', (event) => {
|
||||
@@ -145,7 +146,7 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
});
|
||||
return tab;
|
||||
});
|
||||
return el('div', { class: 'auth-tabs', role: 'tablist', 'aria-label': 'Konto' }, ...tabs);
|
||||
return el('div', { class: 'auth-tabs', role: 'tablist', 'aria-label': t('Konto') }, ...tabs);
|
||||
}
|
||||
|
||||
/** Setzt oder sperrt alle Bedienelemente, solange eine Anfrage läuft; Eingabefelder bleiben fokussierbar. */
|
||||
@@ -196,10 +197,10 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
const submit = (text: string) => el('button', { type: 'submit', class: 'auth-btn auth-btn-primary' }, text);
|
||||
|
||||
if (forView === 'login') {
|
||||
const email = field('E-Mail', 'email', 'email', 'email', draftEmail);
|
||||
const password = field('Passwort', 'password', 'password', 'current-password');
|
||||
const forgot = linkButton('Passwort vergessen?', () => showView('forgot'));
|
||||
form.append(email.label, password.label, errorEl, okEl, submit('Anmelden'), el('p', { class: 'auth-aside' }, forgot));
|
||||
const email = field(t('E-Mail'), 'email', 'email', 'email', draftEmail);
|
||||
const password = field(t('Passwort'), 'password', 'password', 'current-password');
|
||||
const forgot = linkButton(t('Passwort vergessen?'), () => showView('forgot'));
|
||||
form.append(email.label, password.label, errorEl, okEl, submit(t('Anmelden')), el('p', { class: 'auth-aside' }, forgot));
|
||||
const panel: Panel = { form, first: draftEmail ? password.input : email.input, errorEl, okEl };
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
@@ -214,20 +215,22 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
}
|
||||
|
||||
if (forView === 'register') {
|
||||
const email = field('E-Mail', 'email', 'email', 'email', draftEmail);
|
||||
const password = field('Passwort', 'password', 'password', 'new-password', '', 'auth-pw-hint');
|
||||
const repeat = field('Passwort wiederholen', 'password-repeat', 'password', 'new-password');
|
||||
const hint = el('p', { class: 'auth-hint', id: 'auth-pw-hint' }, 'Mindestens 8 Zeichen.');
|
||||
form.append(email.label, password.label, hint, repeat.label, errorEl, okEl, submit('Registrieren'));
|
||||
const email = field(t('E-Mail'), 'email', 'email', 'email', draftEmail);
|
||||
const password = field(t('Passwort'), 'password', 'password', 'new-password', '', 'auth-pw-hint');
|
||||
const repeat = field(t('Passwort wiederholen'), 'password-repeat', 'password', 'new-password');
|
||||
const hint = el('p', { class: 'auth-hint', id: 'auth-pw-hint' }, t('Mindestens 8 Zeichen.'));
|
||||
form.append(email.label, password.label, hint, repeat.label, errorEl, okEl, submit(t('Registrieren')));
|
||||
const panel: Panel = { form, first: draftEmail ? password.input : email.input, errorEl, okEl };
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
if (busy) return;
|
||||
const address = email.input.value.trim();
|
||||
const problem = password.input.value !== repeat.input.value ? MISMATCH : validatePassword(password.input.value);
|
||||
// Der Fehler liegt am Feld, das ihn verursacht; verglichen wird nie mit dem (übersetzten) Text.
|
||||
const mismatch = password.input.value !== repeat.input.value;
|
||||
const problem = mismatch ? t(MISMATCH) : validatePassword(password.input.value);
|
||||
if (problem) {
|
||||
showError(panel, problem);
|
||||
(problem === MISMATCH ? repeat : password).input.focus();
|
||||
(mismatch ? repeat : password).input.focus();
|
||||
return;
|
||||
}
|
||||
void run(
|
||||
@@ -239,16 +242,16 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
},
|
||||
(err) => {
|
||||
if (errorType(err) !== 'user_already_exists') return undefined;
|
||||
return linkButton('Anmelden', () => showView('login'));
|
||||
return linkButton(t('Anmelden'), () => showView('login'));
|
||||
},
|
||||
);
|
||||
});
|
||||
return panel;
|
||||
}
|
||||
|
||||
const email = field('E-Mail', 'email', 'email', 'email', draftEmail);
|
||||
const back = linkButton('Zurück zur Anmeldung', () => showView('login'));
|
||||
form.append(email.label, errorEl, okEl, submit('Link senden'), el('p', { class: 'auth-aside' }, back));
|
||||
const email = field(t('E-Mail'), 'email', 'email', 'email', draftEmail);
|
||||
const back = linkButton(t('Zurück zur Anmeldung'), () => showView('login'));
|
||||
form.append(email.label, errorEl, okEl, submit(t('Link senden')), el('p', { class: 'auth-aside' }, back));
|
||||
const panel: Panel = { form, first: email.input, errorEl, okEl };
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
@@ -256,7 +259,7 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
const address = email.input.value.trim();
|
||||
void run(panel, 'recover', async () => {
|
||||
await requestPasswordReset(address);
|
||||
showOk(panel, RESET_SENT);
|
||||
showOk(panel, t(RESET_SENT));
|
||||
});
|
||||
});
|
||||
return panel;
|
||||
@@ -268,7 +271,7 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
const typed = content.querySelector<HTMLInputElement>('input[name="email"]');
|
||||
if (typed) draftEmail = typed.value.trim();
|
||||
view = next;
|
||||
title.textContent = TITLES[next];
|
||||
title.textContent = t(TITLES[next]);
|
||||
const panel = buildPanel(next);
|
||||
if (next === 'forgot') {
|
||||
content.replaceChildren(panel.form);
|
||||
@@ -288,6 +291,8 @@ function createLoginDialog(): (view: LoginView) => Promise<boolean> {
|
||||
pending = new Promise<boolean>((resolve) => {
|
||||
settle = resolve;
|
||||
});
|
||||
// Der Dialog bleibt zwischen den Aufrufen im DOM; die Sprache der Seite kann sich seit dem Bauen geändert haben.
|
||||
closeButton.setAttribute('aria-label', t('Schließen'));
|
||||
showView(initial);
|
||||
try {
|
||||
dialog.showModal();
|
||||
|
||||
Reference in new issue
Block a user