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,7 +1,9 @@
|
||||
// Konto-Menü im Seitenkopf: abgemeldet „Anmelden“, angemeldet ein Avatar-Kreis mit Initiale
|
||||
// und einem Menü (E-Mail, „Meine Karten“, „Abmelden“).
|
||||
// und einem Menü (E-Mail, „Meine Karten“, „Abmelden“). Die Texte stehen in der Sprache der Seite; nach einem Sprachwechsel
|
||||
// baut main.ts das Menü neu auf.
|
||||
import './account.css';
|
||||
import { el } from '../editor/dom';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { currentUser, logout, onAuthChange } from './auth';
|
||||
import { errorMessage } from './errors';
|
||||
import { requireLogin } from './login-dialog';
|
||||
@@ -26,7 +28,7 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
}
|
||||
const user = currentUser();
|
||||
if (!user) {
|
||||
const signIn = el('button', { type: 'button', class: 'auth-btn auth-btn-small' }, 'Anmelden');
|
||||
const signIn = el('button', { type: 'button', class: 'auth-btn auth-btn-small' }, t('Anmelden'));
|
||||
signIn.addEventListener('click', () => void requireLogin('login'));
|
||||
slot.replaceChildren(el('div', { class: 'account-menu' }, signIn));
|
||||
return;
|
||||
@@ -38,18 +40,18 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
{
|
||||
type: 'button',
|
||||
class: 'account-avatar',
|
||||
'aria-label': 'Konto-Menü',
|
||||
'aria-label': t('Konto-Menü'),
|
||||
'aria-expanded': 'false',
|
||||
'aria-controls': 'account-dropdown',
|
||||
},
|
||||
initial,
|
||||
);
|
||||
const email = el('span', { class: 'account-email', title: user.email }, user.email);
|
||||
const myCards = el('a', { class: 'account-item', href: '/karten', 'data-link': true }, 'Meine Karten');
|
||||
const signOut = el('button', { type: 'button', class: 'account-item' }, 'Abmelden');
|
||||
const myCards = el('a', { class: 'account-item', href: '/karten', 'data-link': true }, t('Meine Karten'));
|
||||
const signOut = el('button', { type: 'button', class: 'account-item' }, t('Abmelden'));
|
||||
const error = el('span', { class: 'account-error', role: 'alert' });
|
||||
const dropdown = el('div', { class: 'account-dropdown', id: 'account-dropdown', hidden: true }, email, myCards, signOut, error);
|
||||
const wrap = el('div', { class: 'account-menu', role: 'group', 'aria-label': 'Konto' }, avatar, dropdown);
|
||||
const wrap = el('div', { class: 'account-menu', role: 'group', 'aria-label': t('Konto') }, avatar, dropdown);
|
||||
|
||||
let isOpen = false;
|
||||
function setOpen(open: boolean): void {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Anmelden, Registrieren, Abmelden, Passwort zurücksetzen. Hält den angemeldeten Nutzer im Speicher
|
||||
// und meldet Änderungen an Abonnenten (Konto-Menü, später Karten-Seite).
|
||||
import { ID } from 'appwrite';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { account } from './appwrite';
|
||||
import { errorType, isUnauthorized } from './errors';
|
||||
|
||||
@@ -132,9 +133,9 @@ export function onLogout(fn: () => void): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
/** null, wenn das Passwort passt; sonst die Meldung für den Nutzer. */
|
||||
/** null, wenn das Passwort passt; sonst die Meldung für den Nutzer (in der Sprache der Seite). */
|
||||
export function validatePassword(pw: string): string | null {
|
||||
return pw.length >= PASSWORD_MIN && pw.length <= PASSWORD_MAX
|
||||
? null
|
||||
: `Das Passwort muss ${PASSWORD_MIN} bis ${PASSWORD_MAX} Zeichen lang sein.`;
|
||||
: t('Das Passwort muss {min} bis {max} Zeichen lang sein.', { min: PASSWORD_MIN, max: PASSWORD_MAX });
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { AppwriteException, ID, Permission, Query, Role, type Models } from 'appwrite';
|
||||
import { ID_PATTERN, parseConfig, serializeConfig, type CardConfig } from '../card/config';
|
||||
import type { SavedCardRef } from '../editor/state';
|
||||
import { msg, t } from '../i18n/i18n';
|
||||
import { APPWRITE_ENDPOINT, IDS, client, storage, tables } from './appwrite';
|
||||
import { currentUserId } from './auth';
|
||||
import { errorType } from './errors';
|
||||
@@ -44,16 +45,16 @@ export interface SaveInput {
|
||||
previous: SavedCardRef | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_CARD_NAME = 'Meine Karte';
|
||||
export const DEFAULT_CARD_NAME = msg('Meine Karte');
|
||||
const NAME_MAX = 80;
|
||||
/** Zeilen- und Datei-IDs von Appwrite (definiert in card/config.ts, dort braucht sie auch die Logo-ID der Konfiguration). */
|
||||
export { ID_PATTERN };
|
||||
|
||||
/** Trimmt, fasst Leerraum zusammen und kürzt auf 80 Zeichen (Codepoints); leer ergibt „Meine Karte“. */
|
||||
/** Trimmt, fasst Leerraum zusammen und kürzt auf 80 Zeichen (Codepoints); leer ergibt „Meine Karte“ (in der Sprache der Seite). */
|
||||
export function normalizeName(name: string): string {
|
||||
const collapsed = name.replace(/\s+/g, ' ').trim();
|
||||
const cut = Array.from(collapsed).slice(0, NAME_MAX).join('').trim();
|
||||
return cut || DEFAULT_CARD_NAME;
|
||||
return cut || t(DEFAULT_CARD_NAME);
|
||||
}
|
||||
|
||||
/** Nur der Besitzer darf die Zeile bzw. Datei lesen, ändern und löschen. */
|
||||
|
||||
+25
-23
@@ -1,11 +1,13 @@
|
||||
// Deutsche Fehlermeldungen für Appwrite-Fehler. Rein und ohne Appwrite-Import, damit gut testbar.
|
||||
// Fehlermeldungen für Appwrite-Fehler in der Sprache der Seite: Die deutschen Texte sind die Schlüssel für t() (i18n/en.ts).
|
||||
// Rein und ohne Appwrite-Import, damit gut testbar.
|
||||
// Ein AppwriteException trägt `code` (HTTP-Status), `type` (z. B. user_invalid_credentials) und `message`.
|
||||
import { msg, t } from '../i18n/i18n';
|
||||
|
||||
export type ErrorContext = 'login' | 'register' | 'recover' | 'reset' | 'save' | 'load';
|
||||
|
||||
const LINK_INVALID = 'Der Link ist ungültig oder abgelaufen.';
|
||||
const WRONG_CREDENTIALS = 'E-Mail oder Passwort ist falsch.';
|
||||
const GENERIC = 'Etwas ist schiefgelaufen. Bitte nochmal versuchen.';
|
||||
const LINK_INVALID = msg('Der Link ist ungültig oder abgelaufen.');
|
||||
const WRONG_CREDENTIALS = msg('E-Mail oder Passwort ist falsch.');
|
||||
const GENERIC = msg('Etwas ist schiefgelaufen. Bitte nochmal versuchen.');
|
||||
|
||||
function field(err: unknown, key: 'code' | 'type' | 'message'): unknown {
|
||||
return typeof err === 'object' && err !== null ? (err as Record<string, unknown>)[key] : undefined;
|
||||
@@ -40,46 +42,46 @@ export function isInvalidLinkError(err: unknown): boolean {
|
||||
}
|
||||
|
||||
function passwordMessage(err: unknown, ctx: ErrorContext): string {
|
||||
if (ctx === 'login') return WRONG_CREDENTIALS;
|
||||
if (ctx === 'login') return t(WRONG_CREDENTIALS);
|
||||
const message = field(err, 'message');
|
||||
if (typeof message === 'string' && message.includes('commonly used')) {
|
||||
return 'Dieses Passwort ist zu verbreitet. Bitte wähle ein anderes.';
|
||||
return t('Dieses Passwort ist zu verbreitet. Bitte wähle ein anderes.');
|
||||
}
|
||||
return 'Das Passwort muss 8 bis 256 Zeichen lang sein.';
|
||||
return t('Das Passwort muss 8 bis 256 Zeichen lang sein.');
|
||||
}
|
||||
|
||||
/** Meldung für Nutzer: zuerst nach `type`, dann nach `code`, Netzfehler (TypeError) zuletzt. */
|
||||
/** Meldung für Nutzer in der Sprache der Seite: zuerst nach `type`, dann nach `code`, Netzfehler (TypeError) zuletzt. */
|
||||
export function errorMessage(err: unknown, ctx: ErrorContext): string {
|
||||
switch (errorType(err)) {
|
||||
case 'user_invalid_credentials':
|
||||
return WRONG_CREDENTIALS;
|
||||
return t(WRONG_CREDENTIALS);
|
||||
case 'user_already_exists':
|
||||
return 'Diese E-Mail ist schon registriert. Melde dich an.';
|
||||
return t('Diese E-Mail ist schon registriert. Melde dich an.');
|
||||
case 'user_invalid_token':
|
||||
return LINK_INVALID;
|
||||
return t(LINK_INVALID);
|
||||
case 'user_not_found':
|
||||
if (ctx === 'recover') return 'Zu dieser E-Mail gibt es kein Konto.';
|
||||
if (ctx === 'reset') return LINK_INVALID;
|
||||
if (ctx === 'recover') return t('Zu dieser E-Mail gibt es kein Konto.');
|
||||
if (ctx === 'reset') return t(LINK_INVALID);
|
||||
break;
|
||||
case 'general_argument_invalid': {
|
||||
const param = invalidParam(err);
|
||||
if (param === 'email') return 'Bitte gib eine gültige E-Mail-Adresse ein.';
|
||||
if (param === 'email') return t('Bitte gib eine gültige E-Mail-Adresse ein.');
|
||||
if (param === 'password') return passwordMessage(err, ctx);
|
||||
if (param === 'userId' || param === 'secret') return LINK_INVALID;
|
||||
if (param === 'userId' || param === 'secret') return t(LINK_INVALID);
|
||||
break;
|
||||
}
|
||||
case 'row_not_found':
|
||||
return 'Diese Karte gibt es nicht (mehr) oder sie gehört nicht zu deinem Konto.';
|
||||
return t('Diese Karte gibt es nicht (mehr) oder sie gehört nicht zu deinem Konto.');
|
||||
case 'storage_file_not_found':
|
||||
return 'Das Bild dieser Karte fehlt.';
|
||||
return t('Das Bild dieser Karte fehlt.');
|
||||
case 'general_unknown_origin':
|
||||
return 'Diese Seite ist beim Server nicht freigeschaltet. Bitte melde dich beim Betreiber.';
|
||||
return t('Diese Seite ist beim Server nicht freigeschaltet. Bitte melde dich beim Betreiber.');
|
||||
case 'user_session_already_exists':
|
||||
return 'Du bist schon angemeldet. Bitte lade die Seite neu.';
|
||||
return t('Du bist schon angemeldet. Bitte lade die Seite neu.');
|
||||
case 'user_count_exceeded':
|
||||
return 'Zurzeit sind keine neuen Konten möglich.';
|
||||
return t('Zurzeit sind keine neuen Konten möglich.');
|
||||
}
|
||||
if (field(err, 'code') === 429) return 'Zu viele Versuche – bitte kurz warten.';
|
||||
if (err instanceof TypeError) return 'Keine Verbindung zum Server.';
|
||||
return GENERIC;
|
||||
if (field(err, 'code') === 429) return t('Zu viele Versuche – bitte kurz warten.');
|
||||
if (err instanceof TypeError) return t('Keine Verbindung zum Server.');
|
||||
return t(GENERIC);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Führt eine Kartenanfrage aus und meldet bei abgelaufener Sitzung (401, oder 404 für Gäste) einmal neu an.
|
||||
import { msg } from '../i18n/i18n';
|
||||
import { getCurrentUser } from './auth';
|
||||
import { errorType, isUnauthorized } from './errors';
|
||||
import { requireLogin } from './login-dialog';
|
||||
|
||||
/** Hinweis, wenn eine Aktion an einer abgebrochenen Anmeldung scheitert. */
|
||||
export const LOGIN_NEEDED = 'Bitte melde dich an, um fortzufahren.';
|
||||
/** Hinweis, wenn eine Aktion an einer abgebrochenen Anmeldung scheitert; angezeigt wird er mit `t(LOGIN_NEEDED)`. */
|
||||
export const LOGIN_NEEDED = msg('Bitte melde dich an, um fortzufahren.');
|
||||
|
||||
/** Der Nutzer hat den Login-Dialog geschlossen, ohne sich anzumelden. */
|
||||
export class LoginCancelled extends Error {
|
||||
|
||||
Reference in new issue
Block a user