mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-28 08:14:48 +08:00
feat: save locale in cookies
This commit is contained in:
@@ -4,6 +4,8 @@ import { type Metadata } from "next";
|
|||||||
import { Geist } from "next/font/google";
|
import { Geist } from "next/font/google";
|
||||||
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { I18nProvider } from "@/core/i18n/context";
|
||||||
|
import { detectLocaleServer } from "@/core/i18n/server";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Welcome to DeerFlow",
|
title: "Welcome to DeerFlow",
|
||||||
@@ -16,11 +18,13 @@ const geist = Geist({
|
|||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
const locale = await detectLocaleServer();
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
|
lang={locale}
|
||||||
className={geist.variable}
|
className={geist.variable}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
@@ -32,7 +36,7 @@ export default function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{children}
|
<I18nProvider initialLocale={locale}>{children}</I18nProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { SettingsIcon } from "lucide-react";
|
import { SettingsIcon } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
41
frontend/src/core/i18n/context.tsx
Normal file
41
frontend/src/core/i18n/context.tsx
Normal file
@@ -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<I18nContextType | null>(null);
|
||||||
|
|
||||||
|
export function I18nProvider({
|
||||||
|
children,
|
||||||
|
initialLocale,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
initialLocale: Locale;
|
||||||
|
}) {
|
||||||
|
const [locale, setLocale] = useState<Locale>(initialLocale);
|
||||||
|
|
||||||
|
const handleSetLocale = (newLocale: Locale) => {
|
||||||
|
setLocale(newLocale);
|
||||||
|
document.cookie = `locale=${newLocale}; path=/; max-age=31536000`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<I18nContext.Provider value={{ locale, setLocale: handleSetLocale }}>
|
||||||
|
{children}
|
||||||
|
</I18nContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useI18nContext() {
|
||||||
|
const context = useContext(I18nContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useI18n must be used within I18nProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
52
frontend/src/core/i18n/cookies.ts
Normal file
52
frontend/src/core/i18n/cookies.ts
Normal file
@@ -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<string | null> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"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 { enUS } from "./locales/en-US";
|
||||||
import { zhCN } from "./locales/zh-CN";
|
import { zhCN } from "./locales/zh-CN";
|
||||||
|
|
||||||
@@ -13,41 +15,24 @@ const translations: Record<Locale, Translations> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useI18n() {
|
export function useI18n() {
|
||||||
const [locale, setLocale] = useState<Locale>(() => {
|
const { locale, setLocale } = useI18nContext();
|
||||||
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 t = translations[locale];
|
const t = translations[locale];
|
||||||
|
|
||||||
const changeLocale = (newLocale: Locale) => {
|
const changeLocale = (newLocale: Locale) => {
|
||||||
setLocale(newLocale);
|
setLocale(newLocale);
|
||||||
if (typeof window !== "undefined") {
|
setLocaleInCookie(newLocale);
|
||||||
localStorage.setItem("locale", newLocale);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize locale on mount
|
// Initialize locale on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
const saved = getLocaleFromCookie() as Locale | null;
|
||||||
const saved = localStorage.getItem("locale") as Locale | null;
|
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
const detected = detectLocale();
|
const detected = detectLocale();
|
||||||
setLocale(detected);
|
setLocale(detected);
|
||||||
localStorage.setItem("locale", detected);
|
setLocaleInCookie(detected);
|
||||||
}
|
}
|
||||||
}
|
}, [setLocale]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ export type Locale = "en-US" | "zh-CN";
|
|||||||
|
|
||||||
// Helper function to detect browser locale
|
// Helper function to detect browser locale
|
||||||
export function detectLocale(): Locale {
|
export function detectLocale(): Locale {
|
||||||
// if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
// return "en-US";
|
return "en-US";
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const browserLang =
|
const browserLang =
|
||||||
// navigator.language ||
|
navigator.language ||
|
||||||
// (navigator as unknown as { userLanguage: string }).userLanguage;
|
(navigator as unknown as { userLanguage: string }).userLanguage;
|
||||||
|
|
||||||
// // Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.)
|
// Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.)
|
||||||
// if (browserLang.toLowerCase().startsWith("zh")) {
|
if (browserLang.toLowerCase().startsWith("zh")) {
|
||||||
// return "zh-CN";
|
return "zh-CN";
|
||||||
// }
|
}
|
||||||
|
|
||||||
return "en-US";
|
return "en-US";
|
||||||
}
|
}
|
||||||
|
|||||||
9
frontend/src/core/i18n/server.ts
Normal file
9
frontend/src/core/i18n/server.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export type Locale = "en-US" | "zh-CN";
|
||||||
|
|
||||||
|
export async function detectLocaleServer(): Promise<Locale> {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const locale = cookieStore.get("locale")?.value ?? "en-US";
|
||||||
|
return locale as Locale;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user