From 9f968be4653f85189d42b662ddb580ea6fda94ab Mon Sep 17 00:00:00 2001 From: longsizhuo Date: Fri, 17 Apr 2026 17:36:37 +0000 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20Header=20=E5=8A=A0=E5=8C=BF?= =?UTF-8?q?=E5=90=8D=E5=8F=AF=E7=94=A8=E7=9A=84=20ZH/EN=20=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E5=88=87=E6=8D=A2=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前切语言的唯一入口在 /settings 页,UserMenu 里只有登录用户能看到; 匿名访客看到的永远是默认 zh,站点对英语用户非常不友好。 新增 LocaleToggle client component,挂在 Header 右上 ThemeToggle 左边: - 写 locale=zh|en 到 document.cookie(字段 / 格式 / 路径 / 一年有效期 与 SettingsForm 完全一致),登录用户在 /settings 改过的偏好不会被打乱 - 切完 router.refresh() 让 server component(Hero / docs 详情等)重新 按 cookie 渲染当前语言 - 初始 render 给默认值,useEffect 里读 cookie 覆盖,避免 hydration 警告 - 按 "ZH / EN" 样式展示,当前语言加粗 不做的事: - 不搞 URL prefix(/zh/xxx、/en/xxx):现站点没有 i18n 路由分段, 硬改牵动路由 / sitemap / canonical 大量逻辑,留给后续专门做 SEO 双语 URL 的 PR - 不读后端 user_preferences:登录用户在设置页改完也只是写同一条 cookie, 这里读 cookie 已经统一视图 --- app/components/Header.tsx | 2 + app/components/LocaleToggle.tsx | 85 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 app/components/LocaleToggle.tsx diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 64960d2..333f14d 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ThemeToggle } from "./ThemeToggle"; +import { LocaleToggle } from "./LocaleToggle"; import { Button } from "@/components/ui/button"; import { MessageCircle } from "lucide-react"; import { Github as GithubIcon } from "./icons/Github"; @@ -106,6 +107,7 @@ export async function Header() { + diff --git a/app/components/LocaleToggle.tsx b/app/components/LocaleToggle.tsx new file mode 100644 index 0000000..dcc7475 --- /dev/null +++ b/app/components/LocaleToggle.tsx @@ -0,0 +1,85 @@ +"use client"; + +/** + * Header 里的语言切换按钮(匿名也能用)。 + * + * 为什么要做: + * 之前切语言的唯一入口在 /settings 页面,UserMenu 里只有登录用户能看到。 + * 访客看到的永远是默认 zh,站点对英语用户非常不友好。 + * + * 实现: + * - 写 locale=zh|en 到 document.cookie(path=/,一年有效期,samesite=lax) + * 字段和格式与 SettingsForm 完全一致,登录用户在设置页改的偏好仍然生效 + * - 切完 router.refresh() 让 SSR 重新渲染,server component(Hero / docs + * 详情页等)从 cookie 读新 locale 切文案 + * - 简单的 ZH / EN 双字母展示,当前语言高亮;button 尺寸与 ThemeToggle 对齐 + * + * 不做的事: + * - 不读后端 user_preferences:游客无账号,登录用户在 /settings 改完也只是 + * 写同一条 cookie,这里读 cookie 即可统一视图。 + * - 不做 URL prefix 切换(/zh/xxx、/en/xxx):站点当前没有 i18n 路由分段, + * 硬改会牵动大量路由和 sitemap。Cookie 方案延续现状,后续若要 SEO 双语 + * URL 再另开 PR。 + */ + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "../../components/ui/button"; + +type Locale = "zh" | "en"; + +function readLocaleCookie(): Locale { + if (typeof document === "undefined") return "zh"; + const m = document.cookie.match(/(?:^|;\s*)locale=([^;]+)/); + const v = m?.[1]; + return v === "en" ? "en" : "zh"; +} + +function writeLocaleCookie(next: Locale) { + // 一年;samesite=lax 够用(这个 cookie 不涉及跨站 POST) + document.cookie = `locale=${next};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`; +} + +export function LocaleToggle() { + const router = useRouter(); + // 初始 render 先给默认值,避免 SSR 和 CSR 结构不同触发 hydration 警告; + // 真实值由 useEffect 读 cookie 后再覆盖 + const [locale, setLocale] = useState("zh"); + const [ready, setReady] = useState(false); + + useEffect(() => { + setLocale(readLocaleCookie()); + setReady(true); + }, []); + + const toggle = () => { + const next: Locale = locale === "zh" ? "en" : "zh"; + writeLocaleCookie(next); + setLocale(next); + if (typeof window !== "undefined" && window.umami) { + window.umami.track("locale_toggle", { locale: next }); + } + // 刷新 server component 树,重新按 cookie 渲染各页面 + router.refresh(); + }; + + return ( + + ); +}