feat: localization service and frontend integration

- 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>
This commit is contained in:
home
2026-02-20 21:45:44 +03:00
parent 4269c8d99b
commit f4d945a2b5
21 changed files with 878 additions and 107 deletions

View File

@@ -0,0 +1,133 @@
'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;
}