Files
ESPDocs/app/[locale]/layout.jsx
T
Lucas RayanandAntigravity 47c55515bb
CI / Lint e auditorias (push) Successful in 50s
CI / Trivy (dependências e segredos) (push) Successful in 10s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m31s
fix(i18n): eliminate portuguese text leaks across english routes
- Localize skip-to-content accessibility link and breadcrumb navigation
- Convert section layouts to dynamic generateMetadata with English titles, descriptions, and structured data
- Eliminate mixed language expressions and translate remaining SoC connection pin warnings and descriptions
- Translate component registry items, categories, tags, and IDF Component Manager guide
- Add strict identical string verification to scripts/check-i18n.mjs with technical allowlist

Co-Authored-By: Antigravity <noreply@google.com>
2026-11-01 17:17:33 -03:00

197 lines
6.1 KiB
React

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 { ENABLED_LOCALES } from "@/i18n/links";
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 ENABLED_LOCALES.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const ogLocaleMap = {
pt: "pt_BR",
en: "en_US",
es: "es_419",
fr: "fr_FR",
de: "de_DE",
"zh-CN": "zh_CN",
};
const alternateLocales = ENABLED_LOCALES.filter((l) => l !== locale).map((l) => ogLocaleMap[l]);
const titleDefault = isEn
? "ESPDocs · ESP32 Open Documentation"
: "ESPDocs · Documentação do ESP32 em português";
const description = isEn
? "Specifications, pinouts, comparison and selection of 12 ESP32 microcontroller series. Datasheets, peripherals, hardware security and connection diagrams."
: "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.";
const keywords = isEn
? [
"ESP32", "ESP32 documentation", "ESP32 pinout",
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
"compare ESP32", "which ESP32 to choose", "ESP32 datasheet",
"microcontroller", "Espressif", "ESP-IDF", "IoT", "embedded systems",
]
: [
"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",
];
const languages = Object.fromEntries(
ENABLED_LOCALES.map((l) => [LANG_MAP[l] || l, `/${l}`])
);
if (ENABLED_LOCALES.includes("en")) {
languages["x-default"] = "/en";
}
return {
metadataBase: new URL(SITE.url),
title: {
default: titleDefault,
template: "%s · ESPDocs",
},
description,
applicationName: "ESPDocs",
authors: [{ name: "Lucas Rayan Guerra", url: "https://lucasrguerra.dev.br" }],
creator: "Lucas Rayan Guerra",
publisher: "Ciência Embarcada",
category: "technology",
keywords,
alternates: {
canonical: `/${locale}`,
languages,
},
openGraph: {
type: "website",
locale: ogLocaleMap[locale] || "pt_BR",
alternateLocale: alternateLocales.length > 0 ? alternateLocales : undefined,
url: `${SITE.url}/${locale}`,
siteName: SITE.nome,
title: titleDefault,
description: isEn
? "Specifications, pinouts, comparison and selection of 12 ESP32 microcontroller series."
: "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: titleDefault,
description: isEn
? "Specifications, pinouts, comparison and selection of 12 ESP32 series."
: "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 (!ENABLED_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(locale)) }}
/>
</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"
>
{messages?.Common?.skipToContent || "Pular para o conteúdo"}
</a>
<NextIntlClientProvider locale={locale} messages={messages}>
<Providers>
{children}
</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}