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,55 @@
import { NextRequest, NextResponse } from 'next/server';
const LOCALIZATION_SERVICE_URL =
process.env.LOCALIZATION_SERVICE_URL ?? 'http://localhost:4003';
const fallbackLocale = {
locale: 'en',
language: 'en',
region: null,
countryCode: null,
timezone: null,
source: 'fallback' as const,
};
export async function GET(req: NextRequest) {
try {
const res = await fetch(`${LOCALIZATION_SERVICE_URL}/api/locale`, {
headers: {
'x-forwarded-for': req.headers.get('x-forwarded-for') ?? '',
'x-real-ip': req.headers.get('x-real-ip') ?? '',
'user-agent': req.headers.get('user-agent') ?? '',
'accept-language': req.headers.get('accept-language') ?? '',
},
});
if (!res.ok) return NextResponse.json(fallbackLocale);
const data = await res.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(fallbackLocale);
}
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const res = await fetch(`${LOCALIZATION_SERVICE_URL}/api/locale`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-forwarded-for': req.headers.get('x-forwarded-for') ?? '',
'x-real-ip': req.headers.get('x-real-ip') ?? '',
'user-agent': req.headers.get('user-agent') ?? '',
'accept-language': req.headers.get('accept-language') ?? '',
},
body: JSON.stringify(body),
});
if (!res.ok) return NextResponse.json(fallbackLocale);
const data = await res.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(fallbackLocale);
}
}

View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from 'next/server';
const LOCALIZATION_SERVICE_URL =
process.env.LOCALIZATION_SERVICE_URL ?? 'http://localhost:4003';
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ locale: string }> },
) {
try {
const { locale } = await params;
if (!locale) {
return NextResponse.json(
{ error: 'Locale is required' },
{ status: 400 },
);
}
const res = await fetch(
`${LOCALIZATION_SERVICE_URL}/api/translations/${encodeURIComponent(locale)}`,
);
if (!res.ok) {
return NextResponse.json(
{ error: 'Failed to fetch translations' },
{ status: 502 },
);
}
const data = await res.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ error: 'Failed to fetch translations' },
{ status: 500 },
);
}
}

View File

@@ -7,6 +7,7 @@ import { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider';
import { LocalizationProvider } from '@/lib/localization/context';
import configManager from '@/lib/config';
import SetupWizard from '@/components/Setup/SetupWizard';
import { ChatProvider } from '@/lib/hooks/useChat';
@@ -36,8 +37,9 @@ export default function RootLayout({
<html className="h-full" lang="en" suppressHydrationWarning>
<body className={cn('h-full antialiased', roboto.className)}>
<ThemeProvider>
{setupComplete ? (
<ChatProvider>
<LocalizationProvider>
{setupComplete ? (
<ChatProvider>
<Sidebar>{children}</Sidebar>
<Toaster
toastOptions={{
@@ -48,10 +50,11 @@ export default function RootLayout({
},
}}
/>
</ChatProvider>
) : (
<SetupWizard configSections={configSections} />
)}
</ChatProvider>
) : (
<SetupWizard configSections={configSections} />
)}
</LocalizationProvider>
</ThemeProvider>
</body>
</html>