feat: save locale in cookies

This commit is contained in:
Henry Li
2026-01-20 16:00:39 +08:00
parent 32a45eb043
commit faba2784e1
7 changed files with 131 additions and 38 deletions

View File

@@ -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 (
<html
lang={locale}
className={geist.variable}
suppressContentEditableWarning
suppressHydrationWarning
@@ -32,7 +36,7 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
{children}
<I18nProvider initialLocale={locale}>{children}</I18nProvider>
</ThemeProvider>
</body>
</html>

View File

@@ -1,3 +1,5 @@
"use client";
import { SettingsIcon } from "lucide-react";
import {

View 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;
}

View 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;
}
}

View File

@@ -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<Locale, Translations> = {
};
export function useI18n() {
const [locale, setLocale] = useState<Locale>(() => {
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,

View File

@@ -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";
}

View 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;
}