feat(i18n): add zh/en locale switching
This commit is contained in:
@@ -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