refactor(guest): retire legacy guest app and move shared modules
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-06 08:42:53 +01:00
parent b14435df8b
commit 0a08f2704f
191 changed files with 243 additions and 12631 deletions

View File

@@ -0,0 +1,124 @@
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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { translate, DEFAULT_LOCALE, type LocaleCode } from './messages';
import { useLocale } from './LocaleContext';
type ReplacementValues = Record<string, string | number>;
export type TranslateFn = {
(key: string): string;
(key: string, fallback: string): string;
(key: string, replacements: ReplacementValues): string;
(key: string, replacements: ReplacementValues, fallback: string): string;
};
function resolveTranslation(locale: LocaleCode, key: string, fallback?: string): string {
return translate(locale, key) ?? translate(DEFAULT_LOCALE, key) ?? fallback ?? key;
}
function applyReplacements(value: string, replacements?: ReplacementValues): string {
if (!replacements) {
return value;
}
return Object.entries(replacements).reduce((acc, [token, replacement]) => {
const pattern = new RegExp(`\\{${token}\\}`, 'g');
return acc.replace(pattern, String(replacement));
}, value);
}
export function useTranslation() {
const { locale } = useLocale();
const t = React.useCallback<TranslateFn>((key: string, arg2?: ReplacementValues | string, arg3?: string) => {
let replacements: ReplacementValues | undefined;
let fallback: string | undefined;
if (typeof arg2 === 'string' || arg2 === undefined) {
fallback = arg2 ?? arg3;
} else {
replacements = arg2;
fallback = arg3;
}
const raw = resolveTranslation(locale, key, fallback);
return applyReplacements(raw, replacements);
}, [locale]);
return React.useMemo(() => ({ t, locale }), [t, locale]);
}