diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 78397c6..578238b 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -4,6 +4,8 @@ import { type Metadata } from "next"; import { Geist } from "next/font/google"; import { ThemeProvider } from "@/components/theme-provider"; +import { I18nProvider } from "@/core/i18n/context"; +import { detectLocaleServer } from "@/core/i18n/server"; export const metadata: Metadata = { title: "Welcome to DeerFlow", @@ -16,11 +18,13 @@ const geist = Geist({ variable: "--font-geist-sans", }); -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + const locale = await detectLocaleServer(); return ( - {children} + {children} diff --git a/frontend/src/components/workspace/workspace-sidebar.tsx b/frontend/src/components/workspace/workspace-sidebar.tsx index ef63a3c..b7240e4 100644 --- a/frontend/src/components/workspace/workspace-sidebar.tsx +++ b/frontend/src/components/workspace/workspace-sidebar.tsx @@ -1,3 +1,5 @@ +"use client"; + import { SettingsIcon } from "lucide-react"; import { diff --git a/frontend/src/core/i18n/context.tsx b/frontend/src/core/i18n/context.tsx new file mode 100644 index 0000000..da325cf --- /dev/null +++ b/frontend/src/core/i18n/context.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { createContext, useContext, useState, type ReactNode } from "react"; + +import type { Locale } from "@/core/i18n"; + +export interface I18nContextType { + locale: Locale; + setLocale: (locale: Locale) => void; +} + +export const I18nContext = createContext(null); + +export function I18nProvider({ + children, + initialLocale, +}: { + children: ReactNode; + initialLocale: Locale; +}) { + const [locale, setLocale] = useState(initialLocale); + + const handleSetLocale = (newLocale: Locale) => { + setLocale(newLocale); + document.cookie = `locale=${newLocale}; path=/; max-age=31536000`; + }; + + return ( + + {children} + + ); +} + +export function useI18nContext() { + const context = useContext(I18nContext); + if (!context) { + throw new Error("useI18n must be used within I18nProvider"); + } + return context; +} diff --git a/frontend/src/core/i18n/cookies.ts b/frontend/src/core/i18n/cookies.ts new file mode 100644 index 0000000..255163d --- /dev/null +++ b/frontend/src/core/i18n/cookies.ts @@ -0,0 +1,52 @@ +/** + * Cookie utilities for locale management + * Works on both client and server side + */ + +const LOCALE_COOKIE_NAME = "locale"; + +/** + * Get locale from cookie (client-side) + */ +export function getLocaleFromCookie(): string | null { + if (typeof document === "undefined") { + return null; + } + + const cookies = document.cookie.split(";"); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split("="); + if (name === LOCALE_COOKIE_NAME) { + return decodeURIComponent(value ?? ""); + } + } + return null; +} + +/** + * Set locale in cookie (client-side) + */ +export function setLocaleInCookie(locale: string): void { + if (typeof document === "undefined") { + return; + } + + // Set cookie with 1 year expiration + const maxAge = 365 * 24 * 60 * 60; // 1 year in seconds + document.cookie = `${LOCALE_COOKIE_NAME}=${encodeURIComponent(locale)}; max-age=${maxAge}; path=/; SameSite=Lax`; +} + +/** + * Get locale from cookie (server-side) + * Use this in server components or API routes + */ +export async function getLocaleFromCookieServer(): Promise { + try { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + return cookieStore.get(LOCALE_COOKIE_NAME)?.value ?? null; + } catch { + // Fallback if cookies() is not available (e.g., in middleware) + return null; + } +} diff --git a/frontend/src/core/i18n/hooks.ts b/frontend/src/core/i18n/hooks.ts index 81296c0..f9e252f 100644 --- a/frontend/src/core/i18n/hooks.ts +++ b/frontend/src/core/i18n/hooks.ts @@ -1,7 +1,9 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useI18nContext } from "./context"; +import { getLocaleFromCookie, setLocaleInCookie } from "./cookies"; import { enUS } from "./locales/en-US"; import { zhCN } from "./locales/zh-CN"; @@ -13,41 +15,24 @@ const translations: Record = { }; export function useI18n() { - const [locale, setLocale] = useState(() => { - if (typeof window === "undefined") { - return "en-US"; - } - - // Try to get from localStorage first - const saved = localStorage.getItem("locale") as Locale | null; - if (saved && (saved === "en-US" || saved === "zh-CN")) { - return saved; - } - - // Otherwise detect from browser - return detectLocale(); - }); + const { locale, setLocale } = useI18nContext(); const t = translations[locale]; const changeLocale = (newLocale: Locale) => { setLocale(newLocale); - if (typeof window !== "undefined") { - localStorage.setItem("locale", newLocale); - } + setLocaleInCookie(newLocale); }; // Initialize locale on mount useEffect(() => { - if (typeof window !== "undefined") { - const saved = localStorage.getItem("locale") as Locale | null; - if (!saved) { - const detected = detectLocale(); - setLocale(detected); - localStorage.setItem("locale", detected); - } + const saved = getLocaleFromCookie() as Locale | null; + if (!saved) { + const detected = detectLocale(); + setLocale(detected); + setLocaleInCookie(detected); } - }, []); + }, [setLocale]); return { locale, diff --git a/frontend/src/core/i18n/index.ts b/frontend/src/core/i18n/index.ts index b347528..90f03ec 100644 --- a/frontend/src/core/i18n/index.ts +++ b/frontend/src/core/i18n/index.ts @@ -6,18 +6,18 @@ export type Locale = "en-US" | "zh-CN"; // Helper function to detect browser locale export function detectLocale(): Locale { - // if (typeof window === "undefined") { - // return "en-US"; - // } + if (typeof window === "undefined") { + return "en-US"; + } - // const browserLang = - // navigator.language || - // (navigator as unknown as { userLanguage: string }).userLanguage; + const browserLang = + navigator.language || + (navigator as unknown as { userLanguage: string }).userLanguage; - // // Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.) - // if (browserLang.toLowerCase().startsWith("zh")) { - // return "zh-CN"; - // } + // Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.) + if (browserLang.toLowerCase().startsWith("zh")) { + return "zh-CN"; + } return "en-US"; } diff --git a/frontend/src/core/i18n/server.ts b/frontend/src/core/i18n/server.ts new file mode 100644 index 0000000..a03efd7 --- /dev/null +++ b/frontend/src/core/i18n/server.ts @@ -0,0 +1,9 @@ +import { cookies } from "next/headers"; + +export type Locale = "en-US" | "zh-CN"; + +export async function detectLocaleServer(): Promise { + const cookieStore = await cookies(); + const locale = cookieStore.get("locale")?.value ?? "en-US"; + return locale as Locale; +}