返回 CodeWhale
layout.tsx
根目录 / web / app / [locale] / layout.tsx
1 import type { Metadata } from "next";
2 import localFont from "next/font/local";
3 import { IBM_Plex_Mono, Newsreader } from "next/font/google";
4 import { Nav } from "@/components/nav";
5 import { Footer } from "@/components/footer";
6 import { UsageCounting } from "@/components/usage-counting";
7 import { BUILD_FACTS } from "@/lib/facts";
8 import { localeDirection, locales, type Locale } from "@/lib/i18n/config";
9 import { getChrome, getHome } from "@/lib/i18n/dictionaries";
10 import { serializeJsonLd } from "@/lib/json-ld";
11 import { buildPageMetadata } from "@/lib/page-meta";
12 import { buildSiteJsonLd } from "@/lib/site-schema";
13 import "../globals.css";
14
15 // Shannon Sans 0.110 supplies body and small-heading roles through one asset.
16 // Its OFL notice lives beside it; Newsreader and IBM Plex Mono keep their roles.
17 const sans = localFont({
18 src: "../../public/brand/fonts/ShannonSans-Variable.woff2",
19 weight: "100 900",
20 style: "normal",
21 variable: "--font-shannon-sans",
22 display: "swap",
23 });
24
25 // IBM Plex Mono is the GPUI app's code face; it fills the same role here.
26 const mono = IBM_Plex_Mono({
27 subsets: ["latin", "latin-ext", "cyrillic"],
28 weight: ["400", "500", "600"],
29 variable: "--font-mono",
30 display: "swap",
31 });
32
33 // Newsreader's optical-size axis is what lets the same face set a 5rem title
34 // and a 1.3rem running head without looking like two fonts.
35 const serif = Newsreader({
36 subsets: ["latin", "latin-ext"],
37 weight: ["400", "500"],
38 style: ["normal", "italic"],
39 variable: "--font-serif",
40 display: "swap",
41 });
42
43 export function generateStaticParams() {
44 return locales.map((locale) => ({ locale }));
45 }
46
47 export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
48 const { locale } = await params;
49 const home = getHome(locale);
50 return buildPageMetadata({
51 path: "/",
52 locale,
53 title: home.metaTitle,
54 description: home.metaDescription,
55 });
56 }
57
58 export default async function LocaleLayout({
59 children,
60 params,
61 }: {
62 children: React.ReactNode;
63 params: Promise<{ locale: string }>;
64 }) {
65 const { locale } = await params;
66 const chrome = getChrome(locale);
67 // RTL locales (e.g. ar) set the document direction from the canonical
68 // registry so the browser handles bidirectional layout from the root.
69 const dir = localeDirection(locale);
70 const siteJsonLd = buildSiteJsonLd(locale);
71
72 return (
73 <html
74 lang={locale}
75 dir={dir}
76 className={`${sans.variable} ${mono.variable} ${serif.variable}`}
77 suppressHydrationWarning
78 >
79 <body>
80 <script
81 type="application/ld+json"
82 dangerouslySetInnerHTML={{ __html: serializeJsonLd(siteJsonLd) }}
83 />
84 {/* Apply the persisted docs theme before paint so there is no flash.
85 The site default is the paper sheet; only an explicit "dark"
86 choice re-themes the docs subtree to the whale's stage. */}
87 <script
88 dangerouslySetInnerHTML={{
89 __html:
90 "(function(){try{var t=localStorage.getItem('cw-theme');if(t==='light'||t==='dark'){document.documentElement.setAttribute('data-theme',t);}}catch(e){}})();",
91 }}
92 />
93 <a href="#main-content" className="skip-link">
94 {chrome.skipToContent}
95 </a>
96 <Nav locale={locale as Locale} />
97 <main id="main-content">{children}</main>
98 <Footer locale={locale as Locale} />
99 {/* Aggregate usage counting, on by default — see lib/telemetry. The
100 choice lives on the privacy page; every opt-out stays off. */}
101 <UsageCounting appVersion={BUILD_FACTS.version ?? "0.0.0"} />
102 </body>
103 </html>
104 );
105 }
106
106 lines Plain Text