Consultando o banco de placas…
++ {isEn ? "Querying boards database…" : "Consultando o banco de placas…"} +
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 = (
Consultando o banco de placas…
+ {isEn ? "Querying boards database…" : "Consultando o banco de placas…"}
+
Não foi possível carregar as placas
-{error}. Verifique a conexão e tente de novo.
++ {isEn ? "Could not load boards" : "Não foi possível carregar as placas"} +
++ {error}. {isEn ? "Check your connection and try again." : "Verifique a conexão e tente de novo."} +