feat(app): activeLocale-store mit persistence + initial-detection

This commit is contained in:
Jörg Lohrer 2026-04-21 13:32:34 +02:00
parent f799223836
commit 8f513495e3
2 changed files with 126 additions and 0 deletions

View File

@ -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<string, string>([['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<string, string>([['locale', 'fr']]);
expect(detectInitialLocale({
storage: {
getItem: (k) => storage.get(k) ?? null,
setItem: () => {}
},
navigatorLanguage: 'en-US',
supported: ['de', 'en']
})).toBe('en');
});
});

View File

@ -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<SupportedLocale> & { bootstrap: () => void } {
const store = writable<SupportedLocale>('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();