| 1 | import type { Metadata } from "next"; |
| 2 | import { Fraunces, IBM_Plex_Sans, JetBrains_Mono, Noto_Serif_SC } from "next/font/google"; |
| 3 | import { Nav } from "@/components/nav"; |
| 4 | import { Footer } from "@/components/footer"; |
| 5 | import { locales, type Locale } from "@/lib/i18n/config"; |
| 6 | import { getChrome, getHome } from "@/lib/i18n/dictionaries"; |
| 7 | import { buildPageMetadata } from "@/lib/page-meta"; |
| 8 | import "../globals.css"; |
| 9 | |
| 10 | // Fraunces is the newspaper-era display face the community asked to keep — |
| 11 | // crisp, editorial, a little futuristic. Body stays IBM Plex for instrument feel. |
| 12 | const display = Fraunces({ |
| 13 | subsets: ["latin", "vietnamese"], |
| 14 | weight: ["400", "500", "600", "700"], |
| 15 | variable: "--font-display", |
| 16 | display: "swap", |
| 17 | }); |
| 18 | |
| 19 | const body = IBM_Plex_Sans({ |
| 20 | subsets: ["latin", "cyrillic", "vietnamese"], |
| 21 | weight: ["400", "500", "600"], |
| 22 | variable: "--font-body", |
| 23 | display: "swap", |
| 24 | }); |
| 25 | |
| 26 | const mono = JetBrains_Mono({ |
| 27 | subsets: ["latin", "cyrillic"], |
| 28 | weight: ["400", "500", "600"], |
| 29 | variable: "--font-mono", |
| 30 | display: "swap", |
| 31 | }); |
| 32 | |
| 33 | // Noto Serif SC is heavy; load only what we need for decorative anchors. |
| 34 | const cjk = Noto_Serif_SC({ |
| 35 | subsets: ["latin"], |
| 36 | weight: ["400", "700"], |
| 37 | variable: "--font-cjk", |
| 38 | display: "swap", |
| 39 | preload: false, |
| 40 | }); |
| 41 | |
| 42 | export function generateStaticParams() { |
| 43 | return locales.map((locale) => ({ locale })); |
| 44 | } |
| 45 | |
| 46 | export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> { |
| 47 | const { locale } = await params; |
| 48 | const home = getHome(locale); |
| 49 | return buildPageMetadata({ |
| 50 | path: "/", |
| 51 | locale, |
| 52 | title: home.metaTitle, |
| 53 | description: home.metaDescription, |
| 54 | }); |
| 55 | } |
| 56 | |
| 57 | export default async function LocaleLayout({ |
| 58 | children, |
| 59 | params, |
| 60 | }: { |
| 61 | children: React.ReactNode; |
| 62 | params: Promise<{ locale: string }>; |
| 63 | }) { |
| 64 | const { locale } = await params; |
| 65 | const chrome = getChrome(locale); |
| 66 | |
| 67 | return ( |
| 68 | <html |
| 69 | lang={locale} |
| 70 | className={`${display.variable} ${body.variable} ${mono.variable} ${cjk.variable}`} |
| 71 | suppressHydrationWarning |
| 72 | > |
| 73 | <body> |
| 74 | {/* Apply the persisted docs theme before paint so there is no flash. |
| 75 | "auto" leaves data-theme unset and defers to prefers-color-scheme. */} |
| 76 | <script |
| 77 | dangerouslySetInnerHTML={{ |
| 78 | __html: |
| 79 | "(function(){try{var t=localStorage.getItem('cw-theme');if(t==='light'||t==='dark'){document.documentElement.setAttribute('data-theme',t);}}catch(e){}})();", |
| 80 | }} |
| 81 | /> |
| 82 | <a href="#main-content" className="skip-link"> |
| 83 | {chrome.skipToContent} |
| 84 | </a> |
| 85 | <Nav locale={locale as Locale} /> |
| 86 | <main id="main-content">{children}</main> |
| 87 | <Footer locale={locale as Locale} /> |
| 88 | </body> |
| 89 | </html> |
| 90 | ); |
| 91 | } |
| 92 |