125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
import React from 'react';
|
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, type LocaleCode, isLocaleCode } from './messages';
|
|
|
|
export interface LocaleContextValue {
|
|
locale: LocaleCode;
|
|
setLocale: (next: LocaleCode) => void;
|
|
resetLocale: () => void;
|
|
hydrated: boolean;
|
|
defaultLocale: LocaleCode;
|
|
storageKey: string;
|
|
availableLocales: typeof SUPPORTED_LOCALES;
|
|
}
|
|
|
|
const LocaleContext = React.createContext<LocaleContextValue | undefined>(undefined);
|
|
|
|
function sanitizeLocale(value: string | null | undefined, fallback: LocaleCode = DEFAULT_LOCALE): LocaleCode {
|
|
if (value && isLocaleCode(value)) {
|
|
return value;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export interface LocaleProviderProps {
|
|
children: React.ReactNode;
|
|
defaultLocale?: LocaleCode;
|
|
storageKey?: string;
|
|
}
|
|
|
|
export function LocaleProvider({
|
|
children,
|
|
defaultLocale = DEFAULT_LOCALE,
|
|
storageKey = 'guestLocale_global',
|
|
}: LocaleProviderProps) {
|
|
const resolvedDefault = sanitizeLocale(defaultLocale, DEFAULT_LOCALE);
|
|
const [locale, setLocaleState] = React.useState<LocaleCode>(resolvedDefault);
|
|
const [userLocale, setUserLocale] = React.useState<LocaleCode | null>(null);
|
|
const [hydrated, setHydrated] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
setHydrated(false);
|
|
|
|
if (typeof window === 'undefined') {
|
|
setLocaleState(resolvedDefault);
|
|
setUserLocale(null);
|
|
setHydrated(true);
|
|
return;
|
|
}
|
|
|
|
let stored: string | null = null;
|
|
try {
|
|
stored = window.localStorage.getItem(storageKey);
|
|
} catch (error) {
|
|
console.warn('Failed to read stored locale', error);
|
|
}
|
|
|
|
const nextLocale = sanitizeLocale(stored, resolvedDefault);
|
|
setLocaleState(nextLocale);
|
|
setUserLocale(isLocaleCode(stored) ? stored : null);
|
|
setHydrated(true);
|
|
}, [storageKey, resolvedDefault]);
|
|
|
|
React.useEffect(() => {
|
|
if (!hydrated || userLocale !== null) {
|
|
return;
|
|
}
|
|
setLocaleState(resolvedDefault);
|
|
}, [hydrated, userLocale, resolvedDefault]);
|
|
|
|
const setLocale = React.useCallback(
|
|
(next: LocaleCode) => {
|
|
const safeLocale = sanitizeLocale(next, resolvedDefault);
|
|
setLocaleState(safeLocale);
|
|
setUserLocale(safeLocale);
|
|
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
window.localStorage.setItem(storageKey, safeLocale);
|
|
} catch (error) {
|
|
console.warn('Failed to persist locale', error);
|
|
}
|
|
}
|
|
},
|
|
[storageKey, resolvedDefault],
|
|
);
|
|
|
|
const resetLocale = React.useCallback(() => {
|
|
setUserLocale(null);
|
|
setLocaleState(resolvedDefault);
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
window.localStorage.removeItem(storageKey);
|
|
} catch (error) {
|
|
console.warn('Failed to clear stored locale', error);
|
|
}
|
|
}
|
|
}, [resolvedDefault, storageKey]);
|
|
|
|
const value = React.useMemo<LocaleContextValue>(
|
|
() => ({
|
|
locale,
|
|
setLocale,
|
|
resetLocale,
|
|
hydrated,
|
|
defaultLocale: resolvedDefault,
|
|
storageKey,
|
|
availableLocales: SUPPORTED_LOCALES,
|
|
}),
|
|
[locale, setLocale, resetLocale, hydrated, resolvedDefault, storageKey],
|
|
);
|
|
|
|
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
|
|
}
|
|
|
|
export function useLocale(): LocaleContextValue {
|
|
const ctx = React.useContext(LocaleContext);
|
|
if (!ctx) {
|
|
throw new Error('useLocale must be used within a LocaleProvider');
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export function useOptionalLocale(): LocaleContextValue | undefined {
|
|
return React.useContext(LocaleContext);
|
|
}
|