- Add localization-service microservice (locale resolution, translations) - Add frontend API routes /api/locale and /api/translations/[locale] - Add LocalizationProvider and localization context - Integrate localization into layout, EmptyChat, MessageInput components - Update MICROSERVICES.md architecture docs - Add localization-service to workspaces Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
2.9 KiB
TypeScript
134 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import {
|
|
fetchLocaleWithClient,
|
|
fetchTranslations,
|
|
type LocalizationContext as LocaleContext,
|
|
} from '@/lib/localization';
|
|
|
|
type Translations = Record<string, string>;
|
|
|
|
interface LocalizationState {
|
|
locale: LocaleContext | null;
|
|
translations: Translations | null;
|
|
loading: boolean;
|
|
error: boolean;
|
|
}
|
|
|
|
interface LocalizationContextValue extends LocalizationState {
|
|
t: (key: string) => string;
|
|
localeCode: string;
|
|
}
|
|
|
|
const defaultTranslations: Translations = {
|
|
'app.title': 'GooSeek',
|
|
'app.searchPlaceholder': 'Search...',
|
|
'empty.subtitle': 'Research begins here.',
|
|
'input.askAnything': 'Ask anything...',
|
|
'input.askFollowUp': 'Ask a follow-up',
|
|
'chat.send': 'Send',
|
|
'chat.newChat': 'New Chat',
|
|
'chat.suggestions': 'Suggestions',
|
|
'common.loading': 'Loading...',
|
|
'common.error': 'Error',
|
|
'common.retry': 'Retry',
|
|
};
|
|
|
|
const LocalizationContext = createContext<LocalizationContextValue | null>(null);
|
|
|
|
export function LocalizationProvider({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const [state, setState] = useState<LocalizationState>({
|
|
locale: null,
|
|
translations: null,
|
|
loading: true,
|
|
error: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function load() {
|
|
try {
|
|
const loc = await fetchLocaleWithClient();
|
|
if (cancelled) return;
|
|
const trans = await fetchTranslations(loc.locale);
|
|
if (cancelled) return;
|
|
setState({
|
|
locale: loc,
|
|
translations: trans,
|
|
loading: false,
|
|
error: false,
|
|
});
|
|
if (typeof document !== 'undefined') {
|
|
document.documentElement.lang = loc.locale;
|
|
}
|
|
} catch {
|
|
if (cancelled) return;
|
|
setState({
|
|
locale: null,
|
|
translations: null,
|
|
loading: false,
|
|
error: true,
|
|
});
|
|
}
|
|
}
|
|
load();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const t = useCallback(
|
|
(key: string): string => {
|
|
if (state.translations && key in state.translations) {
|
|
return state.translations[key] ?? key;
|
|
}
|
|
return defaultTranslations[key] ?? key;
|
|
},
|
|
[state.translations],
|
|
);
|
|
|
|
const localeCode = state.locale?.locale ?? 'en';
|
|
|
|
const value = useMemo<LocalizationContextValue>(
|
|
() => ({
|
|
...state,
|
|
t,
|
|
localeCode,
|
|
}),
|
|
[state, t, localeCode],
|
|
);
|
|
|
|
return (
|
|
<LocalizationContext.Provider value={value}>
|
|
{children}
|
|
</LocalizationContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTranslation(): LocalizationContextValue {
|
|
const ctx = useContext(LocalizationContext);
|
|
if (!ctx) {
|
|
return {
|
|
locale: null,
|
|
translations: null,
|
|
loading: false,
|
|
error: true,
|
|
t: (key: string) => defaultTranslations[key] ?? key,
|
|
localeCode: 'en',
|
|
};
|
|
}
|
|
return ctx;
|
|
}
|