feat(i18n): routing and localization infrastructure (phase 1)
Implement Phase 1 of i18n spec: - Configure next-intl with 6 locales (pt, en, es, fr, de, zh-CN) and pathnames - Move routes to app/[locale] structure with localized layouts and metadata - Add middleware for root language negotiation (307), legacy redirects (308), and 404 for non-pt book - Implement language selector dropdown in header with cookie persistence - Add system typography rules for zh-CN and word-break/hyphenation for de/fr Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
1 parent
e92aea0c84
commit
83c0ba3e74
38 files changed
+1699
-324
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import "../globals.css";
|
||||
import Providers from "@/components/Providers";
|
||||
import { SITE, jsonLd } from "@/lib/seo";
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-space-grotesk",
|
||||
display: "swap",
|
||||
weight: ["500", "600", "700"],
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-manrope",
|
||||
display: "swap",
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jetbrains",
|
||||
display: "swap",
|
||||
weight: ["400", "500", "600"],
|
||||
});
|
||||
|
||||
const LANG_MAP = {
|
||||
pt: "pt-BR",
|
||||
en: "en",
|
||||
es: "es-419",
|
||||
fr: "fr",
|
||||
de: "de",
|
||||
"zh-CN": "zh-CN",
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { locale } = await params;
|
||||
const ogLocaleMap = {
|
||||
pt: "pt_BR",
|
||||
en: "en_US",
|
||||
es: "es_419",
|
||||
fr: "fr_FR",
|
||||
de: "de_DE",
|
||||
"zh-CN": "zh_CN",
|
||||
};
|
||||
|
||||
return {
|
||||
metadataBase: new URL(SITE.url),
|
||||
title: {
|
||||
default: "ESPDocs · Documentação do ESP32 em português",
|
||||
template: "%s · ESPDocs",
|
||||
},
|
||||
description:
|
||||
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro. Datasheets, periféricos, segurança em hardware e diagramas de conexão.",
|
||||
applicationName: "ESPDocs",
|
||||
authors: [{ name: "Lucas Rayan Guerra", url: "https://lucasrguerra.dev.br" }],
|
||||
creator: "Lucas Rayan Guerra",
|
||||
publisher: "Ciência Embarcada",
|
||||
category: "technology",
|
||||
keywords: [
|
||||
"ESP32", "ESP32 português", "documentação ESP32", "pinout ESP32",
|
||||
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
|
||||
"comparar ESP32", "qual ESP32 escolher", "datasheet ESP32",
|
||||
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embarcados",
|
||||
],
|
||||
alternates: { canonical: `/${locale}` },
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: ogLocaleMap[locale] || "pt_BR",
|
||||
url: `${SITE.url}/${locale}`,
|
||||
siteName: SITE.nome,
|
||||
title: "ESPDocs · Documentação do ESP32 em português",
|
||||
description:
|
||||
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "ESPDocs · Documentação do ESP32 em português",
|
||||
description:
|
||||
"Especificações, pinagem, comparação e seleção das 12 séries de ESP32, em português.",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
"max-video-preview": -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.ico", sizes: "48x48", type: "image/x-icon" },
|
||||
{ url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
||||
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
|
||||
{ url: "/icon.svg", type: "image/svg+xml" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||
],
|
||||
shortcut: "/favicon.ico",
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
formatDetection: { telephone: false, address: false, email: false },
|
||||
};
|
||||
}
|
||||
|
||||
export const viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#f5f6f8" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#07090d" },
|
||||
],
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children, params }) {
|
||||
const { locale } = await params;
|
||||
if (!routing.locales.includes(locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const lang = LANG_MAP[locale] || locale;
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={lang} className={`${spaceGrotesk.variable} ${manrope.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Dados estruturados do site e da organização. É o que permite ao
|
||||
Google montar o box de busca interna e entender a autoria. */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.siteESociedade()) }}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans antialiased bg-canvas text-ink overflow-x-clip" suppressHydrationWarning>
|
||||
<a
|
||||
href="#conteudo"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-full focus:bg-ink focus:px-5 focus:py-3 focus:text-sm focus:font-semibold focus:text-canvas"
|
||||
>
|
||||
Pular para o conteúdo
|
||||
</a>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user