diff --git a/apps/frontend/src/app/api/locale/route.ts b/apps/frontend/src/app/api/locale/route.ts
new file mode 100644
index 0000000..dfde8b7
--- /dev/null
+++ b/apps/frontend/src/app/api/locale/route.ts
@@ -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);
+ }
+}
diff --git a/apps/frontend/src/app/api/translations/[locale]/route.ts b/apps/frontend/src/app/api/translations/[locale]/route.ts
new file mode 100644
index 0000000..9b27908
--- /dev/null
+++ b/apps/frontend/src/app/api/translations/[locale]/route.ts
@@ -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 },
+ );
+ }
+}
diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx
index 89363bc..4fbee76 100644
--- a/apps/frontend/src/app/layout.tsx
+++ b/apps/frontend/src/app/layout.tsx
@@ -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({
- {setupComplete ? (
-
+
+ {setupComplete ? (
+
{children}
-
- ) : (
-
- )}
+
+ ) : (
+
+ )}
+
diff --git a/apps/frontend/src/components/EmptyChat.tsx b/apps/frontend/src/components/EmptyChat.tsx
index f4c24a9..b4ab7f4 100644
--- a/apps/frontend/src/components/EmptyChat.tsx
+++ b/apps/frontend/src/components/EmptyChat.tsx
@@ -1,10 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
-import { Settings } from 'lucide-react';
import EmptyChatMessageInput from './EmptyChatMessageInput';
-import { File } from './ChatWindow';
-import Link from 'next/link';
import WeatherWidget from './WeatherWidget';
import NewsArticleWidget from './NewsArticleWidget';
import SettingsButtonMobile from '@/components/Settings/SettingsButtonMobile';
@@ -12,8 +9,10 @@ import {
getShowNewsWidget,
getShowWeatherWidget,
} from '@/lib/config/clientRegistry';
+import { useTranslation } from '@/lib/localization/context';
const EmptyChat = () => {
+ const { t } = useTranslation();
const [showWeather, setShowWeather] = useState(() =>
typeof window !== 'undefined' ? getShowWeatherWidget() : true,
);
@@ -64,7 +63,7 @@ const EmptyChat = () => {
- Research begins here.
+ {t('empty.subtitle')}
diff --git a/apps/frontend/src/components/EmptyChatMessageInput.tsx b/apps/frontend/src/components/EmptyChatMessageInput.tsx
index 206f524..5b35dc8 100644
--- a/apps/frontend/src/components/EmptyChatMessageInput.tsx
+++ b/apps/frontend/src/components/EmptyChatMessageInput.tsx
@@ -5,10 +5,12 @@ import Sources from './MessageInputActions/Sources';
import Optimization from './MessageInputActions/Optimization';
import Attach from './MessageInputActions/Attach';
import { useChat } from '@/lib/hooks/useChat';
+import { useTranslation } from '@/lib/localization/context';
import ModelSelector from './MessageInputActions/ChatModelSelector';
const EmptyChatMessageInput = () => {
const { sendMessage } = useChat();
+ const { t } = useTranslation();
/* const [copilotEnabled, setCopilotEnabled] = useState(false); */
const [message, setMessage] = useState('');
@@ -62,7 +64,7 @@ const EmptyChatMessageInput = () => {
onChange={(e) => setMessage(e.target.value)}
minRows={2}
className="px-2 bg-transparent placeholder:text-[15px] placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
- placeholder="Ask anything..."
+ placeholder={t('input.askAnything')}
/>
diff --git a/apps/frontend/src/components/MessageInput.tsx b/apps/frontend/src/components/MessageInput.tsx
index 281dde2..686c37a 100644
--- a/apps/frontend/src/components/MessageInput.tsx
+++ b/apps/frontend/src/components/MessageInput.tsx
@@ -4,9 +4,11 @@ import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import AttachSmall from './MessageInputActions/AttachSmall';
import { useChat } from '@/lib/hooks/useChat';
+import { useTranslation } from '@/lib/localization/context';
const MessageInput = () => {
const { loading, sendMessage } = useChat();
+ const { t } = useTranslation();
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
@@ -74,7 +76,7 @@ const MessageInput = () => {
setTextareaRows(Math.ceil(height / props.rowHeight));
}}
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
- placeholder="Ask a follow-up"
+ placeholder={t('input.askFollowUp')}
/>
{mode === 'single' && (