diff --git a/CHANGELOG.md b/CHANGELOG.md index d4778f9..7c645eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o ## [Não lançado] +### Adicionado + +- Infraestrutura de internacionalização com `next-intl` (Fase 1): + - Roteamento com prefixo de idioma sob `app/[locale]/`, com 6 locales declarados (`pt`, `en`, `es`, `fr`, `de`, `zh-CN`). + - `middleware.js` com negociação de idioma na raiz por cookie `NEXT_LOCALE` e cabeçalho `Accept-Language` (com pesos `q`), redirecionamento 308 de URLs antigas sem prefixo para `/pt/`, e retorno 404 para `/livro` fora do português. + - Seletor de idiomas acessível no cabeçalho com atalho para os 6 idiomas, persistência via cookie `NEXT_LOCALE` e mapeamento de rota correspondente. + - Regras de tipografia de sistema CJK para `zh-CN` e hifenização/quebra de palavras para `de` e `fr`. + ## [1.1.0] - 2026-11-01 ### Adicionado diff --git a/app/catalogo/layout.jsx b/app/[locale]/catalogo/layout.jsx similarity index 100% rename from app/catalogo/layout.jsx rename to app/[locale]/catalogo/layout.jsx diff --git a/app/catalogo/page.jsx b/app/[locale]/catalogo/page.jsx similarity index 100% rename from app/catalogo/page.jsx rename to app/[locale]/catalogo/page.jsx diff --git a/app/comparacao/layout.jsx b/app/[locale]/comparacao/layout.jsx similarity index 100% rename from app/comparacao/layout.jsx rename to app/[locale]/comparacao/layout.jsx diff --git a/app/comparacao/page.jsx b/app/[locale]/comparacao/page.jsx similarity index 100% rename from app/comparacao/page.jsx rename to app/[locale]/comparacao/page.jsx diff --git a/app/componentes/layout.jsx b/app/[locale]/componentes/layout.jsx similarity index 100% rename from app/componentes/layout.jsx rename to app/[locale]/componentes/layout.jsx diff --git a/app/componentes/page.jsx b/app/[locale]/componentes/page.jsx similarity index 100% rename from app/componentes/page.jsx rename to app/[locale]/componentes/page.jsx diff --git a/app/diagnostico/layout.jsx b/app/[locale]/diagnostico/layout.jsx similarity index 100% rename from app/diagnostico/layout.jsx rename to app/[locale]/diagnostico/layout.jsx diff --git a/app/diagnostico/page.jsx b/app/[locale]/diagnostico/page.jsx similarity index 100% rename from app/diagnostico/page.jsx rename to app/[locale]/diagnostico/page.jsx diff --git a/app/frameworks/[key]/page.jsx b/app/[locale]/frameworks/[key]/page.jsx similarity index 100% rename from app/frameworks/[key]/page.jsx rename to app/[locale]/frameworks/[key]/page.jsx diff --git a/app/frameworks/layout.jsx b/app/[locale]/frameworks/layout.jsx similarity index 100% rename from app/frameworks/layout.jsx rename to app/[locale]/frameworks/layout.jsx diff --git a/app/frameworks/page.jsx b/app/[locale]/frameworks/page.jsx similarity index 100% rename from app/frameworks/page.jsx rename to app/[locale]/frameworks/page.jsx diff --git a/app/glossario/layout.jsx b/app/[locale]/glossario/layout.jsx similarity index 100% rename from app/glossario/layout.jsx rename to app/[locale]/glossario/layout.jsx diff --git a/app/glossario/page.jsx b/app/[locale]/glossario/page.jsx similarity index 100% rename from app/glossario/page.jsx rename to app/[locale]/glossario/page.jsx diff --git a/app/[locale]/layout.jsx b/app/[locale]/layout.jsx new file mode 100644 index 0000000..31e60b2 --- /dev/null +++ b/app/[locale]/layout.jsx @@ -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 ( + +
+ {/* Dados estruturados do site e da organização. É o que permite ao + Google montar o box de busca interna e entender a autoria. */} + + + + + Pular para o conteúdo + +