diff --git a/web/src/i18n/i18n.test.ts b/web/src/i18n/i18n.test.ts index 11d1013..ed73fa1 100644 --- a/web/src/i18n/i18n.test.ts +++ b/web/src/i18n/i18n.test.ts @@ -179,6 +179,45 @@ describe('onLangChange', () => { expect(calls).toEqual(['en']); }); + it('ein Hörer, der wirft, hält die späteren nicht auf: die Sprache steht, der Fehler geht in die Konsole', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + const boom = new Error('boom'); + const before = listen(); + stops.push( + onLangChange(() => { + throw boom; + }), + ); + const after = listen(); + expect(() => setLang('en')).not.toThrow(); + expect(currentLang()).toBe('en'); + expect(document.documentElement.lang).toBe('en'); + expect(before).toEqual(['en']); + expect(after).toEqual(['en']); + expect(error).toHaveBeenCalledTimes(1); + expect(error).toHaveBeenCalledWith(expect.any(String), boom); + // der werfende Hörer bleibt angemeldet und stört auch den nächsten Wechsel nicht + expect(() => setLang('de')).not.toThrow(); + expect(after).toEqual(['en', 'de']); + expect(error).toHaveBeenCalledTimes(2); + }); + + it('mehrere werfende Hörer: jeder wird gemeldet, alle übrigen laufen', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + const calls: string[] = []; + for (const name of ['a', 'b', 'c']) { + stops.push( + onLangChange(() => { + calls.push(name); + if (name !== 'b') throw new Error(name); + }), + ); + } + setLang('en'); + expect(calls).toEqual(['a', 'b', 'c']); + expect(error).toHaveBeenCalledTimes(2); + }); + it('ein Hörer darf sich beim Melden abmelden, die übrigen werden trotzdem gerufen', () => { const calls: string[] = []; const stopA = onLangChange(() => { diff --git a/web/src/i18n/i18n.ts b/web/src/i18n/i18n.ts index 04e039e..f5306fb 100644 --- a/web/src/i18n/i18n.ts +++ b/web/src/i18n/i18n.ts @@ -11,9 +11,9 @@ export const LANG_KEY = 'aircard.lang'; * Startsprache: die gespeicherte Wahl (`de` oder `en`), sonst Deutsch, wenn die erste Browsersprache mit `de` beginnt, * andernfalls Englisch. Dieselbe Regel steht im Skript in index.html, das vor dem ersten Paint setzt. */ -export function initialLang(stored: string | null, browserLanguages: readonly string[]): Lang { +export function initialLang(stored: string | null, languages: readonly string[]): Lang { if (stored === 'de' || stored === 'en') return stored; - return (browserLanguages[0] ?? '').toLowerCase().startsWith('de') ? 'de' : 'en'; + return (languages[0] ?? '').toLowerCase().startsWith('de') ? 'de' : 'en'; } function readStored(): string | null { @@ -42,7 +42,8 @@ export function currentLang(): Lang { /** * Stellt die Sprache um: setzt , merkt sie sich (außer mit `persist: false`) und meldet einen echten Wechsel an - * die Hörer. Das Neuzeichnen von Kopfzeile und Seite ist Sache der Hörer. + * die Hörer. Das Neuzeichnen von Kopfzeile und Seite ist Sache der Hörer; wirft einer, kommt der Fehler in die Konsole und die + * übrigen werden trotzdem gerufen. */ export function setLang(next: Lang, opts: { persist?: boolean } = {}): void { const changed = next !== currentLang(); @@ -55,8 +56,15 @@ export function setLang(next: Lang, opts: { persist?: boolean } = {}): void { // gilt dann nur für diese Sitzung } } + if (!changed) return; // Kopie, damit sich ein Hörer beim Melden abmelden darf. - if (changed) for (const fn of [...listeners]) fn(next); + for (const fn of [...listeners]) { + try { + fn(next); + } catch (err) { + console.error('Ein Hörer des Sprachwechsels ist gescheitert', err); + } + } } /** Meldet jeden Sprachwechsel (nicht den bloßen Aufruf mit der aktuellen Sprache). Gibt die Abmeldung zurück. */