118 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|