From 83c0ba3e74eab8d4a9764273bb20123335109dd2 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sun, 1 Nov 2026 09:31:22 -0300 Subject: [PATCH] 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 --- CHANGELOG.md | 8 + app/{ => [locale]}/catalogo/layout.jsx | 0 app/{ => [locale]}/catalogo/page.jsx | 0 app/{ => [locale]}/comparacao/layout.jsx | 0 app/{ => [locale]}/comparacao/page.jsx | 0 app/{ => [locale]}/componentes/layout.jsx | 0 app/{ => [locale]}/componentes/page.jsx | 0 app/{ => [locale]}/diagnostico/layout.jsx | 0 app/{ => [locale]}/diagnostico/page.jsx | 0 app/{ => [locale]}/frameworks/[key]/page.jsx | 0 app/{ => [locale]}/frameworks/layout.jsx | 0 app/{ => [locale]}/frameworks/page.jsx | 0 app/{ => [locale]}/glossario/layout.jsx | 0 app/{ => [locale]}/glossario/page.jsx | 0 app/[locale]/layout.jsx | 162 ++++ app/{ => [locale]}/livro/page.jsx | 43 +- app/{ => [locale]}/not-found.jsx | 0 app/{ => [locale]}/page.jsx | 143 ++-- app/{ => [locale]}/seletor/layout.jsx | 0 app/{ => [locale]}/seletor/page.jsx | 0 app/{ => [locale]}/series/[key]/page.jsx | 0 app/{ => [locale]}/series/layout.jsx | 0 app/{ => [locale]}/series/page.jsx | 0 app/{ => [locale]}/sobre/page.jsx | 0 app/globals.css | 24 + app/layout.jsx | 134 ---- components/Footer.jsx | 145 ++-- components/Header.jsx | 97 ++- components/LanguageSelector.jsx | 232 ++++++ i18n/navigation.js | 4 + i18n/request.js | 26 + i18n/routing.js | 101 +++ messages/pt.json | 1 + middleware.js | 102 +++ next.config.mjs | 7 +- package-lock.json | 782 ++++++++++++++++++- package.json | 1 + scripts/auditar-paginas.mjs | 11 +- 38 files changed, 1699 insertions(+), 324 deletions(-) rename app/{ => [locale]}/catalogo/layout.jsx (100%) rename app/{ => [locale]}/catalogo/page.jsx (100%) rename app/{ => [locale]}/comparacao/layout.jsx (100%) rename app/{ => [locale]}/comparacao/page.jsx (100%) rename app/{ => [locale]}/componentes/layout.jsx (100%) rename app/{ => [locale]}/componentes/page.jsx (100%) rename app/{ => [locale]}/diagnostico/layout.jsx (100%) rename app/{ => [locale]}/diagnostico/page.jsx (100%) rename app/{ => [locale]}/frameworks/[key]/page.jsx (100%) rename app/{ => [locale]}/frameworks/layout.jsx (100%) rename app/{ => [locale]}/frameworks/page.jsx (100%) rename app/{ => [locale]}/glossario/layout.jsx (100%) rename app/{ => [locale]}/glossario/page.jsx (100%) create mode 100644 app/[locale]/layout.jsx rename app/{ => [locale]}/livro/page.jsx (93%) rename app/{ => [locale]}/not-found.jsx (100%) rename app/{ => [locale]}/page.jsx (71%) rename app/{ => [locale]}/seletor/layout.jsx (100%) rename app/{ => [locale]}/seletor/page.jsx (100%) rename app/{ => [locale]}/series/[key]/page.jsx (100%) rename app/{ => [locale]}/series/layout.jsx (100%) rename app/{ => [locale]}/series/page.jsx (100%) rename app/{ => [locale]}/sobre/page.jsx (100%) delete mode 100644 app/layout.jsx create mode 100644 components/LanguageSelector.jsx create mode 100644 i18n/navigation.js create mode 100644 i18n/request.js create mode 100644 i18n/routing.js create mode 100644 messages/pt.json create mode 100644 middleware.js 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. */} +