From c1bd2c7e1133c1905b7fdc1f6bc399474c8281c9 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sun, 1 Nov 2026 14:17:13 -0300 Subject: [PATCH] feat(i18n): English localization (phase 3) - Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en - Implement localized routing, slugs, and language negotiation with fallback to en - Build dedicated search index for English with strict book exclusion - Update components and pages to dynamically render localized strings and datasets - Verify full structural parity and exact 1:1 match on Portuguese pages via comparator Co-Authored-By: Antigravity --- CHANGELOG.md | 13 + app/[locale]/catalogo/page.jsx | 180 ++- app/[locale]/comparacao/page.jsx | 347 +++-- app/[locale]/componentes/page.jsx | 134 +- app/[locale]/diagnostico/page.jsx | 193 ++- app/[locale]/frameworks/[key]/page.jsx | 101 +- app/[locale]/frameworks/layout.jsx | 69 +- app/[locale]/frameworks/page.jsx | 95 +- app/[locale]/glossario/page.jsx | 107 +- app/[locale]/layout.jsx | 61 +- app/[locale]/page.jsx | 181 ++- app/[locale]/seletor/page.jsx | 1184 +++++++++++---- app/[locale]/series/[key]/page.jsx | 482 +++--- app/[locale]/series/page.jsx | 113 +- app/[locale]/sobre/page.jsx | 143 +- app/api/busca/[locale]/route.js | 17 + components/BoardCard.jsx | 38 +- components/CodeExample.jsx | 70 +- components/ConnectionsDiagram.jsx | 140 +- components/Footer.jsx | 68 +- components/Header.jsx | 79 +- components/PinRestrictions.jsx | 113 +- components/SearchDialog.jsx | 79 +- data/en/conexoes/ESP32-C2.json | 422 ++++++ data/en/conexoes/ESP32-C3.json | 643 ++++++++ data/en/conexoes/ESP32-C5.json | 944 ++++++++++++ data/en/conexoes/ESP32-C6.json | 841 +++++++++++ data/en/conexoes/ESP32-C61.json | 729 +++++++++ data/en/conexoes/ESP32-H2.json | 603 ++++++++ data/en/conexoes/ESP32-P4.json | 1902 ++++++++++++++++++++++++ data/en/conexoes/ESP32-S2.json | 1197 +++++++++++++++ data/en/conexoes/ESP32-S3.json | 1236 +++++++++++++++ data/en/conexoes/ESP32-S31.json | 1326 +++++++++++++++++ data/en/conexoes/ESP32.json | 1132 ++++++++++++++ data/en/frameworks.json | 391 +++++ data/en/series.json | 732 +++++++++ i18n/links.js | 56 +- lib/data/en/componentes.js | 858 +++++++++++ lib/data/en/diagnostico.js | 722 +++++++++ lib/data/en/glossario.js | 193 +++ lib/data/en/topicos.js | 184 +++ lib/searchIndex.js | 77 +- lib/seo.js | 43 +- messages/en.json | 152 ++ middleware.js | 14 +- public/exemplos/en/arduino.ino | 29 + public/exemplos/en/esp-adf.c | 72 + public/exemplos/en/esp-dsp.c | 37 + public/exemplos/en/esp-idf.c | 56 + public/exemplos/en/esp-iot-solution.c | 72 + public/exemplos/en/esp-matter.c | 44 + public/exemplos/en/esp-nn.c | 44 + public/exemplos/en/esp-now.c | 92 ++ public/exemplos/en/esp-rainmaker.c | 76 + public/exemplos/en/esp-sr.c | 98 ++ public/exemplos/en/esp-who.c | 71 + public/exemplos/en/esp-zigbee.c | 97 ++ public/exemplos/en/micropython.py | 11 + public/exemplos/en/rust-esp.rs | 48 + scripts/check-i18n.mjs | 101 +- scripts/smoke-imagem.sh | 55 +- 61 files changed, 18182 insertions(+), 1225 deletions(-) create mode 100644 app/api/busca/[locale]/route.js create mode 100644 data/en/conexoes/ESP32-C2.json create mode 100644 data/en/conexoes/ESP32-C3.json create mode 100644 data/en/conexoes/ESP32-C5.json create mode 100644 data/en/conexoes/ESP32-C6.json create mode 100644 data/en/conexoes/ESP32-C61.json create mode 100644 data/en/conexoes/ESP32-H2.json create mode 100644 data/en/conexoes/ESP32-P4.json create mode 100644 data/en/conexoes/ESP32-S2.json create mode 100644 data/en/conexoes/ESP32-S3.json create mode 100644 data/en/conexoes/ESP32-S31.json create mode 100644 data/en/conexoes/ESP32.json create mode 100644 data/en/frameworks.json create mode 100644 data/en/series.json create mode 100644 lib/data/en/componentes.js create mode 100644 lib/data/en/diagnostico.js create mode 100644 lib/data/en/glossario.js create mode 100644 lib/data/en/topicos.js create mode 100644 messages/en.json create mode 100644 public/exemplos/en/arduino.ino create mode 100644 public/exemplos/en/esp-adf.c create mode 100644 public/exemplos/en/esp-dsp.c create mode 100644 public/exemplos/en/esp-idf.c create mode 100644 public/exemplos/en/esp-iot-solution.c create mode 100644 public/exemplos/en/esp-matter.c create mode 100644 public/exemplos/en/esp-nn.c create mode 100644 public/exemplos/en/esp-now.c create mode 100644 public/exemplos/en/esp-rainmaker.c create mode 100644 public/exemplos/en/esp-sr.c create mode 100644 public/exemplos/en/esp-who.c create mode 100644 public/exemplos/en/esp-zigbee.c create mode 100644 public/exemplos/en/micropython.py create mode 100644 public/exemplos/en/rust-esp.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ba108..8baf16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o ## [Não lançado] +### Adicionado + +- Internacionalização completa para o idioma inglês (`en`) (Fase 3): + - Ativação do inglês em `ENABLED_LOCALES = ["pt", "en"]` com reexibição do seletor de idiomas no cabeçalho. + - Tradução técnica integral e paridade estrutural em `messages/en.json` (15 namespaces) e arquivos de dados em `data/en/` (`series.json`, `frameworks.json`, `conexoes/*.json`) e `lib/data/en/` (`glossario.js`, `diagnostico.js`, `componentes.js`, `topicos.js`). + - 14 exemplos práticos de código com comentários em inglês em `public/exemplos/en/`. + - Roteamento com slugs canônicos em inglês (`/en/catalog`, `/en/compare`, `/en/components`, `/en/diagnostics`, `/en/frameworks`, `/en/glossary`, `/en/selector`, `/en/about`). + - Índice de busca dedicado por idioma em `/api/busca/[locale]`, com links direcionando aos slugs do idioma correspondente e caixa de busca localizada. + - Exclusão total e estrita de referências ao livro no idioma inglês (menu, rodapé, busca, sitemap e metadados), mantendo `/en/livro` respondendo HTTP 404. + - Tabela de catálogo com cabeçalhos traduzidos e mapeamento de valores na interface inglesa. + - Negociação de idioma na raiz com fallback padrão para `en` para navegadores com idiomas inativos ou genéricos, e redirecionamento 307 de prefixos inativos (`es`, `fr`, `de`, `zh-CN`) para `/en` preservando o mapeamento de slugs. + - Metadados de SEO, Open Graph e Twitter Cards localizados, com `hreflang` cruzado entre `pt-BR`, `en` e `x-default`. + ## [1.2.0] - 2026-11-01 ### Adicionado diff --git a/app/[locale]/catalogo/page.jsx b/app/[locale]/catalogo/page.jsx index 669df1c..ce32f9a 100644 --- a/app/[locale]/catalogo/page.jsx +++ b/app/[locale]/catalogo/page.jsx @@ -1,29 +1,57 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useState, useEffect } from "react"; +import { usePathname } from "next/navigation"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import PageHero from "@/components/PageHero"; import BoardCard from "@/components/BoardCard"; import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react"; +const HEADER_MAP_EN = { + "Nome": "Name", + "Descrição": "Description", + "Série": "Series", + "Loja Oficial": "Official Store", + "Fabricante": "Manufacturer", + "Processador": "Processor", + "Memória Flash": "Flash Memory", + "PSRAM": "PSRAM", + "Conector USB": "USB Connector", + "Interface USB": "USB Interface", + "Antena": "Antenna", + "Bateria": "Battery", + "Display": "Display", + "Sensores": "Sensors", + "Botões": "Buttons", + "LEDs": "LEDs", +}; + +const VALUE_MAP_EN = { + "Sim": "Yes", + "Não": "No", + "Opcional": "Optional", +}; + export default function PlacasPage() { + const pathname = usePathname() || ""; + const isEn = pathname === "/en" || pathname.startsWith("/en/"); + const [boards, setBoards] = useState([]); const [filteredBoards, setFilteredBoards] = useState([]); const [headers, setHeaders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(""); const [activeFilters, setActiveFilters] = useState({}); const [filterOptions, setFilterOptions] = useState({}); const [filtrosAbertos, setFiltrosAbertos] = useState(false); - const excludedColumns = ['Nome', 'Descrição', 'Link', 'Imagem', 'Loja Oficial']; + const excludedColumns = ["Nome", "Descrição", "Link", "Imagem", "Loja Oficial"]; useEffect(() => { fetchBoards(); - // No desktop os filtros ficam sempre à vista; no celular começam recolhidos. - setFiltrosAbertos(window.matchMedia('(min-width: 1024px)').matches); + setFiltrosAbertos(window.matchMedia("(min-width: 1024px)").matches); }, []); useEffect(() => { @@ -33,17 +61,17 @@ export default function PlacasPage() { const fetchBoards = async () => { try { setLoading(true); - const response = await fetch('/api/placas'); - + const response = await fetch("/api/placas"); + if (!response.ok) { - throw new Error('Erro ao carregar dados das placas'); + throw new Error(isEn ? "Failed to load board data" : "Erro ao carregar dados das placas"); } const result = await response.json(); setBoards(result.data); setHeaders(result.headers); setFilteredBoards(result.data); - + generateFilterOptions(result.data, result.headers); } catch (err) { setError(err.message); @@ -54,26 +82,24 @@ export default function PlacasPage() { const generateFilterOptions = (data, headers) => { const options = {}; - const filterableColumns = headers.filter(h => !excludedColumns.includes(h)); + const filterableColumns = headers.filter((h) => !excludedColumns.includes(h)); - filterableColumns.forEach(column => { + filterableColumns.forEach((column) => { const uniqueValues = new Set(); - data.forEach(item => { + data.forEach((item) => { const value = item[column]; if (value && value.toString().trim()) { uniqueValues.add(value.toString().trim()); } }); - + const values = Array.from(uniqueValues).sort(); - - // Verificar se é um filtro binário (apenas Sim/Não/Opcional) - const binaryValues = ['Sim', 'Não', 'Opcional']; - const isBinaryFilter = values.every(v => binaryValues.includes(v)); - + const binaryValues = ["Sim", "Não", "Opcional"]; + const isBinaryFilter = values.every((v) => binaryValues.includes(v)); + options[column] = { values: values, - isBinary: isBinaryFilter + isBinary: isBinaryFilter, }; }); @@ -84,7 +110,7 @@ export default function PlacasPage() { let filtered = [...boards]; if (searchTerm) { - filtered = filtered.filter(board => { + filtered = filtered.filter((board) => { const searchLower = searchTerm.toLowerCase(); return ( board.Nome?.toLowerCase().includes(searchLower) || @@ -93,16 +119,13 @@ export default function PlacasPage() { }); } - Object.keys(activeFilters).forEach(column => { + Object.keys(activeFilters).forEach((column) => { if (activeFilters[column] && activeFilters[column].length > 0) { - filtered = filtered.filter(board => { + filtered = filtered.filter((board) => { const boardValue = board[column]?.toString().trim(); - - // Para filtros binários, o valor armazenado é 'binary-enabled' - if (activeFilters[column].includes('binary-enabled')) { - return boardValue === 'Sim' || boardValue === 'Opcional'; + if (activeFilters[column].includes("binary-enabled")) { + return boardValue === "Sim" || boardValue === "Opcional"; } - return activeFilters[column].includes(boardValue); }); } @@ -112,21 +135,19 @@ export default function PlacasPage() { }; const handleFilterToggle = (column, value) => { - setActiveFilters(prev => { + setActiveFilters((prev) => { const newFilters = { ...prev }; - + if (!newFilters[column]) { newFilters[column] = [value]; } else { const index = newFilters[column].indexOf(value); if (index > -1) { - // Remove o valor - newFilters[column] = newFilters[column].filter(v => v !== value); + newFilters[column] = newFilters[column].filter((v) => v !== value); if (newFilters[column].length === 0) { delete newFilters[column]; } } else { - // Adiciona o valor newFilters[column] = [...newFilters[column], value]; } } @@ -137,7 +158,7 @@ export default function PlacasPage() { const clearFilters = () => { setActiveFilters({}); - setSearchTerm(''); + setSearchTerm(""); }; const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0); @@ -146,11 +167,15 @@ export default function PlacasPage() { const hero = ( ); @@ -160,13 +185,15 @@ export default function PlacasPage() {
{hero} -
+
{Array.from({ length: 6 }).map((_, i) => (
))}
-

Consultando o banco de placas…

+

+ {isEn ? "Querying boards database…" : "Consultando o banco de placas…"} +