feat(app): activeLocale-store mit persistence + initial-detection
This commit is contained in:
parent
f799223836
commit
8f513495e3
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
Loading…
Reference in New Issue