From 7fce46b9e1197cb282b88e3a686fc05cda1d8a6f Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sun, 1 Nov 2026 22:06:36 -0300 Subject: [PATCH] feat(i18n): French and German localization (phase 5) - Add full message catalogs and content for French (fr) and German (de) - Implement ASCII canonical slugs for French and German routes - Add language-specific search endpoints (/api/busca/fr, /api/busca/de) - Configure strict 404 responses for /fr/livro and /de/livro - Localize SEO, OpenGraph (fr_FR, de_DE), Twitter and JSON-LD schemas - Add localized code example comments across 14 examples for fr and de - Enable 'fr' and 'de' in ENABLED_LOCALES with root negotiation and redirect - Extend test suite in check-i18n, smoke-imagem and verifica-vazamentos Co-Authored-By: Antigravity --- CHANGELOG.md | 19 + app/[locale]/catalogo/layout.jsx | 26 +- app/[locale]/catalogo/page.jsx | 106 +- app/[locale]/comparacao/layout.jsx | 58 +- app/[locale]/comparacao/page.jsx | 232 +++- app/[locale]/componentes/layout.jsx | 93 +- app/[locale]/componentes/page.jsx | 110 +- app/[locale]/diagnostico/layout.jsx | 87 +- app/[locale]/diagnostico/page.jsx | 178 ++- app/[locale]/frameworks/[key]/page.jsx | 78 +- app/[locale]/frameworks/layout.jsx | 44 +- app/[locale]/frameworks/page.jsx | 110 +- app/[locale]/glossario/layout.jsx | 49 +- app/[locale]/glossario/page.jsx | 135 +- app/[locale]/layout.jsx | 34 +- app/[locale]/not-found.jsx | 30 +- app/[locale]/page.jsx | 226 +++- app/[locale]/seletor/layout.jsx | 118 +- app/[locale]/seletor/page.jsx | 914 ++++++++++--- app/[locale]/series/[key]/page.jsx | 398 +++++- app/[locale]/series/layout.jsx | 62 +- app/[locale]/series/page.jsx | 133 +- app/[locale]/sobre/page.jsx | 142 +- app/api/busca/[locale]/route.js | 4 +- components/BoardCard.jsx | 66 +- components/CodeExample.jsx | 130 +- components/ConnectionsDiagram.jsx | 228 +++- components/Footer.jsx | 156 ++- components/Header.jsx | 223 +++- components/LanguageSelector.jsx | 34 +- components/PageHero.jsx | 32 +- components/PinRestrictions.jsx | 185 ++- components/SearchDialog.jsx | 166 ++- components/SeriesTabMenu.jsx | 20 +- data/de/conexoes/ESP32-C2.json | 385 ++++++ data/de/conexoes/ESP32-C3.json | 574 ++++++++ data/de/conexoes/ESP32-C5.json | 862 ++++++++++++ data/de/conexoes/ESP32-C6.json | 757 +++++++++++ data/de/conexoes/ESP32-C61.json | 642 +++++++++ data/de/conexoes/ESP32-H2.json | 549 ++++++++ data/de/conexoes/ESP32-P4.json | 1707 ++++++++++++++++++++++++ data/de/conexoes/ESP32-S2.json | 1069 +++++++++++++++ data/de/conexoes/ESP32-S3.json | 1100 +++++++++++++++ data/de/conexoes/ESP32-S31.json | 1175 ++++++++++++++++ data/de/conexoes/ESP32.json | 1018 ++++++++++++++ data/de/frameworks.json | 423 ++++++ data/de/series.json | 732 ++++++++++ data/fr/conexoes/ESP32-C2.json | 385 ++++++ data/fr/conexoes/ESP32-C3.json | 574 ++++++++ data/fr/conexoes/ESP32-C5.json | 862 ++++++++++++ data/fr/conexoes/ESP32-C6.json | 757 +++++++++++ data/fr/conexoes/ESP32-C61.json | 642 +++++++++ data/fr/conexoes/ESP32-H2.json | 549 ++++++++ data/fr/conexoes/ESP32-P4.json | 1707 ++++++++++++++++++++++++ data/fr/conexoes/ESP32-S2.json | 1069 +++++++++++++++ data/fr/conexoes/ESP32-S3.json | 1100 +++++++++++++++ data/fr/conexoes/ESP32-S31.json | 1175 ++++++++++++++++ data/fr/conexoes/ESP32.json | 1018 ++++++++++++++ data/fr/frameworks.json | 423 ++++++ data/fr/series.json | 732 ++++++++++ i18n/links.js | 2 +- lib/data/de/componentes.js | 1264 ++++++++++++++++++ lib/data/de/diagnostico.js | 724 ++++++++++ lib/data/de/glossario.js | 408 ++++++ lib/data/de/topicos.js | 177 +++ lib/data/fr/componentes.js | 1264 ++++++++++++++++++ lib/data/fr/diagnostico.js | 724 ++++++++++ lib/data/fr/glossario.js | 408 ++++++ lib/data/fr/topicos.js | 176 +++ lib/searchIndex.js | 77 +- lib/seo.js | 94 +- messages/de.json | 152 +++ messages/fr.json | 152 +++ public/exemplos/de/arduino.ino | 29 + public/exemplos/de/esp-adf.c | 72 + public/exemplos/de/esp-dsp.c | 37 + public/exemplos/de/esp-idf.c | 56 + public/exemplos/de/esp-iot-solution.c | 72 + public/exemplos/de/esp-matter.c | 44 + public/exemplos/de/esp-nn.c | 44 + public/exemplos/de/esp-now.c | 92 ++ public/exemplos/de/esp-rainmaker.c | 76 ++ public/exemplos/de/esp-sr.c | 98 ++ public/exemplos/de/esp-who.c | 71 + public/exemplos/de/esp-zigbee.c | 97 ++ public/exemplos/de/micropython.py | 11 + public/exemplos/de/rust-esp.rs | 48 + public/exemplos/fr/arduino.ino | 29 + public/exemplos/fr/esp-adf.c | 72 + public/exemplos/fr/esp-dsp.c | 37 + public/exemplos/fr/esp-idf.c | 56 + public/exemplos/fr/esp-iot-solution.c | 72 + public/exemplos/fr/esp-matter.c | 44 + public/exemplos/fr/esp-nn.c | 44 + public/exemplos/fr/esp-now.c | 92 ++ public/exemplos/fr/esp-rainmaker.c | 76 ++ public/exemplos/fr/esp-sr.c | 98 ++ public/exemplos/fr/esp-who.c | 71 + public/exemplos/fr/esp-zigbee.c | 97 ++ public/exemplos/fr/micropython.py | 11 + public/exemplos/fr/rust-esp.rs | 48 + scripts/check-i18n.mjs | 1 + scripts/smoke-imagem.sh | 34 + scripts/verifica-vazamentos.py | 92 +- 104 files changed, 33369 insertions(+), 686 deletions(-) create mode 100644 data/de/conexoes/ESP32-C2.json create mode 100644 data/de/conexoes/ESP32-C3.json create mode 100644 data/de/conexoes/ESP32-C5.json create mode 100644 data/de/conexoes/ESP32-C6.json create mode 100644 data/de/conexoes/ESP32-C61.json create mode 100644 data/de/conexoes/ESP32-H2.json create mode 100644 data/de/conexoes/ESP32-P4.json create mode 100644 data/de/conexoes/ESP32-S2.json create mode 100644 data/de/conexoes/ESP32-S3.json create mode 100644 data/de/conexoes/ESP32-S31.json create mode 100644 data/de/conexoes/ESP32.json create mode 100644 data/de/frameworks.json create mode 100644 data/de/series.json create mode 100644 data/fr/conexoes/ESP32-C2.json create mode 100644 data/fr/conexoes/ESP32-C3.json create mode 100644 data/fr/conexoes/ESP32-C5.json create mode 100644 data/fr/conexoes/ESP32-C6.json create mode 100644 data/fr/conexoes/ESP32-C61.json create mode 100644 data/fr/conexoes/ESP32-H2.json create mode 100644 data/fr/conexoes/ESP32-P4.json create mode 100644 data/fr/conexoes/ESP32-S2.json create mode 100644 data/fr/conexoes/ESP32-S3.json create mode 100644 data/fr/conexoes/ESP32-S31.json create mode 100644 data/fr/conexoes/ESP32.json create mode 100644 data/fr/frameworks.json create mode 100644 data/fr/series.json create mode 100644 lib/data/de/componentes.js create mode 100644 lib/data/de/diagnostico.js create mode 100644 lib/data/de/glossario.js create mode 100644 lib/data/de/topicos.js create mode 100644 lib/data/fr/componentes.js create mode 100644 lib/data/fr/diagnostico.js create mode 100644 lib/data/fr/glossario.js create mode 100644 lib/data/fr/topicos.js create mode 100644 messages/de.json create mode 100644 messages/fr.json create mode 100644 public/exemplos/de/arduino.ino create mode 100644 public/exemplos/de/esp-adf.c create mode 100644 public/exemplos/de/esp-dsp.c create mode 100644 public/exemplos/de/esp-idf.c create mode 100644 public/exemplos/de/esp-iot-solution.c create mode 100644 public/exemplos/de/esp-matter.c create mode 100644 public/exemplos/de/esp-nn.c create mode 100644 public/exemplos/de/esp-now.c create mode 100644 public/exemplos/de/esp-rainmaker.c create mode 100644 public/exemplos/de/esp-sr.c create mode 100644 public/exemplos/de/esp-who.c create mode 100644 public/exemplos/de/esp-zigbee.c create mode 100644 public/exemplos/de/micropython.py create mode 100644 public/exemplos/de/rust-esp.rs create mode 100644 public/exemplos/fr/arduino.ino create mode 100644 public/exemplos/fr/esp-adf.c create mode 100644 public/exemplos/fr/esp-dsp.c create mode 100644 public/exemplos/fr/esp-idf.c create mode 100644 public/exemplos/fr/esp-iot-solution.c create mode 100644 public/exemplos/fr/esp-matter.c create mode 100644 public/exemplos/fr/esp-nn.c create mode 100644 public/exemplos/fr/esp-now.c create mode 100644 public/exemplos/fr/esp-rainmaker.c create mode 100644 public/exemplos/fr/esp-sr.c create mode 100644 public/exemplos/fr/esp-who.c create mode 100644 public/exemplos/fr/esp-zigbee.c create mode 100644 public/exemplos/fr/micropython.py create mode 100644 public/exemplos/fr/rust-esp.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cac838e..510e11c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o ## [Não lançado] +### Adicionado + +- Internacionalização completa para os idiomas Francês (`fr`) e Alemão (`de`) (Fase 5): + - Ativação do francês e do alemão em `ENABLED_LOCALES = ["pt", "en", "es", "fr", "de"]` e no seletor de idiomas do cabeçalho. + - Tradução técnica integral e paridade estrutural em `messages/fr.json` e `messages/de.json` (15 namespaces), nos arquivos de dados em `data/{fr,de}/` (`series.json`, `frameworks.json`, `conexoes/*.json`) e `lib/data/{fr,de}/` (`glossario.js`, `diagnostico.js`, `componentes.js`, `topicos.js`). + - Tradução técnica natural com tratamento formal respeitado: "vous" em francês e "Sie" em alemão. + - 14 exemplos práticos de código com comentários em francês e 14 em alemão em `public/exemplos/{fr,de}/`. + - Slugs canônicos ASCII por idioma: + - Francês: `/fr/series`, `/fr/catalogue`, `/fr/comparaison`, `/fr/composants`, `/fr/diagnostic`, `/fr/frameworks`, `/fr/glossaire`, `/fr/selecteur`, `/fr/a-propos`. + - Alemão: `/de/serien`, `/de/katalog`, `/de/vergleich`, `/de/komponenten`, `/de/diagnose`, `/de/frameworks`, `/de/glossar`, `/de/auswahl`, `/de/ueber-uns`. + - Busca dedicada por idioma em `/api/busca/fr` e `/api/busca/de` com normalização de acentos, caracteres alemães (ä, ö, ü, ß) e ligaduras francesas (œ, æ), sem menção ao livro e com links diretos aos slugs canônicos. + - Exclusão total e estrita de referências ao livro fora do português, com `/fr/livro` e `/de/livro` respondendo HTTP 404 estrito. + - Tabela do catálogo de placas com colunas traduzidas e mapeamento de valores técnicos nos idiomas francês e alemão. + - Negociação de idioma na raiz com roteamento para `fr` ou `de` com base nos cabeçalhos Accept-Language ou cookies `NEXT_LOCALE=fr` / `NEXT_LOCALE=de`, e fallback padrão para `en` para navegadores com o idioma inativo restante (`zh-CN`). + - Metadados de SEO, Open Graph e Twitter Cards localizados com `og:locale` (`fr_FR`, `de_DE`), `inLanguage` (`fr`, `de`) e `hreflang` cruzado entre `pt-BR`, `en`, `es-419`, `fr-FR`, `de-DE` e `x-default`. + - Suporte CSS com hifenação automática (`hyphens: auto`) para alemão e francês via atributo `lang` dinâmico no layout raiz. + - Extensão da suíte de verificação: `scripts/check-i18n.mjs` valida `fr` e `de`, `scripts/smoke-imagem.sh` testa as rotas francesas e alemãs e `scripts/verifica-vazamentos.py` varre todas as 36 rotas em francês e 36 em alemão (texto visível e blocos JSON-LD). + + ## [1.4.0] - 2026-11-01 ### Adicionado diff --git a/app/[locale]/catalogo/layout.jsx b/app/[locale]/catalogo/layout.jsx index d3a284b..df49257 100644 --- a/app/[locale]/catalogo/layout.jsx +++ b/app/[locale]/catalogo/layout.jsx @@ -4,12 +4,18 @@ export async function generateMetadata({ params }) { const { locale } = await params; const isEn = locale === "en"; const isEs = locale === "es"; + const isFr = locale === "fr"; + const isDe = locale === "de"; return paginaMeta({ - titulo: isEn ? "ESP32 Board Catalog" : isEs ? "Catálogo de placas ESP32" : "Catálogo de placas ESP32", + titulo: isEn ? "ESP32 Board Catalog" : isEs ? "Catálogo de placas ESP32" : isFr ? "Catalogue de cartes ESP32" : isDe ? "ESP32-Board-Katalog" : "Catálogo de placas ESP32", descricao: isEn ? "ESP32 development boards and DevKits available in Brazil, with filters by series, connector, and USB-serial converter." : isEs ? "Placas de desarrollo y DevKits con ESP32, con filtros por serie, conector y conversor USB-serial." + : isFr + ? "Cartes de développement et DevKits ESP32, avec filtres par série, connecteur et convertisseur USB-série." + : isDe + ? "ESP32-Entwicklungsboards und DevKits, mit Filtern nach Serie, Anschluss und USB-Seriell-Wandler." : "Placas de desenvolvimento com ESP32 disponíveis no Brasil, com filtro por série, conector e conversor USB-serial.", caminho: "/catalogo", locale, @@ -17,6 +23,10 @@ export async function generateMetadata({ params }) { ? ["ESP32 boards", "ESP32 DevKit", "buy ESP32", "ESP32 DevKitC"] : isEs ? ["placas ESP32", "DevKit ESP32", "comprar ESP32", "ESP32 DevKitC"] + : isFr + ? ["cartes ESP32", "DevKit ESP32", "acheter ESP32", "ESP32 DevKitC"] + : isDe + ? ["ESP32 Boards", "ESP32 DevKit", "ESP32 kaufen", "ESP32 DevKitC"] : ["placas ESP32", "DevKit ESP32", "comprar ESP32", "ESP32 DevKitC"], }); } @@ -25,7 +35,9 @@ export default async function Layout({ children, params }) { const { locale } = await params; const isEn = locale === "en"; const isEs = locale === "es"; - const canonPath = isEn ? "/en/catalog" : isEs ? "/es/catalogo" : "/catalogo"; + const isFr = locale === "fr"; + const isDe = locale === "de"; + const canonPath = isEn ? "/en/catalog" : isEs ? "/es/catalogo" : isFr ? "/fr/catalogue" : isDe ? "/de/katalog" : "/catalogo"; return ( <> @@ -34,11 +46,15 @@ export default async function Layout({ children, params }) { dangerouslySetInnerHTML={{ __html: JSON.stringify( jsonLd.aplicacaoWeb({ - nome: isEn ? "ESP32 Development Board Catalog" : isEs ? "Catálogo de placas de desarrollo ESP32" : "Catálogo de Placas ESP32", + nome: isEn ? "ESP32 Development Board Catalog" : isEs ? "Catálogo de placas de desarrollo ESP32" : isFr ? "Catalogue de cartes de développement ESP32" : isDe ? "Katalog für ESP32-Entwicklungsboards" : "Catálogo de Placas ESP32", descricao: isEn ? "Interactive guide and catalog of development boards and DevKits featuring ESP32 microcontrollers." : isEs ? "Guía y catálogo interactivo de placas de desarrollo y DevKits con microcontroladores ESP32." + : isFr + ? "Guide interactif et catalogue de cartes de développement et DevKits avec microcontrôleurs ESP32." + : isDe + ? "Interaktiver Leitfaden und Katalog für Entwicklungsboards und DevKits mit ESP32-Mikrocontrollern." : "Guia e catálogo interativo de placas de desenvolvimento e DevKits com microcontroladores ESP32.", caminho: canonPath, locale, @@ -51,8 +67,8 @@ export default async function Layout({ children, params }) { dangerouslySetInnerHTML={{ __html: JSON.stringify( jsonLd.trilha([ - { nome: isEn ? "Home" : isEs ? "Inicio" : "Início", caminho: isEn ? "/en" : isEs ? "/es" : "/" }, - { nome: isEn ? "Board Catalog" : isEs ? "Catálogo de placas" : "Catálogo de Placas", caminho: canonPath }, + { nome: isEn ? "Home" : isEs ? "Inicio" : isFr ? "Accueil" : isDe ? "Startseite" : "Início", caminho: isEn ? "/en" : isEs ? "/es" : isFr ? "/fr" : isDe ? "/de" : "/" }, + { nome: isEn ? "Board Catalog" : isEs ? "Catálogo de placas" : isFr ? "Catalogue de cartes" : isDe ? "Board-Katalog" : "Catálogo de Placas", caminho: canonPath }, ]) ), }} diff --git a/app/[locale]/catalogo/page.jsx b/app/[locale]/catalogo/page.jsx index dcef360..8c25f15 100644 --- a/app/[locale]/catalogo/page.jsx +++ b/app/[locale]/catalogo/page.jsx @@ -58,10 +58,62 @@ const VALUE_MAP_ES = { "Opcional": "Opcional", }; +const HEADER_MAP_FR = { + "Nome": "Nom", + "Descrição": "Description", + "Série": "Série", + "Loja Oficial": "Boutique officielle", + "Fabricante": "Fabricant", + "Processador": "Processeur", + "Memória Flash": "Mémoire Flash", + "PSRAM": "PSRAM", + "Conector USB": "Connecteur USB", + "Interface USB": "Interface USB", + "Antena": "Antenne", + "Bateria": "Batterie", + "Display": "Écran", + "Sensores": "Capteurs", + "Botões": "Boutons", + "LEDs": "LEDs", +}; + +const VALUE_MAP_FR = { + "Sim": "Oui", + "Não": "Non", + "Opcional": "Optionnel", +}; + +const HEADER_MAP_DE = { + "Nome": "Name", + "Descrição": "Beschreibung", + "Série": "Serie", + "Loja Oficial": "Offizieller Store", + "Fabricante": "Hersteller", + "Processador": "Prozessor", + "Memória Flash": "Flash-Speicher", + "PSRAM": "PSRAM", + "Conector USB": "USB-Anschluss", + "Interface USB": "USB-Schnittstelle", + "Antena": "Antenne", + "Bateria": "Batterie", + "Display": "Display", + "Sensores": "Sensoren", + "Botões": "Tasten", + "LEDs": "LEDs", +}; + +const VALUE_MAP_DE = { + "Sim": "Ja", + "Não": "Nein", + "Opcional": "Optional", +}; + export default function PlacasPage() { const pathname = usePathname() || ""; const isEn = pathname === "/en" || pathname.startsWith("/en/"); const isEs = pathname === "/es" || pathname.startsWith("/es/"); + const isFr = pathname === "/fr" || pathname.startsWith("/fr/"); + const isDe = pathname === "/de" || pathname.startsWith("/de/"); const [boards, setBoards] = useState([]); const [filteredBoards, setFilteredBoards] = useState([]); @@ -90,7 +142,7 @@ export default function PlacasPage() { const response = await fetch("/api/placas"); if (!response.ok) { - throw new Error(isEn ? "Failed to load board data" : isEs ? "Error al cargar los datos de las placas" : "Erro ao carregar dados das placas"); + throw new Error(isEn ? "Failed to load board data" : isEs ? "Error al cargar los datos de las placas" : isFr ? "Erreur lors du chargement des données des cartes" : isDe ? "Fehler beim Laden der Board-Daten" : "Erro ao carregar dados das placas"); } const result = await response.json(); @@ -195,15 +247,21 @@ export default function PlacasPage() { @@ -215,14 +273,14 @@ export default function PlacasPage() {
{hero} -
+
{Array.from({ length: 6 }).map((_, i) => (
))}

- {isEn ? "Querying boards database…" : isEs ? "Consultando la base de datos de placas…" : "Consultando o banco de placas…"} + {isEn ? "Querying boards database…" : isEs ? "Consultando la base de datos de placas…" : isFr ? "Consultation de la base de cartes…" : isDe ? "Board-Datenbank wird abgefragt…" : "Consultando o banco de placas…"}

@@ -242,14 +300,14 @@ export default function PlacasPage() {