新增 SPA 支持

This commit is contained in:
2026-09-25 06:33:03 +08:00
parent d3bbe95aa5
commit 6f1dbce1a6
3 changed files with 31 additions and 16 deletions
+13 -6
View File
@@ -3,6 +3,7 @@
import { useRouteLocale } from "@/components/layout/locale-context"; import { useRouteLocale } from "@/components/layout/locale-context";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { useEffect, useState, useSyncExternalStore } from "react"; import { useEffect, useState, useSyncExternalStore } from "react";
import { applyLocale, getInitialLocale, withLocale, getLocaleMessages, type Locale } from "@/lib/i18n"; import { applyLocale, getInitialLocale, withLocale, getLocaleMessages, type Locale } from "@/lib/i18n";
import { ThemeToggle } from "@/components/layout/theme-toggle"; import { ThemeToggle } from "@/components/layout/theme-toggle";
@@ -81,9 +82,12 @@ export function SiteFooter() {
<div key={`column-${groupIndex}`} className="site-footer-column"> <div key={`column-${groupIndex}`} className="site-footer-column">
<h3>{group.title}</h3> <h3>{group.title}</h3>
<ul> <ul>
{group.links.map((link, linkIndex) => ( {group.links.map((link, linkIndex) => {
<li key={`column-${groupIndex}-link-${linkIndex}`}><a href={withLocale(footerColumnHrefs[groupIndex][linkIndex], locale)}>{link}</a></li> const href = withLocale(footerColumnHrefs[groupIndex][linkIndex], locale);
))} return <li key={`column-${groupIndex}-link-${linkIndex}`}>
{href.startsWith("/") ? <Link href={href}>{link}</Link> : <a href={href}>{link}</a>}
</li>;
})}
</ul> </ul>
</div> </div>
))} ))}
@@ -104,9 +108,12 @@ export function SiteFooter() {
<div className="site-footer-meta"> <div className="site-footer-meta">
<div className="site-footer-legal-row"> <div className="site-footer-legal-row">
{messages.footer.legal.map((item, itemIndex) => ( {messages.footer.legal.map((item, itemIndex) => {
<a key={`legal-${itemIndex}`} href={withLocale(footerLegalHrefs[itemIndex], locale)}>{item}</a> const href = withLocale(footerLegalHrefs[itemIndex], locale);
))} return href.startsWith("/")
? <Link key={`legal-${itemIndex}`} href={href}>{item}</Link>
: <a key={`legal-${itemIndex}`} href={href}>{item}</a>;
})}
</div> </div>
<p className="site-footer-description">{messages.footer.description}</p> <p className="site-footer-description">{messages.footer.description}</p>
<div className="site-footer-copyright-row"> <div className="site-footer-copyright-row">
+15 -9
View File
@@ -4,12 +4,23 @@ import { useRouteLocale } from "@/components/layout/locale-context";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { applyLocale, getInitialLocale, withLocale, withoutLocale, getLocaleMessages, localeLabels, locales, localeShortLabels, type Locale } from "@/lib/i18n"; import { applyLocale, getInitialLocale, withLocale, withoutLocale, getLocaleMessages, localeLabels, locales, localeShortLabels, type Locale } from "@/lib/i18n";
export function SiteHeader() { export function SiteHeader() {
const [locale, setLocale] = useState<Locale>(useRouteLocale()); const locale = useRouteLocale();
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const router = useRouter();
const changeLocale = (nextLocale: Locale) => {
try {
window.localStorage.setItem("kr-locale", nextLocale);
} catch { /* The URL remains the source of truth when storage is unavailable. */ }
const href = `${window.location.pathname}${window.location.search}${window.location.hash}`;
router.replace(withLocale(href, nextLocale));
};
useEffect(() => { useEffect(() => {
const updateDocumentTitle = (nextLocale: Locale) => { const updateDocumentTitle = (nextLocale: Locale) => {
@@ -25,10 +36,7 @@ export function SiteHeader() {
document.title = pageLabel ? `${pageLabel} | ${messages.common.siteTitle}` : messages.common.siteTitle; document.title = pageLabel ? `${pageLabel} | ${messages.common.siteTitle}` : messages.common.siteTitle;
}; };
const syncLocale = (nextLocale: Locale) => { const syncLocale = (nextLocale: Locale) => updateDocumentTitle(nextLocale);
setLocale(nextLocale);
updateDocumentTitle(nextLocale);
};
const handleLocaleChange = (event: Event) => { const handleLocaleChange = (event: Event) => {
const nextLocale = (event as CustomEvent<{ locale?: Locale }>).detail?.locale ?? getInitialLocale(); const nextLocale = (event as CustomEvent<{ locale?: Locale }>).detail?.locale ?? getInitialLocale();
@@ -83,8 +91,7 @@ export function SiteHeader() {
className={item === locale ? "active" : ""} className={item === locale ? "active" : ""}
aria-pressed={item === locale} aria-pressed={item === locale}
onClick={() => { onClick={() => {
setLocale(item); changeLocale(item);
applyLocale(item);
}} }}
aria-label={localeLabels[item]} aria-label={localeLabels[item]}
title={localeLabels[item]} title={localeLabels[item]}
@@ -103,8 +110,7 @@ export function SiteHeader() {
className={item === locale ? "active" : ""} className={item === locale ? "active" : ""}
aria-pressed={item === locale} aria-pressed={item === locale}
onClick={() => { onClick={() => {
setLocale(item); changeLocale(item);
applyLocale(item);
}} }}
aria-label={localeLabels[item]} aria-label={localeLabels[item]}
title={localeLabels[item]} title={localeLabels[item]}
+3 -1
View File
@@ -3,6 +3,7 @@
import { useRouteLocale } from "@/components/layout/locale-context"; import { useRouteLocale } from "@/components/layout/locale-context";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { getInitialLocale, withLocale, type Locale } from "@/lib/i18n"; import { getInitialLocale, withLocale, type Locale } from "@/lib/i18n";
@@ -72,6 +73,7 @@ function parseLocalizedMessages(source: string): Partial<Record<Locale, string[]
export function HttpStatusPage({ code }: { code: HttpStatusCode }) { export function HttpStatusPage({ code }: { code: HttpStatusCode }) {
const [locale, setLocale] = useState<Locale>(useRouteLocale()); const [locale, setLocale] = useState<Locale>(useRouteLocale());
const [randomMessages, setRandomMessages] = useState<Partial<Record<Locale, string[]>>>({}); const [randomMessages, setRandomMessages] = useState<Partial<Record<Locale, string[]>>>({});
const router = useRouter();
useEffect(() => { useEffect(() => {
const syncLocale = () => setLocale(getInitialLocale()); const syncLocale = () => setLocale(getInitialLocale());
@@ -105,7 +107,7 @@ export function HttpStatusPage({ code }: { code: HttpStatusCode }) {
window.history.back(); window.history.back();
return; return;
} }
window.location.assign(withLocale("/", locale)); router.replace(withLocale("/", locale));
}; };
return ( return (