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() {