feat(i18n): add zh/en locale switching

This commit is contained in:
Leon-in
2026-04-29 10:36:24 +08:00
parent 437dc976fb
commit 3213f00b7b
11 changed files with 915 additions and 33 deletions
+10 -6
View File
@@ -2,10 +2,14 @@
import Image from "next/image"
import Link from "next/link"
import { useTranslations } from "next-intl"
import NewsletterForm from "@/components/NewsletterForm"
import { CONTACT_CHANNELS, FOOTER_GROUPS, SITE_BRAND, SITE_DESCRIPTION } from "@/lib/site-content"
import { CONTACT_CHANNELS, FOOTER_GROUPS } from "@/lib/site-content"
export default function Footer() {
const t = useTranslations("footer")
const site = useTranslations("site")
return (
<footer className="footer-wrapper">
<div data-w-id="f1ff1ac2-5ccd-56f8-612a-0570791caa19" className="footer-main-section">
@@ -18,7 +22,7 @@ export default function Footer() {
</div>
<Link href="/contact" className="footer-button w-inline-block">
<div className="button-content footer">
<div></div>
<div>{t("applyTrial")}</div>
</div>
<div className="button-icon-wrapper footer primary">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 17 17" fill="none" className="squared-icon">
@@ -43,9 +47,9 @@ export default function Footer() {
<div className="w-layout-blockcontainer container-default w-container">
<div className="w-layout-grid footer-middle-content">
<div className="inner-container _355px _100-tablet">
<div className="display-6 medium text-titles-dm">{SITE_BRAND}</div>
<div className="display-6 medium text-titles-dm">{site("brand")}</div>
<div className="mg-top-8-px">
<p className="text-color-neutral-400 mg-bottom-20px">{SITE_DESCRIPTION}</p>
<p className="text-color-neutral-400 mg-bottom-20px">{site("description")}</p>
</div>
<NewsletterForm variant="dark" />
</div>
@@ -113,10 +117,10 @@ export default function Footer() {
</path>
</svg>
</div>
<div></div>
<div>{t("learnPositioning")}</div>
</Link>
<p className="text-color-neutral-500">
Copyright © 2026 DAL Code by DeepAILab. Next.js
{t("copyright")}
</p>
</div>
</div>
+10 -8
View File
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"
import Image from "next/image"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useTranslations } from "next-intl"
import ThemeToggle from "@/components/ThemeToggle"
import { NAV_GROUPS, NAV_LINKS } from "@/lib/site-content"
@@ -24,6 +25,7 @@ function DropdownIcon() {
}
export default function Header() {
const t = useTranslations("nav")
const [mobileOpen, setMobileOpen] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
@@ -82,7 +84,7 @@ export default function Header() {
<ChevronIcon />
</div>
</div>
<div className="link-text">{label}</div>
<div className="link-text">{label === "首页" ? t("home") : label === "产品定位" ? t("about") : label === "洞察" ? t("blog") : t("contact")}</div>
</Link>
</li>
))}
@@ -98,7 +100,7 @@ export default function Header() {
aria-expanded={dropdownOpen}
type="button"
>
<div>Explore</div>
<div>{t("explore")}</div>
<div className="dropdown-icon">
<DropdownIcon />
</div>
@@ -109,14 +111,14 @@ export default function Header() {
<div className="w-layout-grid header-dropdown-grid">
{NAV_GROUPS.map((group) => (
<div key={group.title}>
<div className="dropdown-title">{group.title}</div>
<div className="dropdown-title">{group.title === "产品" ? t("groups.product") : t("groups.content")}</div>
<div className="w-layout-grid pages-column">
{group.links.map(({ href, label }) => (
<Link key={href} href={href} className="link w-inline-block" onClick={closeMenus}>
<div className="link-icon-wrapper">
<div className="link-icon"><ChevronIcon /></div>
</div>
<div className="link-text">{label}</div>
<div className="link-text">{label === "首页" ? t("groupLinks.home") : label === "为什么是 DAL Code" ? t("groupLinks.whyDalCode") : label === "申请试用" ? t("groupLinks.applyTrial") : label === "产品洞察" ? t("groupLinks.insights") : t("groupLinks.buildingRoles")}</div>
</Link>
))}
</div>
@@ -131,7 +133,7 @@ export default function Header() {
<li className="link-nav-item mbl-button">
<Link href="/contact" className="primary-button w-inline-block" onClick={closeMenus}>
<div className="button-content">
<div></div>
<div>{t("applyTrial")}</div>
<div className="button-icon-wrapper primary">
<ChevronIcon />
<div className="button-icon-bg" />
@@ -146,7 +148,7 @@ export default function Header() {
<div className="show-light-mode">
<Link href="/contact" className="primary-button w-inline-block" onClick={closeMenus}>
<div className="button-content">
<div></div>
<div>{t("applyTrial")}</div>
<div className="button-icon-wrapper primary">
<ChevronIcon />
<div className="button-icon-bg" />
@@ -158,7 +160,7 @@ export default function Header() {
<div className="show-dark-mode">
<Link href="/contact" className="secondary-button w-inline-block" onClick={closeMenus}>
<div className="button-content">
<div></div>
<div>{t("applyTrial")}</div>
<div className="button-icon-wrapper secondary">
<ChevronIcon />
<div className="button-icon-bg bg-neutral-800" />
@@ -172,7 +174,7 @@ export default function Header() {
<button
className={`hamburger-menu w-nav-button ${mobileOpen ? "w--open" : ""}`}
onClick={toggleMobile}
aria-label="Toggle menu"
aria-label={t("toggleMenu")}
type="button"
>
<div className="hamburger-menu-flex">
+109
View File
@@ -0,0 +1,109 @@
"use client"
import { useState, useRef, useEffect } from "react"
import { useLocale } from "next-intl"
import { useRouter } from "next/navigation"
import { locales } from "@/i18n/config"
const localeLabels: Record<string, string> = {
"zh-CN": "中",
en: "EN",
}
function GlobeIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
)
}
function ChevronDownIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<polyline points="6 9 12 15 18 9" />
</svg>
)
}
export default function LocaleSwitcher() {
const locale = useLocale()
const router = useRouter()
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
setOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
const otherLocale = locales.find((l) => l !== locale)
if (!otherLocale) return null
function setLocale(nextLocale: string) {
document.cookie = `NEXT_LOCALE=${nextLocale}; path=/; max-age=31536000; samesite=lax`
setOpen(false)
router.refresh()
}
return (
<div ref={ref} className="locale-switcher">
<button
className="locale-switcher-trigger"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-haspopup="listbox"
type="button"
>
<GlobeIcon />
<span className="locale-switcher-label">{localeLabels[locale] ?? locale}</span>
<ChevronDownIcon className={open ? "rotate-180" : ""} />
</button>
{open && (
<div className="locale-switcher-dropdown" role="listbox">
{locales.map((l) => (
<button
key={l}
className={`locale-switcher-option ${l === locale ? "active" : ""}`}
role="option"
aria-selected={l === locale}
onClick={() => setLocale(l)}
type="button"
>
{localeLabels[l] ?? l}
</button>
))}
</div>
)}
</div>
)
}