Files
2026-10-08 11:09:37 +08:00

118 lines
3.1 KiB
TypeScript

import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import {
getMessages,
getTranslations,
setRequestLocale,
} from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import { SiteHeader } from "@/components/layout/site-header";
import { SiteFooter } from "@/components/layout/site-footer";
import { siteConfig } from "@/lib/site-config";
/**
* 语言段布局。
*
* 文档骨架(<html> / <body>)由根布局 app/layout.tsx 提供,
* 这里只负责:校验 locale、注入 i18n Provider、渲染站点壳(页头 / 页脚)。
*
* 注意:因为根布局固定了 <html lang="zh-CN">,实际请求语言通过
* `app/[locale]/layout.tsx` 无法直接改写 <html>;如需精确的 lang 属性,
* 参见 README 的「已知限制」一节。
*/
/** 预生成所有语言的路由 */
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
type Locale = (typeof routing.locales)[number];
type LocaleLayoutProps = {
children: React.ReactNode;
params: Promise<{ locale: string }>;
};
/** 校验 locale 合法性,非法则交给 404 */
function assertLocale(locale: string): asserts locale is Locale {
if (!routing.locales.includes(locale as Locale)) {
notFound();
}
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
assertLocale(locale);
const t = await getTranslations({ locale, namespace: "Site" });
const title = `${siteConfig.name} · ${t("tagline")}`;
return {
title: {
default: title,
template: `%s · ${siteConfig.name}`,
},
description: siteConfig.description,
alternates: {
canonical: `/${locale}`,
languages: Object.fromEntries(
routing.locales.map((item) => [item, `/${item}`]),
),
},
openGraph: {
type: "website",
siteName: siteConfig.name,
locale,
url: `${siteConfig.url}/${locale}`,
title,
description: siteConfig.description,
images: [{ url: siteConfig.ogImage }],
},
twitter: {
card: "summary_large_image",
title,
description: siteConfig.description,
images: [siteConfig.ogImage],
},
};
}
export default async function LocaleLayout({
children,
params,
}: LocaleLayoutProps) {
const { locale } = await params;
assertLocale(locale);
// 启用静态渲染(next-intl 要求显式声明当前请求的 locale)
setRequestLocale(locale);
const messages = await getMessages();
const t = await getTranslations({ locale, namespace: "Nav" });
return (
<NextIntlClientProvider messages={messages}>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:left-md focus:top-md focus:z-[60] focus:rounded-full focus:bg-primary focus:px-lg focus:py-xs focus:text-body-sm focus:text-on-primary"
>
{t("skipToContent")}
</a>
<div className="flex min-h-dvh flex-col">
<SiteHeader />
<main id="main-content" className="flex-1">
{children}
</main>
<SiteFooter />
</div>
</NextIntlClientProvider>
);
}