fix(editor): der Standardname einer neuen Karte folgt der Sprache
Der Editor-Zustand überlebt den Sprachwechsel (nur die Seite wird neu aufgebaut). Sein Standardname „Meine Karte“ blieb dabei deutsch, auch wenn die Seite danach Englisch sprach: Herunterladen („aircard-meine-karte.png“) und Speichern nahmen ihn mit. - state.ts: `defaultCardName()` (der Standardname in der Sprache der Seite, trägt auch initialEditorState) und `followLanguage(store)`: ein Hörer von onLangChange, der den Namen einer noch nicht gespeicherten Karte ersetzt, wenn er dem unberührten Standardnamen der bisherigen Sprache gleicht. Ein eigener Name bleibt, ebenso der Name einer gespeicherten Karte (auch wenn er dem Standard gleicht). Er merkt sich den Standard der Sprache bis zum nächsten Wechsel, so braucht es kein Wörterbuch der Gegensprache; gibt die Abmeldung zurück. - main.ts: `followLanguage(editorStore)` direkt nach bindEditorToAuth, also vor dem Hörer, der Rahmen und Seite neu aufbaut: die neue Seite sieht den neuen Namen. - Tests: Wechsel hin und zurück, Start auf Englisch, eigener Name, zurückgesetzter Name, gespeicherte Karte, leerer Name, Rest des Zustands und genau eine Benachrichtigung, Abmeldung; in main.test.ts mit dem echten Sprachknopf samt Name, den die neu gebaute Seite beim Aufbau sieht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0f11f905e
commit
691b28df19
4 files changed
+154
-5
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DEFAULT_CONFIG, DEFAULT_CROP } from '../card/config';
|
||||
import type { CardImage } from '../card/render';
|
||||
import { createStore, initialEditorState, setImage } from './state';
|
||||
import { setLang } from '../i18n/i18n';
|
||||
import { createStore, defaultCardName, followLanguage, initialEditorState, setImage, type SavedCardRef } from './state';
|
||||
|
||||
const fakeImage = (w = 2000, h = 1000): CardImage => ({ source: {} as CanvasImageSource, w, h });
|
||||
const fakeBlob = () => new Blob(['x'], { type: 'image/png' });
|
||||
@@ -119,3 +120,110 @@ describe('setImage', () => {
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultCardName', () => {
|
||||
it('der Standardname in der Sprache der Seite; initialEditorState trägt ihn', () => {
|
||||
expect(defaultCardName()).toBe('Meine Karte');
|
||||
expect(initialEditorState().cardName).toBe('Meine Karte');
|
||||
setLang('en', { persist: false });
|
||||
expect(defaultCardName()).toBe('My card');
|
||||
expect(initialEditorState().cardName).toBe('My card');
|
||||
});
|
||||
});
|
||||
|
||||
describe('followLanguage: der Standardname einer neuen Karte folgt der Sprache', () => {
|
||||
/** Abmeldungen der Hörer dieses Tests; sie werden nach jedem Test aufgehoben (die Hörerliste von i18n.ts lebt so lange wie die Datei). */
|
||||
const stops: Array<() => void> = [];
|
||||
afterEach(() => {
|
||||
for (const stop of stops.splice(0)) stop();
|
||||
});
|
||||
|
||||
const bound = (patch: Partial<ReturnType<typeof initialEditorState>> = {}) => {
|
||||
const store = createStore({ ...initialEditorState(), ...patch });
|
||||
stops.push(followLanguage(store));
|
||||
return store;
|
||||
};
|
||||
const ref = (name: string): SavedCardRef => ({ id: 'k1', name, imageFileId: 'i', previewFileId: 'p', updatedAt: '2026-10-05T10:00:00.000Z' });
|
||||
|
||||
it('der unberührte Standardname wechselt mit der Sprache, hin und zurück', () => {
|
||||
const store = bound();
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('My card');
|
||||
setLang('de', { persist: false });
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
});
|
||||
|
||||
it('auch wenn der Editor auf Englisch beginnt', () => {
|
||||
setLang('en', { persist: false });
|
||||
const store = bound();
|
||||
expect(store.get().cardName).toBe('My card');
|
||||
setLang('de', { persist: false });
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
});
|
||||
|
||||
it('ein selbst gewählter Name bleibt, in jeder Richtung', () => {
|
||||
const store = bound({ cardName: 'Mein Urlaub' });
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('Mein Urlaub');
|
||||
store.set({ cardName: 'Holiday' });
|
||||
setLang('de', { persist: false });
|
||||
expect(store.get().cardName).toBe('Holiday');
|
||||
});
|
||||
|
||||
it('der Name wurde geändert und wieder auf den Standardtext zurückgesetzt: er gilt als unberührt', () => {
|
||||
const store = bound();
|
||||
store.set({ cardName: 'Urlaub' });
|
||||
store.set({ cardName: 'Meine Karte' });
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('My card');
|
||||
});
|
||||
|
||||
it('eine gespeicherte Karte behält ihren Namen, auch wenn er dem Standard gleicht', () => {
|
||||
const store = bound({ saved: ref('Meine Karte'), cardName: 'Meine Karte' });
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
});
|
||||
|
||||
it('wird die Karte gespeichert, bevor die Sprache wechselt, bleibt der Name', () => {
|
||||
const store = bound();
|
||||
store.set({ saved: ref('Meine Karte') }); // Speichern unter dem Standardnamen
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
});
|
||||
|
||||
it('ein leerer oder fremder Name wird nicht angefasst', () => {
|
||||
const store = bound({ cardName: '' });
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('');
|
||||
});
|
||||
|
||||
it('der Rest des Zustands bleibt unberührt (Bild, Konfiguration), und es gibt genau eine Benachrichtigung', () => {
|
||||
const store = bound();
|
||||
const image = fakeImage();
|
||||
store.set({ image, imageBlob: fakeBlob(), imageChanged: true });
|
||||
const before = store.get();
|
||||
const fn = vi.fn();
|
||||
store.subscribe(fn);
|
||||
setLang('en', { persist: false });
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(store.get()).toEqual({ ...before, cardName: 'My card' });
|
||||
expect(store.get().image).toBe(image);
|
||||
});
|
||||
|
||||
it('derselbe Wechsel ohne Wirkung: gleiche Sprache noch einmal setzen ändert nichts und benachrichtigt nicht', () => {
|
||||
const store = bound();
|
||||
const fn = vi.fn();
|
||||
store.subscribe(fn);
|
||||
setLang('de', { persist: false });
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('nach der Abmeldung des Hörers folgt der Name nicht mehr', () => {
|
||||
const store = createStore(initialEditorState());
|
||||
const stop = followLanguage(store);
|
||||
stop();
|
||||
setLang('en', { persist: false });
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
});
|
||||
});
|
||||
+22
-2
@@ -2,7 +2,7 @@
|
||||
// Der Store wird einmal in main.ts erzeugt und überlebt Seitenwechsel.
|
||||
import { DEFAULT_CONFIG, DEFAULT_CROP, type CardConfig } from '../card/config';
|
||||
import type { CardImage } from '../card/render';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { onLangChange, t } from '../i18n/i18n';
|
||||
|
||||
export interface SavedCardRef {
|
||||
id: string;
|
||||
@@ -50,6 +50,11 @@ export function createStore(initial: EditorState) {
|
||||
|
||||
export type EditorStore = ReturnType<typeof createStore>;
|
||||
|
||||
/** Der Standardname einer neuen Karte in der Sprache der Seite. */
|
||||
export function defaultCardName(): string {
|
||||
return t('Meine Karte');
|
||||
}
|
||||
|
||||
/** Frischer Anfangszustand (der Standardname der Karte in der Sprache der Seite); DEFAULT_CONFIG und DEFAULT_CROP werden kopiert, nie geteilt. */
|
||||
export function initialEditorState(): EditorState {
|
||||
return {
|
||||
@@ -59,11 +64,26 @@ export function initialEditorState(): EditorState {
|
||||
imageChanged: false,
|
||||
saved: null,
|
||||
savedConfig: null,
|
||||
cardName: t('Meine Karte'),
|
||||
cardName: defaultCardName(),
|
||||
saving: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Standardname einer neuen Karte folgt der Sprache der Seite, solange ihn niemand geändert hat: Nach einem Sprachwechsel bekommt eine
|
||||
* Karte, die noch nicht gespeichert ist und den unberührten Standardnamen der bisherigen Sprache trägt, den der neuen. Ein selbst gewählter
|
||||
* Name bleibt, ebenso der Name einer gespeicherten Karte. Gibt die Abmeldung zurück.
|
||||
*/
|
||||
export function followLanguage(store: EditorStore): () => void {
|
||||
let known = defaultCardName(); // der Standardname in der Sprache, die bis zum nächsten Wechsel gilt
|
||||
return onLangChange(() => {
|
||||
const previous = known;
|
||||
known = defaultCardName();
|
||||
const { saved, cardName } = store.get();
|
||||
if (saved === null && cardName === previous && previous !== known) store.set({ cardName: known });
|
||||
});
|
||||
}
|
||||
|
||||
/** Übernimmt ein neues Bild und setzt den Ausschnitt zurück (der alte passt nicht zum neuen Bild). */
|
||||
export function setImage(store: EditorStore, image: CardImage, blob: Blob): void {
|
||||
store.set({
|
||||
|
||||
@@ -148,6 +148,25 @@ describe('Sprachwechsel', () => {
|
||||
expect(document.querySelector('.donate-banner')).toBeNull();
|
||||
});
|
||||
|
||||
it('der unberührte Standardname der Karte wechselt mit der Sprache, schon bevor die Seite neu aufgebaut wird; ein eigener Name bleibt', async () => {
|
||||
const seen: string[] = [];
|
||||
mocks.pages.editor.mockImplementation((_root: unknown, store: unknown) => {
|
||||
seen.push((store as { get(): { cardName: string } }).get().cardName);
|
||||
return vi.fn();
|
||||
});
|
||||
await start('/gestalten');
|
||||
const store = mocks.pages.editor.mock.calls[0][1] as { get(): { cardName: string }; set(patch: object): void };
|
||||
expect(store.get().cardName).toBe('Meine Karte');
|
||||
|
||||
langButton().click();
|
||||
expect(store.get().cardName).toBe('My card');
|
||||
expect(seen).toEqual(['Meine Karte', 'My card']); // die neu gebaute Seite sieht den Namen der neuen Sprache
|
||||
|
||||
store.set({ cardName: 'Mein Urlaub' });
|
||||
langButton().click(); // zurück auf Deutsch
|
||||
expect(store.get().cardName).toBe('Mein Urlaub');
|
||||
});
|
||||
|
||||
it('das Konto-Menü wird neu gebaut und zeigt sich sofort, wenn der Anmeldestatus schon bekannt ist', async () => {
|
||||
await start('/');
|
||||
await vi.waitFor(() => expect(signIn()).toBeDefined());
|
||||
|
||||
+3
-1
@@ -12,7 +12,7 @@ import { mountHomePage } from './pages/home-page';
|
||||
import { mountResetPage } from './pages/reset-page';
|
||||
import { mountEditor } from './editor/editor-page';
|
||||
import { bindEditorToAuth } from './editor/auth-reset';
|
||||
import { createStore, initialEditorState } from './editor/state';
|
||||
import { createStore, followLanguage, initialEditorState } from './editor/state';
|
||||
import { mountDonateBanner } from './shell/donate-banner';
|
||||
import { mountFooter } from './shell/footer';
|
||||
import { mountHeader } from './shell/header';
|
||||
@@ -34,6 +34,8 @@ if (legacyTarget) history.replaceState(null, '', legacyTarget);
|
||||
const editorStore = createStore(initialEditorState());
|
||||
// … und gehört zum angemeldeten Konto: beim Abmelden oder Kontowechsel wird er geleert bzw. entkoppelt.
|
||||
bindEditorToAuth(editorStore);
|
||||
// … und sein Standardname („Meine Karte“) folgt der Sprache, solange ihn niemand ändert. Der Hörer steht vor dem Neuaufbau der Seite (unten).
|
||||
followLanguage(editorStore);
|
||||
|
||||
// Der Rahmen der Seite: Spenden-Banner (ohne DONATE_URL bleibt der Platz leer), Kopfzeile mit Navigation, Suche und Knöpfen,
|
||||
// Konto-Menü im Seitenkopf und Fußzeile. Alle tragen Texte der gewählten Sprache und werden bei einem Sprachwechsel neu aufgebaut.
|
||||
|
||||
Reference in new issue
Block a user