feat(i18n): add zh/en locale switching
This commit is contained in:
+10
-6
@@ -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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user