This commit is contained in:
1 parent
c1541b80df
commit
b4c7908029
97 files changed
+12403
-154
No files matched your search
@@ -0,0 +1,117 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user