From 8f513495e33942453d6b4e3aec5517e4fb8a01cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Tue, 21 Apr 2026 13:32:34 +0200 Subject: [PATCH] feat(app): activeLocale-store mit persistence + initial-detection --- app/src/lib/i18n/activeLocale.test.ts | 65 +++++++++++++++++++++++++++ app/src/lib/i18n/activeLocale.ts | 61 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 app/src/lib/i18n/activeLocale.test.ts create mode 100644 app/src/lib/i18n/activeLocale.ts diff --git a/app/src/lib/i18n/activeLocale.test.ts b/app/src/lib/i18n/activeLocale.test.ts new file mode 100644 index 0000000..300d73f --- /dev/null +++ b/app/src/lib/i18n/activeLocale.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { detectInitialLocale } from './activeLocale'; + +describe('detectInitialLocale', () => { + beforeEach(() => { + globalThis.localStorage?.clear?.(); + }); + + it('nimmt wert aus localStorage, wenn vorhanden und gültig', () => { + const storage = new Map([['locale', 'en']]); + expect(detectInitialLocale({ + storage: { + getItem: (k) => storage.get(k) ?? null, + setItem: () => {} + }, + navigatorLanguage: 'de-DE', + supported: ['de', 'en'] + })).toBe('en'); + }); + + it('fällt auf navigator.language zurück, wenn storage leer', () => { + expect(detectInitialLocale({ + storage: { + getItem: () => null, + setItem: () => {} + }, + navigatorLanguage: 'en-US', + supported: ['de', 'en'] + })).toBe('en'); + }); + + it('normalisiert navigator.language (de-AT → de)', () => { + expect(detectInitialLocale({ + storage: { + getItem: () => null, + setItem: () => {} + }, + navigatorLanguage: 'de-AT', + supported: ['de', 'en'] + })).toBe('de'); + }); + + it('fällt auf ersten supported eintrag, wenn navigator unbekannt', () => { + expect(detectInitialLocale({ + storage: { + getItem: () => null, + setItem: () => {} + }, + navigatorLanguage: 'fr-FR', + supported: ['de', 'en'] + })).toBe('de'); + }); + + it('ignoriert ungültige werte im storage', () => { + const storage = new Map([['locale', 'fr']]); + expect(detectInitialLocale({ + storage: { + getItem: (k) => storage.get(k) ?? null, + setItem: () => {} + }, + navigatorLanguage: 'en-US', + supported: ['de', 'en'] + })).toBe('en'); + }); +}); diff --git a/app/src/lib/i18n/activeLocale.ts b/app/src/lib/i18n/activeLocale.ts new file mode 100644 index 0000000..611be98 --- /dev/null +++ b/app/src/lib/i18n/activeLocale.ts @@ -0,0 +1,61 @@ +import { writable, type Writable } from 'svelte/store'; + +export type SupportedLocale = 'de' | 'en'; +export const SUPPORTED_LOCALES: readonly SupportedLocale[] = ['de', 'en'] as const; +const STORAGE_KEY = 'locale'; + +interface Storage { + getItem: (key: string) => string | null; + setItem: (key: string, value: string) => void; +} + +export interface DetectArgs { + storage: Storage; + navigatorLanguage: string | undefined; + supported: readonly string[]; +} + +export function detectInitialLocale(args: DetectArgs): SupportedLocale { + const stored = args.storage.getItem(STORAGE_KEY); + if (stored && (args.supported as readonly string[]).includes(stored)) { + return stored as SupportedLocale; + } + const nav = (args.navigatorLanguage ?? '').slice(0, 2).toLowerCase(); + if ((args.supported as readonly string[]).includes(nav)) { + return nav as SupportedLocale; + } + return args.supported[0] as SupportedLocale; +} + +function createActiveLocale(): Writable & { bootstrap: () => void } { + const store = writable('de'); + let bootstrapped = false; + + function bootstrap() { + if (bootstrapped) return; + bootstrapped = true; + if (typeof window === 'undefined') return; + const initial = detectInitialLocale({ + storage: window.localStorage, + navigatorLanguage: window.navigator.language, + supported: SUPPORTED_LOCALES + }); + store.set(initial); + store.subscribe((v) => { + try { + window.localStorage.setItem(STORAGE_KEY, v); + } catch { + // private-mode / quota — ignorieren + } + }); + } + + return { + subscribe: store.subscribe, + set: store.set, + update: store.update, + bootstrap + }; +} + +export const activeLocale = createActiveLocale();