feat(i18n): Spanish localization (phase 4)
CI / Lint e auditorias (push) Successful in 1m31s
CI / Trivy (dependências e segredos) (push) Successful in 10s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m23s

Complete implementation of Spanish (es-419) language support:
- Canonical catalogs and data: messages/es.json, data/es/**, lib/data/es/**, public/exemplos/es/**
- Canonical slugs: /es/series, /es/catalogo, /es/comparacion, /es/componentes, /es/diagnostico, /es/frameworks, /es/glosario, /es/selector, /es/acerca
- Dedicated search index at /api/busca/es with Spanish accent normalization
- Strict exclusion of book references in Spanish routes and catalogs
- Language selector and root negotiation with es active (ENABLED_LOCALES = ['pt', 'en', 'es'])
- Extended verification suite: check-i18n, smoke-imagem and verifica-vazamentos

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 19:50:20 -03:00
1 parent 0940814522
commit 660b28c09d
72 files changed
+17848 -947

No files matched your search

+14 -5
View File
@@ -3,15 +3,20 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn ? "ESP32 Board Catalog" : "Catálogo de placas ESP32",
titulo: isEn ? "ESP32 Board Catalog" : isEs ? "Catálogo de placas ESP32" : "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."
: "Placas de desenvolvimento com ESP32 disponíveis no Brasil, com filtro por série, conector e conversor USB-serial.",
caminho: "/catalogo",
locale,
keywords: isEn
? ["ESP32 boards", "ESP32 DevKit", "buy ESP32", "ESP32 DevKitC"]
: isEs
? ["placas ESP32", "DevKit ESP32", "comprar ESP32", "ESP32 DevKitC"]
: ["placas ESP32", "DevKit ESP32", "comprar ESP32", "ESP32 DevKitC"],
});
}
@@ -19,6 +24,8 @@ export async function generateMetadata({ params }) {
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";
return (
<>
@@ -27,11 +34,13 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEn ? "ESP32 Development Board Catalog" : "Catálogo de Placas ESP32",
nome: isEn ? "ESP32 Development Board Catalog" : isEs ? "Catálogo de placas de desarrollo ESP32" : "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."
: "Guia e catálogo interativo de placas de desenvolvimento e DevKits com microcontroladores ESP32.",
caminho: isEn ? "/en/catalog" : "/catalogo",
caminho: canonPath,
locale,
})
),
@@ -42,8 +51,8 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Board Catalog" : "Catálogo de Placas", caminho: isEn ? "/en/catalog" : "/catalogo" },
{ 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 },
])
),
}}
+53 -18
View File
@@ -33,9 +33,35 @@ const VALUE_MAP_EN = {
"Opcional": "Optional",
};
const HEADER_MAP_ES = {
"Nome": "Nombre",
"Descrição": "Descripción",
"Série": "Serie",
"Loja Oficial": "Tienda oficial",
"Fabricante": "Fabricante",
"Processador": "Procesador",
"Memória Flash": "Memoria flash",
"PSRAM": "PSRAM",
"Conector USB": "Conector USB",
"Interface USB": "Interfaz USB",
"Antena": "Antena",
"Bateria": "Batería",
"Display": "Pantalla",
"Sensores": "Sensores",
"Botões": "Botones",
"LEDs": "LEDs",
};
const VALUE_MAP_ES = {
"Sim": "Sí",
"Não": "No",
"Opcional": "Opcional",
};
export default function PlacasPage() {
const pathname = usePathname() || "";
const isEn = pathname === "/en" || pathname.startsWith("/en/");
const isEs = pathname === "/es" || pathname.startsWith("/es/");
const [boards, setBoards] = useState([]);
const [filteredBoards, setFilteredBoards] = useState([]);
@@ -64,7 +90,7 @@ export default function PlacasPage() {
const response = await fetch("/api/placas");
if (!response.ok) {
throw new Error(isEn ? "Failed to load board data" : "Erro ao carregar dados das placas");
throw new Error(isEn ? "Failed to load board data" : isEs ? "Error al cargar los datos de las placas" : "Erro ao carregar dados das placas");
}
const result = await response.json();
@@ -168,13 +194,16 @@ export default function PlacasPage() {
const hero = (
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Board Catalog" : "Catálogo de placas" }]}
isEs={isEs}
trail={[{ label: isEn ? "Board Catalog" : isEs ? "Catálogo de placas" : "Catálogo de placas" }]}
icon={ShoppingCart}
label={isEn ? "Real DevKits and modules" : "DevKits e módulos reais"}
title={isEn ? "ESP32 Board Catalog" : "Catálogo de placas ESP32"}
label={isEn ? "Real DevKits and modules" : isEs ? "DevKits y módulos reales" : "DevKits e módulos reais"}
title={isEn ? "ESP32 Board Catalog" : isEs ? "Catálogo de placas ESP32" : "Catálogo de placas ESP32"}
lede={
isEn
? "Compare official and third-party boards for your project: USB interface, on-board sensors and direct store links."
: isEs
? "Compare placas oficiales y alternativas para su proyecto: interfaz USB, sensores integrados y enlace directo a la tienda."
: "Compare placas oficiais e alternativas para o seu projeto: interface USB, sensores integrados e link direto para a loja."
}
/>
@@ -186,14 +215,14 @@ export default function PlacasPage() {
<Header />
<main id="conteudo">
{hero}
<section className="shell section pt-10" aria-busy="true" aria-label={isEn ? "Loading boards" : "Carregando placas"}>
<section className="shell section pt-10" aria-busy="true" aria-label={isEn ? "Loading boards" : isEs ? "Cargando placas" : "Carregando placas"}>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card h-72 animate-pulse bg-surface-2" />
))}
</div>
<p role="status" className="mt-6 text-center text-sm text-muted">
{isEn ? "Querying boards database…" : "Consultando o banco de placas…"}
{isEn ? "Querying boards database…" : isEs ? "Consultando la base de datos de placas…" : "Consultando o banco de placas…"}
</p>
</section>
</main>
@@ -213,14 +242,14 @@ export default function PlacasPage() {
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-danger" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">
{isEn ? "Could not load boards" : "Não foi possível carregar as placas"}
{isEn ? "Could not load boards" : isEs ? "No se pudieron cargar las placas" : "Não foi possível carregar as placas"}
</p>
<p className="mt-1">
{error}. {isEn ? "Check your connection and try again." : "Verifique a conexão e tente de novo."}
{error}. {isEn ? "Check your connection and try again." : isEs ? "Compruebe la conexión y vuelva a intentarlo." : "Verifique a conexão e tente de novo."}
</p>
<button type="button" onClick={fetchBoards} className="btn btn-primary btn-sm mt-4">
<RefreshCw className="h-4 w-4" aria-hidden="true" />
{isEn ? "Try again" : "Tentar novamente"}
{isEn ? "Try again" : isEs ? "Reintentar" : "Tentar novamente"}
</button>
</div>
</div>
@@ -235,14 +264,14 @@ export default function PlacasPage() {
<div className="space-y-6">
<div>
<label htmlFor="busca-placa" className="text-sm font-semibold text-ink">
{isEn ? "Search board" : "Buscar placa"}
{isEn ? "Search board" : isEs ? "Buscar placa" : "Buscar placa"}
</label>
<div className="relative mt-2">
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-placa"
type="search"
placeholder={isEn ? "Name or description" : "Nome ou descrição"}
placeholder={isEn ? "Name or description" : isEs ? "Nombre o descripción" : "Nome ou descrição"}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="field pl-10 text-sm"
@@ -253,7 +282,7 @@ export default function PlacasPage() {
{filterableColumns.map((column) => {
const filterData = filterOptions[column];
if (!filterData) return null;
const colLabel = isEn ? (HEADER_MAP_EN[column] || column) : column;
const colLabel = isEn ? (HEADER_MAP_EN[column] || column) : isEs ? (HEADER_MAP_ES[column] || column) : column;
if (filterData.isBinary) {
return (
@@ -277,7 +306,7 @@ export default function PlacasPage() {
</legend>
<div className="mt-2 max-h-44 space-y-0.5 overflow-y-auto pr-1">
{filterData.values.map((option) => {
const optLabel = isEn ? (VALUE_MAP_EN[option] || option) : option;
const optLabel = isEn ? (VALUE_MAP_EN[option] || option) : isEs ? (VALUE_MAP_ES[option] || option) : option;
return (
<label key={option} className="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2 text-sm text-muted transition-colors hover:bg-surface-2 hover:text-ink">
<input
@@ -304,14 +333,14 @@ export default function PlacasPage() {
<main id="conteudo">
{hero}
<section className="shell section pt-10 sm:pt-12" aria-label={isEn ? "Boards" : "Placas"}>
<section className="shell section pt-10 sm:pt-12" aria-label={isEn ? "Boards" : isEs ? "Placas" : "Placas"}>
<div className="grid gap-8 lg:grid-cols-[17rem_1fr] lg:gap-10">
<aside className="lg:sticky lg:top-28 lg:self-start">
<details className="panel" open={filtrosAbertos} onToggle={(e) => setFiltrosAbertos(e.currentTarget.open)}>
<summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 px-5 py-3 lg:cursor-default">
<span className="flex items-center gap-2 text-sm font-semibold text-ink">
<SlidersHorizontal className="h-4 w-4 text-accent" aria-hidden="true" />
{isEn ? "Filters" : "Filtros"}
{isEn ? "Filters" : isEs ? "Filtros" : "Filtros"}
{activeFilterCount > 0 && <span className="chip chip-accent px-2 py-0">{activeFilterCount}</span>}
</span>
{activeFilterCount > 0 && (
@@ -323,7 +352,7 @@ export default function PlacasPage() {
}}
className="text-sm font-medium text-accent hover:text-accent-strong"
>
{isEn ? "Clear" : "Limpar"}
{isEn ? "Clear" : isEs ? "Limpiar" : "Limpar"}
</button>
)}
</summary>
@@ -338,6 +367,10 @@ export default function PlacasPage() {
? filteredBoards.length === 1
? "board found"
: "boards found"
: isEs
? filteredBoards.length === 1
? "placa encontrada"
: "placas encontradas"
: filteredBoards.length === 1
? "placa encontrada"
: "placas encontradas"}
@@ -347,15 +380,17 @@ export default function PlacasPage() {
<div className="card mt-4 px-6 py-14 text-center">
<Search className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h2 className="mt-4 font-display text-xl font-semibold text-ink">
{isEn ? "No boards match these filters" : "Nenhuma placa com esses filtros"}
{isEn ? "No boards match these filters" : isEs ? "Ninguna placa coincide con estos filtros" : "Nenhuma placa com esses filtros"}
</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEn
? "Remove some filters or search by another name."
: isEs
? "Elimine algunos filtros o busque por otro nombre."
: "Remova alguns filtros ou busque por outro nome."}
</p>
<button type="button" onClick={clearFilters} className="btn btn-primary mt-6">
{isEn ? "Clear filters" : "Limpar filtros"}
{isEn ? "Clear filters" : isEs ? "Limpiar filtros" : "Limpar filtros"}
</button>
</div>
) : (
+30 -6
View File
@@ -3,15 +3,20 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn ? "Compare ESP32 Series: Side-by-Side Table" : "Comparar séries ESP32",
titulo: isEn ? "Compare ESP32 Series: Side-by-Side Table" : isEs ? "Comparar series ESP32: tabla comparativa" : "Comparar séries ESP32",
descricao: isEn
? "Compare up to four ESP32 series side by side: processor, Wi-Fi, Bluetooth, memory, GPIO, peripherals, and hardware security, with official datasheet data."
: isEs
? "Compare hasta cuatro series de ESP32 frente a frente: procesador, Wi-Fi, Bluetooth, memoria, GPIO, periféricos y seguridad en hardware, con datos de hojas de datos oficiales."
: "Compare até quatro séries de ESP32 lado a lado: processador, Wi-Fi, Bluetooth, memória, GPIO, periféricos e segurança em hardware, com os dados dos datasheets oficiais.",
caminho: "/comparacao",
locale,
keywords: isEn
? ["compare ESP32", "ESP32 vs ESP32-S3", "ESP32 comparison table", "which ESP32 is best"]
: isEs
? ["comparar ESP32", "ESP32 vs ESP32-S3", "tabla comparativa ESP32", "cuál ESP32 es mejor"]
: ["comparar ESP32", "ESP32 vs ESP32-S3", "tabela comparativa ESP32", "qual ESP32 é melhor"],
});
}
@@ -46,10 +51,27 @@ const faqsEn = [
},
];
const faqsEs = [
{
pergunta: "¿Cómo comparar microcontroladores ESP32 frente a frente?",
resposta: "El comparador de ESPDocs le permite seleccionar hasta cuatro series simultáneamente para cotejar arquitectura, frecuencia de reloj, memoria SRAM/PSRAM, estándares de radio (Wi-Fi 4/6, Bluetooth 5/6, 802.15.4), interfaces de periféricos y bloques de seguridad en hardware.",
},
{
pergunta: "¿Cuál es la diferencia entre el ESP32 clásico y el ESP32-S3?",
resposta: "El ESP32 clásico utiliza núcleos Xtensa LX6 y Bluetooth clásico/LE. El ESP32-S3 incorpora núcleos Xtensa LX7 con instrucciones vectoriales para inteligencia artificial (TinyML), USB nativo OTG, más GPIOs (45 frente a 36) y Bluetooth 5.0 LE.",
},
{
pergunta: "¿Cuál es la diferencia entre ESP32-C3 y ESP32-C6?",
resposta: "El ESP32-C3 es un microcontrolador RISC-V con Wi-Fi 4 y Bluetooth 5 LE. El ESP32-C6 evoluciona a Wi-Fi 6 (802.11ax), Bluetooth 5.4 LE, radio IEEE 802.15.4 para Zigbee 3.0 y Thread 1.3, además de un coprocesador de consumo ultrabajo.",
},
];
export default async function Layout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const faqs = isEn ? faqsEn : faqsPt;
const isEs = locale === "es";
const faqs = isEn ? faqsEn : isEs ? faqsEs : faqsPt;
const canonPath = isEn ? "/en/compare" : isEs ? "/es/comparacion" : "/comparacao";
return (
<>
@@ -58,11 +80,13 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEn ? "ESP32 Series Comparator" : "Comparador de Séries ESP32",
nome: isEn ? "ESP32 Series Comparator" : isEs ? "Comparador de series ESP32" : "Comparador de Séries ESP32",
descricao: isEn
? "Technical comparison table to analyze specifications, pinouts, and features of multiple ESP32 series."
: isEs
? "Tabla comparativa técnica para analizar especificaciones, patillaje y características de múltiples series de ESP32."
: "Tabela comparativa técnica para analisar especificações, pinagens e recursos de múltiplas séries de ESP32.",
caminho: isEn ? "/en/compare" : "/comparacao",
caminho: canonPath,
locale,
})
),
@@ -73,8 +97,8 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Compare Series" : "Comparar Séries", caminho: isEn ? "/en/compare" : "/comparacao" },
{ nome: isEn ? "Home" : isEs ? "Inicio" : "Início", caminho: isEn ? "/en" : isEs ? "/es" : "/" },
{ nome: isEn ? "Compare Series" : isEs ? "Comparar series" : "Comparar Séries", caminho: canonPath },
])
),
}}
+121 -26
View File
@@ -8,6 +8,7 @@ import { usePathname } from "next/navigation";
import Link from "next/link";
import seriesDataPt from "@/public/series.json";
import seriesDataEn from "@/data/en/series.json";
import seriesDataEs from "@/data/es/series.json";
import { getLocalizedHref } from "@/i18n/links";
import { ArrowRight, ArrowUpRight, Check, ChevronDown, Scale } from "lucide-react";
@@ -177,18 +178,98 @@ const COMPARE_FIELDS_EN = [
]},
];
const COMPARE_FIELDS_ES = [
{ category: "General", fields: [
{ key: "nome_completo", label: "Nombre completo" },
{ key: "descricao", label: "Descripción" },
]},
{ category: "Disponibilidad y herramientas", fields: [
{ key: "esp_idf_minimo", label: "ESP-IDF mínimo" },
{ key: "arduino_core", label: "Core Arduino" },
{ key: "depuracao", label: "Depuración / JTAG" },
{ key: "encapsulamento", label: "Encapsulado" },
{ key: "modulos", label: "Módulos disponibles" },
]},
{ category: "Procesador", fields: [
{ key: "arquitetura", label: "Arquitectura" },
{ key: "nucleos", label: "Núcleos" },
{ key: "frequencia", label: "Frecuencia" },
{ key: "coprocessador_ulp", label: "Coprocesador ULP" },
{ key: "aceleradores_ia", label: "Aceleradores de IA" },
]},
{ category: "Conectividad", fields: [
{ key: "wifi", label: "Wi-Fi" },
{ key: "bluetooth", label: "Bluetooth" },
{ key: "zigbee_thread", label: "Zigbee/Thread" },
{ key: "matter", label: "Matter" },
{ key: "ethernet", label: "Ethernet" },
]},
{ category: "Memoria", fields: [
{ key: "memoria_sram", label: "SRAM" },
{ key: "memoria_sram_rtc", label: "SRAM RTC" },
{ key: "memoria_rom", label: "ROM" },
{ key: "flash_externa", label: "Flash externa" },
{ key: "psram_externa", label: "PSRAM externa" },
]},
{ category: "Periféricos digitales", fields: [
{ key: "gpio", label: "GPIO" },
{ key: "uart", label: "UART" },
{ key: "spi", label: "SPI" },
{ key: "i2c", label: "I²C" },
{ key: "i2s", label: "I²S" },
{ key: "pwm", label: "PWM" },
{ key: "can", label: "CAN" },
{ key: "usb", label: "USB" },
{ key: "depuracao", label: "Depuración / JTAG" },
{ key: "sdio", label: "SDIO" },
]},
{ category: "Periféricos analógicos", fields: [
{ key: "adc", label: "ADC" },
{ key: "dac", label: "DAC" },
{ key: "touch", label: "Touch capacitivo" },
{ key: "sensor_hall", label: "Sensor Hall" },
{ key: "sensor_temperatura", label: "Sensor de temperatura" },
]},
{ category: "Seguridad", fields: [
{ key: "seguranca.aes", label: "AES" },
{ key: "seguranca.sha", label: "SHA" },
{ key: "seguranca.rsa", label: "RSA / MPI" },
{ key: "seguranca.ecc", label: "ECC (curvas elípticas)" },
{ key: "seguranca.ecdsa", label: "ECDSA en hardware" },
{ key: "seguranca.hmac", label: "HMAC" },
{ key: "seguranca.assinatura_digital", label: "Firma digital (DS)" },
{ key: "seguranca.key_manager", label: "Key Manager" },
{ key: "seguranca.rng", label: "Generador aleatorio (TRNG)" },
{ key: "seguranca.criptografia_flash", label: "Cifrado de flash" },
{ key: "seguranca.secure_boot", label: "Secure Boot" },
{ key: "seguranca.protecao_dpa", label: "Protección contra DPA" },
]},
{ category: "Interfaces especiales", fields: [
{ key: "lcd", label: "LCD" },
{ key: "camera", label: "Cámara" },
{ key: "mipi_csi", label: "MIPI CSI" },
{ key: "mipi_dsi", label: "MIPI DSI" },
{ key: "ethernet_mac", label: "Ethernet MAC" },
]},
{ category: "Especificaciones ambientales", fields: [
{ key: "temperatura_operacao", label: "Temp. operación" },
{ key: "temperatura_armazenamento", label: "Temp. almacenamiento" },
]},
];
export default function Comparacao() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const currentLocale = isEn ? "en" : "pt";
const seriesData = isEn ? seriesDataEn : seriesDataPt;
const compareFields = isEn ? COMPARE_FIELDS_EN : COMPARE_FIELDS_PT;
const isEs = pathname?.startsWith("/es");
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const seriesData = isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
const compareFields = isEs ? COMPARE_FIELDS_ES : isEn ? COMPARE_FIELDS_EN : COMPARE_FIELDS_PT;
const [selectedSeries, setSelectedSeries] = useState(["ESP32", "ESP32-S3"]);
const [soDiferencas, setSoDiferencas] = useState(false);
const [collapsedCategories, setCollapsedCategories] = useState(() => ({
[isEn ? "Special Interfaces" : "Interfaces Especiais"]: true,
[isEn ? "Environmental Specifications" : "Especificações Ambientais"]: true,
[isEs ? "Interfaces especiales" : isEn ? "Special Interfaces" : "Interfaces Especiais"]: true,
[isEs ? "Especificaciones ambientales" : isEn ? "Environmental Specifications" : "Especificações Ambientais"]: true,
}));
const series = Object.entries(seriesData);
@@ -214,19 +295,19 @@ export default function Comparacao() {
};
const renderValue = (value) => {
if (!value || value === "Não" || value === "No") return <span className="text-muted">{isEn ? "No" : "Não"}</span>;
if (value === "Sim" || value === "Yes") {
if (!value || value === "Não" || value === "No") return <span className="text-muted">{isEs || isEn ? "No" : "Não"}</span>;
if (value === "Sim" || value === "Yes" || value === "Sí") {
return (
<span className="inline-flex items-center gap-1.5 font-medium text-ok">
<Check className="h-4 w-4" aria-hidden="true" />
{isEn ? "Yes" : "Sim"}
{isEs ? "Sí" : isEn ? "Yes" : "Sim"}
</span>
);
}
return <span className="font-medium text-ink">{value}</span>;
};
const valores = (key) => selectedSeries.map((k) => String(valorDe(seriesData[k], key) ?? (isEn ? "No" : "Não")));
const valores = (key) => selectedSeries.map((k) => String(valorDe(seriesData[k], key) ?? (isEs || isEn ? "No" : "Não")));
const diferem = (key) => new Set(valores(key)).size > 1;
return (
@@ -236,12 +317,15 @@ export default function Comparacao() {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Comparator" : "Comparador" }]}
isEs={isEs}
trail={[{ label: isEs ? "Comparador" : isEn ? "Comparator" : "Comparador" }]}
icon={Scale}
label={isEn ? "Up to four series side by side" : "Até quatro séries lado a lado"}
title={isEn ? "ESP32 Series Comparator" : "Comparador de séries"}
label={isEs ? "Hasta cuatro series lado a lado" : isEn ? "Up to four series side by side" : "Até quatro séries lado a lado"}
title={isEs ? "Comparador de series ESP32" : isEn ? "ESP32 Series Comparator" : "Comparador de séries"}
lede={
isEn
isEs
? "Ponga núcleos, radio, memoria, periféricos y seguridad en la misma tabla y vea lo que realmente cambia de un chip a otro."
: isEn
? "Put cores, radio, memory, peripherals, and security in the same table and see what really changes from chip to chip."
: "Coloque núcleos, rádio, memória, periféricos e segurança na mesma tabela e veja o que realmente muda de um chip para outro."
}
@@ -252,14 +336,21 @@ export default function Comparacao() {
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 id="escolha-titulo" className="text-base font-semibold text-ink">
{isEn ? (
{isEs ? (
<>Elija de {MIN_SERIES} a {MAX_SERIES} series</>
) : isEn ? (
<>Choose {MIN_SERIES} to {MAX_SERIES} series</>
) : (
<>Escolha de {MIN_SERIES} a {MAX_SERIES} séries</>
)}
</h2>
<p role="status" aria-live="polite" className="mt-1 text-sm text-muted">
{isEn ? (
{isEs ? (
<>
{selectedSeries.length} de {MAX_SERIES} seleccionadas
{selectedSeries.length === MAX_SERIES && ". Elimine una para cambiar."}
</>
) : isEn ? (
<>
{selectedSeries.length} of {MAX_SERIES} selected
{selectedSeries.length === MAX_SERIES && ". Remove one to switch."}
@@ -298,7 +389,7 @@ export default function Comparacao() {
<section className="shell mt-8" aria-labelledby="tabela-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="tabela-titulo" className="text-base font-semibold text-ink">
{isEn ? "Specifications" : "Especificações"}
{isEs ? "Especificaciones" : isEn ? "Specifications" : "Especificações"}
</h2>
<label className="flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-medium text-ink-soft">
<input
@@ -307,7 +398,7 @@ export default function Comparacao() {
onChange={(e) => setSoDiferencas(e.target.checked)}
className="h-5 w-5 cursor-pointer accent-[var(--accent)]"
/>
{isEn ? "Show differences only" : "Mostrar só as diferenças"}
{isEs ? "Mostrar solo diferencias" : isEn ? "Show differences only" : "Mostrar só as diferenças"}
</label>
</div>
@@ -315,7 +406,9 @@ export default function Comparacao() {
<div className="overflow-x-auto">
<table className="w-full min-w-[30rem] border-collapse text-sm">
<caption className="sr-only">
{isEn ? (
{isEs ? (
<>Comparación de las series {selectedSeries.join(", ")}</>
) : isEn ? (
<>Comparison of series {selectedSeries.join(", ")}</>
) : (
<>Comparação das séries {selectedSeries.join(", ")}</>
@@ -324,7 +417,7 @@ export default function Comparacao() {
<thead>
<tr className="border-b border-line bg-surface-2">
<th scope="col" className="sticky left-0 z-20 w-28 bg-surface-2 px-4 py-4 text-left font-semibold text-muted sm:w-52 sm:px-6">
{isEn ? "Specification" : "Especificação"}
{isEs ? "Especificación" : isEn ? "Specification" : "Especificação"}
</th>
{selectedSeries.map((seriesKey) => (
<th key={seriesKey} scope="col" className="min-w-40 px-4 py-4 text-left sm:px-6">
@@ -381,7 +474,7 @@ export default function Comparacao() {
<section className="shell section" aria-labelledby="docs-titulo">
<h2 id="docs-titulo" className="section-title">
{isEn ? "Official documentation" : "Documentação oficial"}
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : "Documentação oficial"}
</h2>
<div className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{selectedSeries.map((seriesKey) => {
@@ -393,13 +486,13 @@ export default function Comparacao() {
</Link>
{seriesItem.datasheet ? (
<a href={seriesItem.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
{isEn ? "Official datasheet" : "Datasheet oficial"}
{isEs ? "Datasheet oficial" : isEn ? "Official datasheet" : "Datasheet oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> {isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
</a>
) : (
<span className="text-sm text-muted">
{isEn ? "Datasheet not yet published" : "Datasheet ainda não publicado"}
{isEs ? "Datasheet aún no publicado" : isEn ? "Datasheet not yet published" : "Datasheet ainda não publicado"}
</span>
)}
</div>
@@ -410,16 +503,18 @@ export default function Comparacao() {
<div className="panel mt-10 flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">
{isEn ? "Want to test on the bench?" : "Quer testar na bancada?"}
{isEs ? "¿Desea probar en banco?" : isEn ? "Want to test on the bench?" : "Quer testar na bancada?"}
</h2>
<p className="mt-3 text-base text-muted">
{isEn
{isEs
? "Vea las placas de desarrollo con patillaje, periféricos y enlace a la tienda oficial."
: isEn
? "Explore development boards with pinouts, peripherals, and official store links."
: "Veja as placas de desenvolvimento com pinagem, periféricos e link para a loja."}
</p>
</div>
<Link href={getLocalizedHref("/catalogo", currentLocale)} className="btn btn-primary shrink-0">
{isEn ? "View boards catalog" : "Ver catálogo de placas"}
{isEs ? "Ver catálogo de placas" : isEn ? "View boards catalog" : "Ver catálogo de placas"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
+52 -9
View File
@@ -3,16 +3,33 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn
titulo: isEs
? "ESP Component Registry & IDF Component Manager · ESPDocs"
: isEn
? "ESP Component Registry & IDF Component Manager · ESPDocs"
: "ESP Component Registry & IDF Component Manager · ESPDocs",
descricao: isEn
descricao: isEs
? "Catálogo interactivo del ESP Component Registry: controladores de pantalla, LVGL, reconocimiento de voz ESP-SR, RainMaker, BSPs y generador de idf_component.yml."
: isEn
? "Official interactive catalog of the ESP Component Registry: display drivers, LVGL, ESP-SR voice recognition, RainMaker, BSPs, and idf_component.yml generator."
: "Catálogo oficial e interativo do ESP Component Registry: drivers de displays, LVGL, reconhecimento de voz ESP-SR, RainMaker, BSPs e gerador de idf_component.yml.",
caminho: "/componentes",
locale,
keywords: isEn
keywords: isEs
? [
"ESP Component Registry",
"IDF Component Manager",
"idf_component.yml",
"idf.py add-dependency",
"componentes ESP32",
"espressif esp_lvgl_port",
"espressif esp_rainmaker",
"espressif esp-sr",
"bibliotecas ESP-IDF",
]
: isEn
? [
"ESP Component Registry",
"IDF Component Manager",
@@ -76,10 +93,30 @@ const faqsEn = [
},
];
const faqsEs = [
{
pergunta: "¿Qué es el ESP Component Registry y el IDF Component Manager?",
resposta: "El ESP Component Registry (components.espressif.com) es el repositorio oficial de paquetes modulares de Espressif. El IDF Component Manager es la herramienta integrada en ESP-IDF (a partir de v4.4 y estándar en v5.x+) que descarga, compila y resuelve versiones de dependencias automáticamente desde el manifiesto idf_component.yml.",
},
{
pergunta: "¿Cómo agregar un componente a mi proyecto ESP-IDF?",
resposta: "Puede ejecutar el comando 'idf.py add-dependency \"namespace/componente^version\"' en la terminal del proyecto (por ejemplo: idf.py add-dependency \"espressif/esp_lvgl_port^2.4.0\") o editar directamente el archivo main/idf_component.yml.",
},
{
pergunta: "¿Cuál es la diferencia entre usar Component Registry y submódulos git?",
resposta: "El Component Registry elimina la complejidad de gestionar submódulos Git pesados y versiones manuales. Ofrece versionamiento semántico (^ y ~), resolución automática de dependencias transitivas y compilación modular bajo demanda.",
},
{
pergunta: "¿Puedo crear y publicar mis propios componentes?",
resposta: "Sí, cualquier desarrollador o empresa puede publicar componentes públicos o privados en el ESP Component Registry utilizando la herramienta oficial Compote CLI (pip install idf-component-manager).",
},
];
export default async function ComponentesLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const faqs = isEn ? faqsEn : faqsPt;
const isEs = locale === "es";
const faqs = isEs ? faqsEs : isEn ? faqsEn : faqsPt;
return (
<>
@@ -88,8 +125,8 @@ export default async function ComponentesLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: "ESP Component Registry", caminho: isEn ? "/en/components" : "/componentes" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: "ESP Component Registry", caminho: isEs ? "/es/componentes" : isEn ? "/en/components" : "/componentes" },
])
),
}}
@@ -105,11 +142,17 @@ export default async function ComponentesLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEn ? "ESP Component Registry Hub & Manifest Builder" : "Hub do ESP Component Registry & Construtor de Manifesto",
descricao: isEn
nome: isEs
? "Hub de ESP Component Registry & Generador de Manifiesto"
: isEn
? "ESP Component Registry Hub & Manifest Builder"
: "Hub do ESP Component Registry & Construtor de Manifesto",
descricao: isEs
? "Catálogo curado y generador de manifiesto idf_component.yml para componentes modulares de ESP-IDF."
: isEn
? "Curated catalog and idf_component.yml manifest generator for modular ESP-IDF components."
: "Catálogo curado e gerador de manifesto idf_component.yml para componentes modulares do ESP-IDF.",
caminho: isEn ? "/en/components" : "/componentes",
caminho: isEs ? "/es/componentes" : isEn ? "/en/components" : "/componentes",
locale,
})
),
+63 -41
View File
@@ -8,6 +8,7 @@ import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import * as componentesPt from "@/lib/componentRegistryData";
import * as componentesEn from "@/lib/data/en/componentes";
import * as componentesEs from "@/lib/data/es/componentes";
import {
ArrowUpRight,
Box,
@@ -44,7 +45,8 @@ const ICONES_CATEGORIA = {
export default function ComponentRegistryPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const { CATEGORIAS_COMPONENTES, COMPONENTES_REGISTRY, GUIA_COMPONENT_MANAGER } = isEn ? componentesEn : componentesPt;
const isEs = pathname?.startsWith("/es");
const { CATEGORIAS_COMPONENTES, COMPONENTES_REGISTRY, GUIA_COMPONENT_MANAGER } = isEs ? componentesEs : isEn ? componentesEn : componentesPt;
const [busca, setBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
@@ -98,19 +100,23 @@ export default function ComponentRegistryPage() {
);
if (selecionados.length === 0) {
return isEn
return isEs
? `# No hay componentes seleccionados.\n# Haga clic en "+ Agregar al manifiesto" en las tarjetas de abajo.`
: isEn
? `# No components selected.\n# Click "+ Add to Manifest" on the cards below.`
: `# Nenhum componente selecionado.\n# Clique em "+ Adicionar ao Manifesto" nos cards abaixo.`;
}
let yaml = isEn
let yaml = isEs
? `# main/idf_component.yml\n# Generado mediante ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/es/componentes)\n\ndependencies:\n`
: isEn
? `# main/idf_component.yml\n# Generated via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/en/components)\n\ndependencies:\n`
: `# main/idf_component.yml\n# Gerado via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/componentes)\n\ndependencies:\n`;
selecionados.forEach((c) => {
yaml += ` ${c.namespace}/${c.name}: "${c.versao}"\n`;
});
return yaml;
}, [componentesSelecionados, COMPONENTES_REGISTRY, isEn]);
}, [componentesSelecionados, COMPONENTES_REGISTRY, isEn, isEs]);
const copiarManifesto = () => {
navigator.clipboard.writeText(manifestoYaml);
@@ -145,31 +151,34 @@ export default function ComponentRegistryPage() {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Component Registry" : "Component Registry" }]}
isEs={isEs}
trail={[{ label: isEs ? "Component Registry" : isEn ? "Component Registry" : "Component Registry" }]}
icon={Box}
label="ESP-IDF Component Manager"
title="ESP Component Registry"
lede={
isEn
isEs
? "El catálogo oficial de paquetes reutilizables de Espressif para ESP-IDF. Elija controladores, voz y pilas industriales, copie el comando y genere el manifiesto del proyecto."
: isEn
? "Official Espressif catalog of reusable packages for ESP-IDF. Choose drivers, voice stacks, and industrial protocols, copy commands, and generate project manifests."
: "O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF. Escolha drivers, voz e pilhas industriais, copie o comando e gere o manifesto do projeto."
}
>
<a href="https://components.espressif.com" target="_blank" rel="noopener noreferrer" className="btn btn-primary">
{isEn ? "Open official registry" : "Abrir o registro oficial"}
{isEs ? "Abrir el registro oficial" : isEn ? "Open official registry" : "Abrir o registro oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> {isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
</a>
<a href="#manifesto-builder" className="btn btn-ghost">
{isEn ? "Generate idf_component.yml" : "Gerar idf_component.yml"}
{isEs ? "Generar idf_component.yml" : isEn ? "Generate idf_component.yml" : "Gerar idf_component.yml"}
</a>
</PageHero>
<section className="shell pt-10 sm:pt-12" aria-label={isEn ? "Search and filters" : "Busca e filtros"}>
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Búsqueda y filtros" : isEn ? "Search and filters" : "Busca e filtros"}>
<div className="panel p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-componente" className="sr-only">
{isEn ? "Search by name, namespace, or tag" : "Buscar por nome, namespace ou tag"}
{isEs ? "Buscar por nombre, namespace o etiqueta" : isEn ? "Search by name, namespace, or tag" : "Buscar por nome, namespace ou tag"}
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
@@ -177,14 +186,14 @@ export default function ComponentRegistryPage() {
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder={isEn ? "Search by name, namespace, or tag" : "Buscar por nome, namespace ou tag"}
placeholder={isEs ? "Buscar por nombre, namespace o etiqueta" : isEn ? "Search by name, namespace, or tag" : "Buscar por nome, namespace ou tag"}
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
</div>
<div className="mt-5">
<h2 className="text-sm font-semibold text-ink">{isEn ? "Category" : "Categoria"}</h2>
<h2 className="text-sm font-semibold text-ink">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</h2>
<div className="mt-3 flex flex-wrap gap-2">
{CATEGORIAS_COMPONENTES.map((cat) => {
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
@@ -192,7 +201,7 @@ export default function ComponentRegistryPage() {
return (
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
<Icone className="h-4 w-4" aria-hidden="true" />
{cat.id === "todos" ? (isEn ? "All" : "Todos") : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
{cat.id === "todos" ? (isEs ? "Todos" : isEn ? "All" : "Todos") : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
<span className="tabular-nums opacity-80">{contagem}</span>
</button>
);
@@ -201,11 +210,11 @@ export default function ComponentRegistryPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">{isEn ? "Compatible chip" : "Chip compatível"}</h2>
<h2 className="text-sm font-semibold text-ink">{isEs ? "Chip compatible" : isEn ? "Compatible chip" : "Chip compatível"}</h2>
<div className="mt-3 flex flex-wrap gap-2">
{chipsDisponiveis.map((chip) => (
<button key={chip} type="button" onClick={() => setChipFiltro(chip)} aria-pressed={chipFiltro === chip} className="chip-btn">
{chip === "todos" ? (isEn ? "All" : "Todos") : chip}
{chip === "todos" ? (isEs ? "Todos" : isEn ? "All" : "Todos") : chip}
</button>
))}
</div>
@@ -216,17 +225,17 @@ export default function ComponentRegistryPage() {
<section className="shell section pt-8 sm:pt-10" aria-labelledby="lista-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="lista-titulo" role="status" aria-live="polite" className="text-base font-semibold text-ink">
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? (isEn ? "component" : "componente") : (isEn ? "components" : "componentes")}
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? (isEs ? "componente" : isEn ? "component" : "componente") : (isEs ? "componentes" : isEn ? "components" : "componentes")}
</h2>
<div className="flex flex-wrap items-center gap-x-5 gap-y-1">
{totalFiltros > 0 && (
<button type="button" onClick={limparFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<X className="h-4 w-4" aria-hidden="true" />
{isEn ? "Clear filters" : "Limpar filtros"}
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
</button>
)}
<a href="#manifesto-builder" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{isEn ? "Manifest:" : "Manifesto:"} {componentesSelecionados.length} {componentesSelecionados.length === 1 ? (isEn ? "component" : "componente") : (isEn ? "components" : "componentes")}
{isEs ? "Manifiesto:" : isEn ? "Manifest:" : "Manifesto:"} {componentesSelecionados.length} {componentesSelecionados.length === 1 ? (isEs ? "componente" : isEn ? "component" : "componente") : (isEs ? "componentes" : isEn ? "components" : "componentes")}
</a>
</div>
</div>
@@ -235,13 +244,17 @@ export default function ComponentRegistryPage() {
<div className="card mt-6 px-6 py-14 text-center">
<Box className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
{isEn ? "No components found" : "Nenhum componente encontrado"}
{isEs ? "No se encontraron componentes" : isEn ? "No components found" : "Nenhum componente encontrado"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEn ? "Search for a more general term or return to all categories." : "Busque por um termo mais geral ou volte para todas as categorias."}
{isEs
? "Busque por un término más general o regrese a todas las categorías."
: isEn
? "Search for a more general term or return to all categories."
: "Busque por um termo mais geral ou volte para todas as categorias."}
</p>
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">
{isEn ? "Clear filters" : "Limpar filtros"}
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
</button>
</div>
) : (
@@ -261,7 +274,7 @@ export default function ComponentRegistryPage() {
{comp.oficial && (
<span className="chip chip-accent px-2 py-0">
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
{isEn ? "Official" : "Oficial"}
{isEs ? "Oficial" : isEn ? "Official" : "Oficial"}
</span>
)}
</p>
@@ -285,12 +298,12 @@ export default function ComponentRegistryPage() {
type="button"
onClick={() => copiarComando(comp.id, cliCmd)}
className="code-panel flex w-full min-h-11 items-center justify-between gap-3 rounded-lg px-3 py-2 text-left font-mono text-xs transition-colors hover:border-white/30"
aria-label={isEn ? `Copy command to add ${comp.name}` : `Copiar comando para adicionar ${comp.name}`}
aria-label={isEs ? `Copiar comando para agregar ${comp.name}` : isEn ? `Copy command to add ${comp.name}` : `Copiar comando para adicionar ${comp.name}`}
>
<span className="truncate text-[#e9eef4]">{cliCmd}</span>
{foiCopiado ? <Check className="h-4 w-4 shrink-0 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4 shrink-0 text-[#a6afbd]" aria-hidden="true" />}
</button>
<p role="status" className="sr-only">{foiCopiado ? (isEn ? "Command copied" : "Comando copiado") : ""}</p>
<p role="status" className="sr-only">{foiCopiado ? (isEs ? "Comando copiado" : isEn ? "Command copied" : "Comando copiado") : ""}</p>
<p className="mt-3 text-xs text-muted">
<span className="font-medium text-ink-soft">Chips: </span>
@@ -306,14 +319,14 @@ export default function ComponentRegistryPage() {
className={`btn btn-sm ${selecionado ? "btn-primary" : "btn-ghost"}`}
>
{selecionado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Plus className="h-4 w-4" aria-hidden="true" />}
{selecionado ? (isEn ? "In manifest" : "No manifesto") : (isEn ? "Add to manifest" : "Adicionar ao manifesto")}
{selecionado ? (isEs ? "En manifiesto" : isEn ? "In manifest" : "No manifesto") : (isEs ? "Agregar al manifiesto" : isEn ? "Add to manifest" : "Adicionar ao manifesto")}
</button>
<a
href={comp.urlRegistry}
target="_blank"
rel="noopener noreferrer"
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
aria-label={isEn ? `Open ${comp.name} in official registry (new tab)` : `Abrir ${comp.name} no registro oficial (nova aba)`}
aria-label={isEs ? `Abrir ${comp.name} en el registro oficial (nueva pestaña)` : isEn ? `Open ${comp.name} in official registry (new tab)` : `Abrir ${comp.name} no registro oficial (nova aba)`}
>
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
@@ -329,12 +342,14 @@ export default function ComponentRegistryPage() {
<div className="panel p-6 sm:p-9">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div className="max-w-2xl">
<p className="eyebrow">{isEn ? "Manifest generator" : "Gerador de manifesto"}</p>
<p className="eyebrow">{isEs ? "Generador de manifiesto" : isEn ? "Manifest generator" : "Gerador de manifesto"}</p>
<h2 id="manifesto-titulo" className="section-title mt-3">
{isEn ? "Your idf_component.yml" : "Seu idf_component.yml"}
{isEs ? "Su idf_component.yml" : isEn ? "Your idf_component.yml" : "Seu idf_component.yml"}
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
{isEn ? (
{isEs ? (
<>Guarde el archivo en la carpeta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> del proyecto y ESP-IDF descargará todas las bibliotecas durante la compilación.</>
) : isEn ? (
<>Save this file in your project&apos;s <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> folder and ESP-IDF will download all dependencies during build.</>
) : (
<>Salve o arquivo na pasta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> do projeto e o ESP-IDF baixa todas as bibliotecas na compilação.</>
@@ -344,27 +359,27 @@ export default function ComponentRegistryPage() {
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={copiarManifesto} className="btn btn-primary btn-sm">
{manifestoCopiado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
<span role="status">{manifestoCopiado ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy YAML" : "Copiar YAML")}</span>
<span role="status">{manifestoCopiado ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar YAML" : isEn ? "Copy YAML" : "Copiar YAML")}</span>
</button>
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
<Download className="h-4 w-4" aria-hidden="true" />
{isEn ? "Download .yml" : "Baixar .yml"}
{isEs ? "Descargar .yml" : isEn ? "Download .yml" : "Baixar .yml"}
</button>
{componentesSelecionados.length > 0 && (
<button type="button" onClick={() => setComponentesSelecionados([])} className="btn btn-ghost btn-sm">
<Trash2 className="h-4 w-4" aria-hidden="true" />
{isEn ? "Clear" : "Limpar"}
{isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
</button>
)}
</div>
</div>
<div className="code-panel mt-6">
<pre tabIndex={0} aria-label={isEn ? "Content of idf_component.yml" : "Conteúdo do idf_component.yml"} className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
<pre tabIndex={0} aria-label={isEs ? "Contenido de idf_component.yml" : isEn ? "Content of idf_component.yml" : "Conteúdo do idf_component.yml"} className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
</div>
{componentesSelecionados.length > 0 && (
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label={isEn ? "Included components" : "Componentes incluídos"}>
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label={isEs ? "Componentes incluidos" : isEn ? "Included components" : "Componentes incluídos"}>
{componentesSelecionados.map((id) => {
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
if (!c) return null;
@@ -375,7 +390,7 @@ export default function ComponentRegistryPage() {
type="button"
onClick={() => toggleSelecaoManifesto(id)}
className="-my-2 grid h-11 w-11 place-items-center rounded-full hover:bg-accent/15"
aria-label={isEn ? `Remove ${c.name} from manifest` : `Remover ${c.name} do manifesto`}
aria-label={isEs ? `Eliminar ${c.name} del manifiesto` : isEn ? `Remove ${c.name} from manifest` : `Remover ${c.name} do manifesto`}
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>
@@ -388,7 +403,7 @@ export default function ComponentRegistryPage() {
</section>
<section className="shell section border-t border-line pt-14" aria-labelledby="guia-titulo">
<SectionHead id="guia-titulo" eyebrow={isEn ? "Quick guide" : "Guia rápido"} title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
<SectionHead id="guia-titulo" eyebrow={isEs ? "Guía rápida" : isEn ? "Quick guide" : "Guia rápido"} title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
<ul className="mt-8 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{GUIA_COMPONENT_MANAGER.vantagens.map((v) => (
@@ -401,7 +416,7 @@ export default function ComponentRegistryPage() {
<h3 className="mt-12 flex items-center gap-2 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<Terminal className="h-5 w-5 text-accent" aria-hidden="true" />
{isEn ? "Essential commands" : "Comandos essenciais"}
{isEs ? "Comandos esenciales" : isEn ? "Essential commands" : "Comandos essenciais"}
</h3>
<ul className="mt-4 space-y-3">
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
@@ -416,7 +431,7 @@ export default function ComponentRegistryPage() {
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 self-start rounded-full border border-white/15 px-4 text-sm font-medium text-[#e9eef4] transition-colors hover:bg-white/10 md:self-auto"
>
{copiadoId === `cmd-${i}` ? <Check className="h-4 w-4 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `cmd-${i}` ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy" : "Copiar")}
{copiadoId === `cmd-${i}` ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar" : isEn ? "Copy" : "Copiar")}
</button>
</li>
))}
@@ -425,10 +440,17 @@ export default function ComponentRegistryPage() {
<div className="callout mt-8">
<h3 className="flex items-center gap-2 font-semibold text-ink">
<HelpCircle className="h-4 w-4 text-accent" aria-hidden="true" />
{isEn ? "Semantic versioning in ESP-IDF" : "Versionamento semântico no ESP-IDF"}
{isEs ? "Versionamiento semántico en ESP-IDF" : isEn ? "Semantic versioning in ESP-IDF" : "Versionamento semântico no ESP-IDF"}
</h3>
<ul className="mt-2 list-disc space-y-1.5 pl-5">
{isEn ? (
{isEs ? (
<>
<li><code>^1.2.0</code>: acepta actualizaciones menores y correcciones (1.2.1, 1.3.0), pero bloquea cambios importantes de API (2.0.0). <strong>Recomendado.</strong></li>
<li><code>~1.2.0</code>: acepta solo correcciones de errores (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: bloquea en esta versión exacta.</li>
<li><code>*</code>: acepta cualquier versión. Evite en producción.</li>
</>
) : isEn ? (
<>
<li><code>^1.2.0</code>: accepts minor updates and bug fixes (1.2.1, 1.3.0), but blocks breaking API changes (2.0.0). <strong>Recommended.</strong></li>
<li><code>~1.2.0</code>: accepts patch releases only (1.2.1, 1.2.5).</li>
+47 -9
View File
@@ -3,16 +3,32 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn
titulo: isEs
? "Diagnóstico de errores comunes en ESP32: Guía profesional"
: isEn
? "Common ESP32 Errors & Diagnostics: Professional Guide"
: "Diagnóstico de Erros Comuns no ESP32: Guia Profissional",
descricao: isEn
descricao: isEs
? "Guía técnica definitiva para identificar y solucionar Brownout, Guru Meditation, Task Watchdog, Stack Overflow, fugas de memoria y fallas de arranque en ESP32 (Arduino y ESP-IDF)."
: isEn
? "Definitive technical guide to identify and fix Brownout, Guru Meditation, Task Watchdog, Stack Overflow, memory leaks, and boot issues on ESP32 (Arduino & ESP-IDF)."
: "Guia técnico definitivo para identificar e resolver Brownout, Guru Meditation, Task Watchdog, Stack Overflow, vazamento de memória e falhas de boot no ESP32 (Arduino e ESP-IDF).",
caminho: "/diagnostico",
locale,
keywords: isEn
keywords: isEs
? [
"errores comunes ESP32",
"brownout detector was triggered",
"guru meditation error",
"task watchdog got triggered",
"stack canary watchpoint triggered",
"cómo depurar ESP32",
"ESP32 reiniciando en bucle",
"failed to connect to esp32",
]
: isEn
? [
"common ESP32 errors",
"brownout detector was triggered",
@@ -74,10 +90,30 @@ const faqsEn = [
},
];
const faqsEs = [
{
pergunta: "¿Qué causa el error 'Brownout detector was triggered' en el ESP32?",
resposta: "Ocurre cuando la tensión en el pin 3V3 cae por debajo de 2.8V ~ 2.43V debido a picos rápidos de corriente de la radio Wi-Fi/BLE (que superan los 500mA), cables USB delgados o un regulador LDO inadecuado. La solución de ingeniería implica agregar condensadores Low-ESR de 10µF a 100µF cerca de VCC y usar reguladores LDO robustos de más de 600mA.",
},
{
pergunta: "¿Cómo solucionar el 'Guru Meditation Error (LoadProhibited)'?",
resposta: "LoadProhibited indica un intento de acceder a un puntero nulo (NULL) o inválido en la memoria. La solución consiste en utilizar Espressif Exception Decoder o addr2line para localizar la línea exacta en el código e implementar comprobaciones defensivas como if (puntero != NULL).",
},
{
pergunta: "¿Por qué se activa el Task Watchdog Timer (TWDT) en FreeRTOS?",
resposta: "El TWDT se activa cuando una tarea de mayor prioridad monopoliza la CPU en un bucle bloqueante (por ejemplo: while(1) sin vTaskDelay), impidiendo que la tarea IDLE alimente el watchdog del sistema operativo.",
},
{
pergunta: "¿Por qué fallan las conexiones HTTPS y TLS con el código -0x2700 en ESP32?",
resposta: "Porque al encenderse, el reloj interno del ESP32 comienza en el año 1970. El validador de certificados TLS considera inválido el certificado del servidor por estar en el 'futuro'. Sincronizar el reloj mediante SNTP (configTime) antes de la conexión resuelve el problema.",
},
];
export default async function DiagnosticoLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const faqs = isEn ? faqsEn : faqsPt;
const isEs = locale === "es";
const faqs = isEs ? faqsEs : isEn ? faqsEn : faqsPt;
return (
<>
@@ -86,8 +122,8 @@ export default async function DiagnosticoLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Diagnostics & Common Errors" : "Diagnóstico & Erros Comuns", caminho: isEn ? "/en/diagnostics" : "/diagnostico" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: isEs ? "Diagnóstico y errores comunes" : isEn ? "Diagnostics & Common Errors" : "Diagnóstico & Erros Comuns", caminho: isEs ? "/es/diagnostico" : isEn ? "/en/diagnostics" : "/diagnostico" },
])
),
}}
@@ -105,11 +141,13 @@ export default async function DiagnosticoLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEn ? "ESP32 Error Diagnostics & Debugging Guide" : "Guia de Diagnóstico e Erros no ESP32",
descricao: isEn
nome: isEs ? "Guía de diagnóstico y errores en ESP32" : isEn ? "ESP32 Error Diagnostics & Debugging Guide" : "Guia de Diagnóstico e Erros no ESP32",
descricao: isEs
? "Herramienta y guía técnica de análisis de fallas, pánicos de CPU y decodificación de registros en ESP32."
: isEn
? "Technical guide and log decoder for troubleshooting CPU panics, crashes, and exceptions on ESP32."
: "Ferramenta e guia técnico de análise de falhas, pânicos de CPU e decodificação de logs no ESP32.",
caminho: isEn ? "/en/diagnostics" : "/diagnostico",
caminho: isEs ? "/es/diagnostico" : isEn ? "/en/diagnostics" : "/diagnostico",
locale,
})
),
+90 -43
View File
@@ -7,6 +7,7 @@ import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import * as diagnosticoPt from "@/lib/diagnosticoData";
import * as diagnosticoEn from "@/lib/data/en/diagnostico";
import * as diagnosticoEs from "@/lib/data/es/diagnostico";
import {
Activity,
AlertTriangle,
@@ -49,6 +50,13 @@ const EXEMPLOS_LOG_EN = [
["TLS Handshake", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_ES = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const GRAVIDADE = {
"Crítica": "text-danger",
"Critical": "text-danger",
@@ -56,6 +64,7 @@ const GRAVIDADE = {
High: "text-gold",
"Média": "text-accent",
Medium: "text-accent",
Media: "text-accent",
};
const CHECKLISTS_PT = [
@@ -104,12 +113,36 @@ const CHECKLISTS_EN = [
},
];
const CHECKLISTS_ES = [
{
icon: Zap,
titulo: "Hardware y alimentación",
itens: [
"Condensador de 10 µF a 100 µF de bajo ESR en VCC, más uno cerámico de 100 nF junto al chip.",
"Regulador LDO dimensionado para picos de 600 mA a 1 A, con margen térmico.",
"Ningún periférico forzando niveles lógicos en los pines de strapping durante el arranque.",
"Condensador de 10 µF o 1 µF en el pin EN (RST) para auto-reset estable durante la programación.",
],
},
{
icon: Layers,
titulo: "Firmware y FreeRTOS",
itens: [
"Cada tarea de FreeRTOS cede la CPU con vTaskDelay, colas o semáforos.",
"Cada ISR marcada con IRAM_ATTR y sin llamadas bloqueantes.",
"Sin uso continuo de String de Arduino en bucles para evitar la fragmentación del heap.",
"Hora sincronizada mediante SNTP antes de abrir conexiones TLS/HTTPS.",
],
},
];
export default function DiagnosticoPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEn ? diagnosticoEn : diagnosticoPt;
const exemplosLog = isEn ? EXEMPLOS_LOG_EN : EXEMPLOS_LOG_PT;
const checklists = isEn ? CHECKLISTS_EN : CHECKLISTS_PT;
const isEs = pathname?.startsWith("/es");
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEs ? diagnosticoEs : isEn ? diagnosticoEn : diagnosticoPt;
const exemplosLog = isEs ? EXEMPLOS_LOG_ES : isEn ? EXEMPLOS_LOG_EN : EXEMPLOS_LOG_PT;
const checklists = isEs ? CHECKLISTS_ES : isEn ? CHECKLISTS_EN : CHECKLISTS_PT;
const [termoBusca, setTermoBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
@@ -139,7 +172,16 @@ export default function DiagnosticoPage() {
}
setResultadoLog(
isEn
isEs
? {
titulo: "Patrón de error no categorizado automáticamente",
tipo: "Desconocido",
explicacao:
"No identificamos una firma clásica de Guru Meditation o Brownout en este fragmento. Verifique que la velocidad de baudios (ej: 115200) sea correcta y si el encabezado 'rst:' o 'panic' está presente.",
acaoRecomendada:
"Busque palabras clave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' o 'panic' en la consola serie.",
}
: isEn
? {
titulo: "Error pattern not automatically classified",
tipo: "Unknown",
@@ -213,12 +255,15 @@ export default function DiagnosticoPage() {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Diagnostics" : "Diagnóstico" }]}
isEs={isEs}
trail={[{ label: isEs ? "Diagnóstico" : isEn ? "Diagnostics" : "Diagnóstico" }]}
icon={AlertTriangle}
label={isEn ? "Firmware Debugging" : "Depuração de firmware"}
title={isEn ? "ESP32 Diagnostics and Crash Decoder" : "Diagnóstico de falhas do ESP32"}
label={isEs ? "Depuración de firmware" : isEn ? "Firmware Debugging" : "Depuração de firmware"}
title={isEs ? "Diagnóstico de fallas del ESP32" : isEn ? "ESP32 Diagnostics and Crash Decoder" : "Diagnóstico de falhas do ESP32"}
lede={
isEn
isEs
? "Comprenda la causa raíz de las fallas por Brownout, Guru Meditation, Watchdog y Stack Overflow, y aplique la corrección de ingeniería en lugar de soluciones improvisadas."
: isEn
? "Understand the root cause of Brownout, Guru Meditation, Watchdog, and Stack Overflow crashes, and apply engineering solutions instead of workarounds."
: "Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
}
@@ -229,9 +274,9 @@ export default function DiagnosticoPage() {
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong"
>
{isEn ? "Based on article by Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
{isEs ? "Basado en el artículo de Ciência Embarcada" : isEn ? "Based on article by Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> {isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
</a>
</PageHero>
@@ -240,14 +285,14 @@ export default function DiagnosticoPage() {
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="max-w-xl">
<h2 id="decodificador-titulo" className="section-title">
{isEn ? "Paste the log to see the cause" : "Cole o log e veja a causa"}
{isEs ? "Pegue el registro y descubra la causa" : isEn ? "Paste the log to see the cause" : "Cole o log e veja a causa"}
</h2>
<p className="mt-2 text-sm text-muted">
{isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : "Copie o trecho do monitor serial. O diagnóstico aparece na hora."}
{isEs ? "Copie el fragmento del monitor serie. El diagnóstico aparece al instante." : isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : "Copie o trecho do monitor serial. O diagnóstico aparece na hora."}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted">{isEn ? "Test with" : "Testar com"}</span>
<span className="text-sm text-muted">{isEs ? "Probar con" : isEn ? "Test with" : "Testar com"}</span>
{exemplosLog.map(([rotulo, texto]) => (
<button key={rotulo} type="button" onClick={() => analisarLog(texto)} className="chip-btn">
{rotulo}
@@ -258,21 +303,21 @@ export default function DiagnosticoPage() {
<div className="relative mt-5">
<label htmlFor="log-serial" className="sr-only">
{isEn ? "Serial monitor log" : "Log do monitor serial"}
{isEs ? "Registro del monitor serie" : isEn ? "Serial monitor log" : "Log do monitor serial"}
</label>
<textarea
id="log-serial"
rows={4}
value={logInput}
onChange={(e) => analisarLog(e.target.value)}
placeholder={isEn ? "Paste serial monitor log here" : "Cole o log do monitor serial aqui"}
placeholder={isEs ? "Pegue el registro del monitor serie aquí" : isEn ? "Paste serial monitor log here" : "Cole o log do monitor serial aqui"}
spellCheck={false}
className="code-panel field !min-h-32 w-full resize-y !rounded-xl !border-white/10 !bg-[var(--code-bg)] p-4 font-mono text-sm !text-[#e9eef4] placeholder:!text-[#a6afbd]"
/>
{logInput && (
<button type="button" onClick={() => analisarLog("")} className="absolute right-3 top-3 inline-flex min-h-11 items-center gap-1 rounded-full border border-white/15 px-3 text-xs font-medium text-[#e9eef4] hover:bg-white/10">
<X className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "Clear" : "Limpar"}
{isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
</button>
)}
</div>
@@ -286,8 +331,8 @@ export default function DiagnosticoPage() {
<h3 className="font-semibold text-ink">{resultadoLog.titulo}</h3>
<span className="chip">{resultadoLog.tipo}</span>
</div>
<p><strong className="font-semibold text-ink">{isEn ? "Likely cause:" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
<p><strong className="font-semibold text-ink">{isEn ? "What to do:" : "O que fazer:"}</strong> {resultadoLog.acaoRecomendada}</p>
<p><strong className="font-semibold text-ink">{isEs ? "Causa probable:" : isEn ? "Likely cause:" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
<p><strong className="font-semibold text-ink">{isEs ? "Qué hacer:" : isEn ? "What to do:" : "O que fazer:"}</strong> {resultadoLog.acaoRecomendada}</p>
</div>
</div>
)}
@@ -297,13 +342,13 @@ export default function DiagnosticoPage() {
<section className="shell section pt-12 sm:pt-14" aria-labelledby="catalogo-titulo">
<h2 id="catalogo-titulo" className="section-title">
{isEn ? "Fault catalog" : "Catálogo de falhas"}
{isEs ? "Catálogo de fallas" : isEn ? "Fault catalog" : "Catálogo de falhas"}
</h2>
<div className="panel mt-6 p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-erro" className="sr-only">
{isEn ? "Search by error, symptom, or serial message" : "Buscar por erro, sintoma ou mensagem serial"}
{isEs ? "Buscar por error, síntoma o mensaje serie" : isEn ? "Search by error, symptom, or serial message" : "Buscar por erro, sintoma ou mensagem serial"}
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
@@ -312,14 +357,14 @@ export default function DiagnosticoPage() {
type="search"
value={termoBusca}
onChange={(e) => setTermoBusca(e.target.value)}
placeholder={isEn ? "Search error, symptom, or message" : "Buscar erro, sintoma ou mensagem"}
placeholder={isEs ? "Buscar error, síntoma o mensaje" : isEn ? "Search error, symptom, or message" : "Buscar erro, sintoma ou mensagem"}
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
</div>
<div className="mt-5">
<h3 className="text-sm font-semibold text-ink">{isEn ? "Symptom" : "Sintoma"}</h3>
<h3 className="text-sm font-semibold text-ink">{isEs ? "Síntoma" : isEn ? "Symptom" : "Sintoma"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
{sintomasRapidos.map((sintoma) => (
<button key={sintoma.id} type="button" onClick={() => setSintomaAtivo(sintomaAtivo === sintoma.id ? null : sintoma.id)} aria-pressed={sintomaAtivo === sintoma.id} className="chip-btn">
@@ -330,11 +375,11 @@ export default function DiagnosticoPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h3 className="text-sm font-semibold text-ink">{isEn ? "Area" : "Área"}</h3>
<h3 className="text-sm font-semibold text-ink">{isEs ? "Área" : isEn ? "Area" : "Área"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaAtiva("todos")} aria-pressed={categoriaAtiva === "todos"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
{isEn ? "All" : "Todas"}
{isEs ? "Todas" : isEn ? "All" : "Todas"}
<span className="tabular-nums opacity-80">{errosCatalogo.length}</span>
</button>
{categoriasDiagnostico.map((cat) => {
@@ -354,13 +399,13 @@ export default function DiagnosticoPage() {
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
<p role="status" aria-live="polite" className="text-sm font-semibold text-ink">
{errosFiltrados.length} {errosFiltrados.length === 1 ? (isEn ? "fault" : "falha") : (isEn ? "faults" : "falhas")}
{totalFiltros > 0 && <span className="font-normal text-muted">{isEn ? " with current filters" : " com os filtros atuais"}</span>}
{errosFiltrados.length} {errosFiltrados.length === 1 ? (isEs ? "falla" : isEn ? "fault" : "falha") : (isEs ? "fallas" : isEn ? "faults" : "falhas")}
{totalFiltros > 0 && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : " com os filtros atuais"}</span>}
</p>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{isEn ? "Reset filters" : "Redefinir filtros"}
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : "Redefinir filtros"}
</button>
)}
</div>
@@ -370,13 +415,13 @@ export default function DiagnosticoPage() {
<div className="card px-6 py-14 text-center">
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
{isEn ? "No faults matching these filters" : "Nenhuma falha com esses filtros"}
{isEs ? "Ninguna falla coincide con estos filtros" : isEn ? "No faults matching these filters" : "Nenhuma falha com esses filtros"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEn ? "Search for a more general term or remove selected symptom and area." : "Busque por um termo mais geral ou remova o sintoma e a área selecionados."}
{isEs ? "Busque por un término más general o elimine el síntoma y el área seleccionados." : isEn ? "Search for a more general term or remove selected symptom and area." : "Busque por um termo mais geral ou remova o sintoma e a área selecionados."}
</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
{isEn ? "Clear filters" : "Limpar filtros"}
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
</button>
</div>
) : (
@@ -384,7 +429,7 @@ export default function DiagnosticoPage() {
const estaAberto = cardAberto === erro.id;
const lang = linguagemCodigo[erro.id] || "arduino";
const codigoAtual =
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || (isEn ? "# Example not applicable" : "# Exemplo não aplicável");
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || (isEs ? "# Ejemplo no aplicable" : isEn ? "# Example not applicable" : "# Exemplo não aplicável");
const catInfo = categoriasDiagnostico.find((c) => c.id === erro.categoria);
const IconeCat = ICONES[catInfo?.icone] ?? AlertTriangle;
@@ -405,7 +450,9 @@ export default function DiagnosticoPage() {
{catInfo?.nome}
</span>
<span className={`chip ${GRAVIDADE[erro.gravidade] ?? "text-accent"}`}>
{isEn ? (
{isEs ? (
<>Severidad {erro.gravidade.toLowerCase()}</>
) : isEn ? (
<>Severity: {erro.gravidade.toLowerCase()}</>
) : (
<>Gravidade {erro.gravidade.toLowerCase()}</>
@@ -427,11 +474,11 @@ export default function DiagnosticoPage() {
<div className="flex items-center justify-between gap-3">
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
{isEn ? "How it appears in serial monitor" : "Como aparece no monitor serial"}
{isEs ? "Cómo aparece en el monitor serie" : isEn ? "How it appears in serial monitor" : "Como aparece no monitor serial"}
</h4>
<button type="button" onClick={() => copiarParaClipboard(erro.logSerial, `log-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `log-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `log-${erro.id}` ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy log" : "Copiar log")}
{copiadoId === `log-${erro.id}` ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar registro" : isEn ? "Copy log" : "Copiar log")}
</button>
</div>
<div className="code-panel mt-1">
@@ -442,7 +489,7 @@ export default function DiagnosticoPage() {
<div>
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Cpu className="h-4 w-4 text-accent" aria-hidden="true" />
{isEn ? "Root cause" : "Causa raiz"}
{isEs ? "Causa raíz" : isEn ? "Root cause" : "Causa raiz"}
</h4>
<p className="callout mt-2">{erro.causaRaiz}</p>
</div>
@@ -451,17 +498,17 @@ export default function DiagnosticoPage() {
<div className="callout callout-danger">
<p className="flex items-center gap-2 font-semibold text-ink">
<XCircle className="h-4 w-4 shrink-0 text-danger" aria-hidden="true" />
{isEn ? "The common workaround" : "O remendo comum"}
{isEs ? "La solución improvisada común" : isEn ? "The common workaround" : "O remendo comum"}
</p>
<p className="mt-2 font-medium text-ink">{erro.gambiarra.titulo}</p>
<p className="mt-1">{erro.gambiarra.descricao}</p>
<p className="mt-2"><strong className="font-semibold text-danger">{isEn ? "Consequence:" : "Consequência:"}</strong> {erro.gambiarra.consequencia}</p>
<p className="mt-2"><strong className="font-semibold text-danger">{isEs ? "Consecuencia:" : isEn ? "Consequence:" : "Consequência:"}</strong> {erro.gambiarra.consequencia}</p>
</div>
<div className="callout callout-ok">
<p className="flex items-center gap-2 font-semibold text-ink">
<CheckCircle2 className="h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
{isEn ? "The engineering solution" : "A solução de engenharia"}
{isEs ? "La solución de ingeniería" : isEn ? "The engineering solution" : "A solução de engenharia"}
</p>
<p className="mt-2 font-medium text-ink">{erro.solucaoEngenharia.titulo}</p>
<ul className="mt-2 space-y-1.5">
@@ -477,7 +524,7 @@ export default function DiagnosticoPage() {
<div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div role="group" aria-label={isEn ? "Example language" : "Linguagem do exemplo"} className="flex flex-wrap gap-2">
<div role="group" aria-label={isEs ? "Lenguaje del ejemplo" : isEn ? "Example language" : "Linguagem do exemplo"} className="flex flex-wrap gap-2">
{[
["arduino", "Arduino C++"],
["espidf", "ESP-IDF (C)"],
@@ -490,7 +537,7 @@ export default function DiagnosticoPage() {
</div>
<button type="button" onClick={() => copiarParaClipboard(codigoAtual, `code-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `code-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `code-${erro.id}` ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy code" : "Copiar código")}
{copiadoId === `code-${erro.id}` ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar código" : isEn ? "Copy code" : "Copiar código")}
</button>
</div>
<div className="code-panel mt-3">
@@ -500,7 +547,7 @@ export default function DiagnosticoPage() {
<div className="callout callout-warn flex items-start gap-3">
<Wrench className="mt-0.5 h-4 w-4 shrink-0 text-gold" aria-hidden="true" />
<p><strong className="font-semibold text-ink">{isEn ? "Engineering tip:" : "Dica de engenharia:"}</strong> {erro.dicaPro}</p>
<p><strong className="font-semibold text-ink">{isEs ? "Consejo de ingeniería:" : isEn ? "Engineering tip:" : "Dica de engenharia:"}</strong> {erro.dicaPro}</p>
</div>
</div>
)}
@@ -513,10 +560,10 @@ export default function DiagnosticoPage() {
<section className="shell pb-20 md:pb-28" aria-labelledby="checklist-titulo">
<h2 id="checklist-titulo" className="section-title">
{isEn ? "Pre-deployment checklist" : "Checklist antes de ir para campo"}
{isEs ? "Lista de verificación previa a campo" : isEn ? "Pre-deployment checklist" : "Checklist antes de ir para campo"}
</h2>
<p className="mt-3 max-w-2xl text-base text-muted">
{isEn ? "Essential items to validate on hardware and firmware before shipping a product." : "Itens essenciais para validar no hardware e no firmware antes de entregar o produto."}
{isEs ? "Elementos esenciales para validar en hardware y firmware antes de entregar el producto." : isEn ? "Essential items to validate on hardware and firmware before shipping a product." : "Itens essenciais para validar no hardware e no firmware antes de entregar o produto."}
</p>
<div className="mt-8 grid gap-4 md:grid-cols-2">
{checklists.map(({ icon: Icon, titulo, itens }) => (
+43 -25
View File
@@ -7,14 +7,16 @@ import { notFound } from "next/navigation";
import CodeExample from "@/components/CodeExample";
import frameworksDataPt from "@/public/frameworks.json";
import frameworksDataEn from "@/data/en/frameworks.json";
import frameworksDataEs from "@/data/es/frameworks.json";
import { getLocalizedHref } from "@/i18n/links";
import { ArrowRight, ArrowUpRight, BookOpen, Box, Check, Code, Layers } from "lucide-react";
export default async function FrameworkDetail({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const frameworksData = isEn ? frameworksDataEn : frameworksDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const framework = frameworksData[key];
if (!framework) {
@@ -36,8 +38,8 @@ export default async function FrameworkDetail({ params }) {
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Frameworks" : "Frameworks", caminho: getLocalizedHref("/frameworks", currentLocale) },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: "Frameworks", caminho: getLocalizedHref("/frameworks", currentLocale) },
{ nome: framework.nome, caminho: getLocalizedHref(`/frameworks/${encodeURIComponent(key)}`, currentLocale) },
])),
}}
@@ -48,7 +50,8 @@ export default async function FrameworkDetail({ params }) {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
isEs={isEs}
trail={[{ label: isEs ? "Frameworks y SDKs" : isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
icon={Layers}
label={framework.tipo}
title={framework.nome}
@@ -57,41 +60,41 @@ export default async function FrameworkDetail({ params }) {
{framework.documentacao && (
<a href={framework.documentacao} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<BookOpen className="h-4 w-4" aria-hidden="true" />
{isEn ? "Official documentation" : "Documentação oficial"}
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : "Documentação oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{framework.repositorio && (
<a href={framework.repositorio} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
{isEn ? "GitHub repository" : "Repositório no GitHub"}
{isEs ? "Repositorio en GitHub" : isEn ? "GitHub repository" : "Repositório no GitHub"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
<section className="shell mt-10" aria-label={isEn ? "Summary" : "Resumo"}>
<section className="shell mt-10" aria-label={isEs ? "Resumen" : isEn ? "Summary" : "Resumo"}>
<dl className="grid gap-3 md:grid-cols-3">
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEn ? "Language" : "Linguagem"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Lenguaje" : isEn ? "Language" : "Linguagem"}</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.linguagem}</dd>
<dd className="mt-0.5 text-xs text-muted">{framework.nome_completo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEn ? "Category" : "Categoria"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.tipo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEn ? "Main purpose" : "Objetivo principal"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Objetivo principal" : isEn ? "Main purpose" : "Objetivo principal"}</dt>
<dd className="mt-1 text-sm font-medium leading-relaxed text-ink">{framework.funcao_principal}</dd>
</div>
</dl>
</section>
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label={isEn ? "Features and applications" : "Recursos e aplicações"}>
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label={isEs ? "Funciones y aplicaciones" : isEn ? "Features and applications" : "Recursos e aplicações"}>
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
{isEn ? "Key features" : "Recursos de destaque"}
{isEs ? "Funciones destacadas" : isEn ? "Key features" : "Recursos de destaque"}
</h2>
<ul className="mt-5 space-y-3">
{features.map((feature) => (
@@ -105,7 +108,7 @@ export default async function FrameworkDetail({ params }) {
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
{isEn ? "When to use" : "Quando usar"}
{isEs ? "Cuándo usar" : isEn ? "When to use" : "Quando usar"}
</h2>
<ul className="mt-5 space-y-2.5">
{useCases.map((useCase) => (
@@ -123,13 +126,19 @@ export default async function FrameworkDetail({ params }) {
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "ESP-IDF 4.4 or higher" : "ESP-IDF 4.4 ou superior"}
{isEs ? "ESP-IDF 4.4 o superior" : isEn ? "ESP-IDF 4.4 or higher" : "ESP-IDF 4.4 ou superior"}
</span>
<h2 className="section-title mt-4">
{isEn ? "Modules with the ESP Component Registry" : "Módulos com o ESP Component Registry"}
{isEs ? "Módulos con ESP Component Registry" : isEn ? "Modules with the ESP Component Registry" : "Módulos com o ESP Component Registry"}
</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
{isEn ? (
{isEs ? (
<>
En los proyectos ESP-IDF v4.4+ y v5.x, las bibliotecas ya no necesitan agregarse como submódulos Git. Utilice el{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> y el comando{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> para descargar controladores de pantalla (LVGL), pilas industriales (Modbus), RainMaker y más.
</>
) : isEn ? (
<>
In ESP-IDF v4.4+ and v5.x projects, libraries no longer need to be added as Git submodules. Use{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> and the command{" "}
@@ -145,7 +154,7 @@ export default async function FrameworkDetail({ params }) {
</p>
</div>
<Link href={getLocalizedHref("/componentes", currentLocale)} className="btn btn-primary shrink-0">
{isEn ? "Explore components" : "Explorar componentes"}
{isEs ? "Explorar componentes" : isEn ? "Explore components" : "Explorar componentes"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -161,7 +170,7 @@ export default async function FrameworkDetail({ params }) {
<div className="shell section pt-10">
<Link href={getLocalizedHref("/frameworks", currentLocale)} className="inline-flex min-h-11 items-center gap-2 text-sm font-medium text-accent hover:text-accent-strong">
<ArrowRight className="h-4 w-4 rotate-180" aria-hidden="true" />
{isEn ? "View all frameworks" : "Ver todos os frameworks"}
{isEs ? "Ver todos los frameworks" : isEn ? "View all frameworks" : "Ver todos os frameworks"}
</Link>
</div>
</main>
@@ -174,24 +183,33 @@ export default async function FrameworkDetail({ params }) {
export async function generateMetadata({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const frameworksData = isEn ? frameworksDataEn : frameworksDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const framework = frameworksData[key];
if (!framework) {
return { title: isEn ? "Framework not found" : "Framework não encontrado", robots: { index: false, follow: false } };
return { title: isEs ? "Framework no encontrado" : isEn ? "Framework not found" : "Framework não encontrado", robots: { index: false, follow: false } };
}
return {
...paginaMeta({
titulo: isEn ? `${framework.nome} for ESP32 · Technical Guide` : `${framework.nome} para ESP32 · Guia Técnico`,
descricao: isEn
titulo: isEs ? `${framework.nome} para ESP32 · Guía técnica` : isEn ? `${framework.nome} for ESP32 · Technical Guide` : `${framework.nome} para ESP32 · Guia Técnico`,
descricao: isEs
? `${framework.nome_completo}: ${framework.descricao} Guía con ejemplos prácticos de código y casos de uso.`
: isEn
? `${framework.nome_completo}: ${framework.descricao} Guide with practical code examples and use cases.`
: `${framework.nome_completo}: ${framework.descricao} Guia com exemplos práticos de código e casos de uso.`,
caminho: `/frameworks/${encodeURIComponent(key)}`,
tipo: "article",
locale: currentLocale,
keywords: isEn
keywords: isEs
? [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `programar ESP32 con ${framework.nome}`,
"ESP32", "framework ESP32",
]
: isEn
? [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `program ESP32 with ${framework.nome}`,
+26 -6
View File
@@ -3,17 +3,36 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return {
...paginaMeta({
titulo: isEn
titulo: isEs
? "Frameworks y SDKs para ESP32: Guía completa y comparativa"
: isEn
? "ESP32 Frameworks & SDKs: Complete Guide and Comparison"
: "Frameworks & SDKs para ESP32: Guia Completo e Comparativo",
descricao: isEn
descricao: isEs
? "Guía completa de desarrollo: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino y MicroPython con ejemplos de código."
: isEn
? "Complete development guide: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino, and MicroPython with code examples."
: "Guia completo de desenvolvimento: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino e MicroPython com exemplos de código.",
caminho: "/frameworks",
locale: isEn ? "en" : "pt",
keywords: isEn
locale,
keywords: isEs
? [
"ESP-IDF",
"ESP-RainMaker",
"ESP-Matter",
"Rust ESP32",
"ESP-SR",
"ESP-ADF",
"ESP-IoT-Solution",
"Arduino ESP32",
"MicroPython ESP32",
"frameworks ESP32",
"programar ESP32",
]
: isEn
? [
"ESP-IDF",
"ESP-RainMaker",
@@ -47,6 +66,7 @@ export async function generateMetadata({ params }) {
export default async function FrameworksLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return (
<>
<script
@@ -54,8 +74,8 @@ export default async function FrameworksLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: "Frameworks", caminho: isEn ? "/en/frameworks" : "/frameworks" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: "Frameworks", caminho: isEs ? "/es/frameworks" : isEn ? "/en/frameworks" : "/frameworks" },
])
),
}}
+58 -15
View File
@@ -5,6 +5,7 @@ import SectionHead from "@/components/SectionHead";
import Link from "next/link";
import frameworksDataPt from "@/public/frameworks.json";
import frameworksDataEn from "@/data/en/frameworks.json";
import frameworksDataEs from "@/data/es/frameworks.json";
import { paginaMeta } from "@/lib/seo";
import { getLocalizedHref } from "@/i18n/links";
import { ArrowRight, ArrowUpRight, Box, Brain, Cloud, GraduationCap, Layers, Mic, Music, Sparkles } from "lucide-react";
@@ -75,13 +76,47 @@ const GUIAS_EN = [
},
];
const GUIAS_ES = [
{
icon: GraduationCap,
titulo: "Estoy comenzando",
texto: "Arduino o MicroPython son los caminos más rápidos para probar sensores y APIs. Para producto, elija ESP-IDF o Rust.",
links: [
["Arduino", "/frameworks/Arduino"],
["MicroPython", "/frameworks/MicroPython"],
["Rust", "/frameworks/Rust-ESP"],
],
},
{
icon: Cloud,
titulo: "Nube y hogar conectado",
texto: "ESP-Matter para interoperabilidad, ESP-RainMaker para nube lista y Zigbee para redes locales de bajo consumo.",
links: [
["RainMaker", "/frameworks/ESP-RainMaker"],
["Matter", "/frameworks/ESP-Matter"],
["Zigbee", "/frameworks/ESP-Zigbee-SDK"],
],
},
{
icon: Sparkles,
titulo: "Audio, voz e IA",
texto: "ESP-ADF para procesamiento de audio, ESP-SR para reconocimiento de voz offline y ESP-NN para acelerar TinyML.",
links: [
["Audio", "/frameworks/ESP-ADF", Music],
["Voz", "/frameworks/ESP-SR", Mic],
["IA", "/frameworks/ESP-NN", Brain],
],
},
];
export default async function Frameworks({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const frameworksData = isEn ? frameworksDataEn : frameworksDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const frameworks = Object.entries(frameworksData);
const guias = isEn ? GUIAS_EN : GUIAS_PT;
const guias = isEs ? GUIAS_ES : isEn ? GUIAS_EN : GUIAS_PT;
return (
<div className="site-canvas min-h-screen">
@@ -90,18 +125,21 @@ export default async function Frameworks({ params }) {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs" }]}
isEs={isEs}
trail={[{ label: isEs ? "Frameworks y SDKs" : isEn ? "Frameworks & SDKs" : "Frameworks e SDKs" }]}
icon={Layers}
label={isEn ? `${frameworks.length} frameworks and SDKs` : `${frameworks.length} frameworks e SDKs`}
title={isEn ? "ESP32 Frameworks and SDKs" : "Frameworks e SDKs do ESP32"}
label={isEs ? `${frameworks.length} frameworks y SDKs` : isEn ? `${frameworks.length} frameworks and SDKs` : `${frameworks.length} frameworks e SDKs`}
title={isEs ? "Frameworks y SDKs de ESP32" : isEn ? "ESP32 Frameworks and SDKs" : "Frameworks e SDKs do ESP32"}
lede={
isEn
isEs
? "Desde C/C++ nativo con FreeRTOS hasta Rust, nube IoT, reconocimiento de voz, audio y Matter. Elija según lo que desee construir."
: isEn
? "From native C/C++ with FreeRTOS to Rust, IoT cloud, speech recognition, audio, and Matter. Choose by what you want to build."
: "Do C/C++ nativo com FreeRTOS ao Rust, passando por nuvem IoT, reconhecimento de voz, áudio e Matter. Escolha pelo que você quer construir."
}
/>
<section className="shell pt-10 sm:pt-12" aria-label={isEn ? "Where to begin" : "Por onde começar"}>
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Por dónde empezar" : isEn ? "Where to begin" : "Por onde começar"}>
<div className="grid gap-4 md:grid-cols-3">
{guias.map(({ icon: Icon, titulo, texto, links }) => (
<div key={titulo} className="card flex flex-col p-5 sm:p-6">
@@ -130,11 +168,16 @@ export default async function Frameworks({ params }) {
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "Modular architecture" : "Arquitetura modular"}
{isEs ? "Arquitectura modular" : isEn ? "Modular architecture" : "Arquitetura modular"}
</span>
<h2 className="section-title mt-4">ESP Component Registry</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
{isEn ? (
{isEs ? (
<>
Instale controladores de pantalla (LVGL), pilas industriales (Modbus), integraciones en la nube y paquetes de soporte para placas
(BSPs) con una sola línea en su manifiesto <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code>.
</>
) : isEn ? (
<>
Install display drivers (LVGL), industrial stacks (Modbus), cloud integrations, and board support packages
(BSPs) with a single line in your <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> manifest.
@@ -148,7 +191,7 @@ export default async function Frameworks({ params }) {
</p>
</div>
<Link href={getLocalizedHref("/componentes", currentLocale)} className="btn btn-primary shrink-0">
{isEn ? "Explore components" : "Explorar componentes"}
{isEs ? "Explorar componentes" : isEn ? "Explore components" : "Explorar componentes"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -157,9 +200,9 @@ export default async function Frameworks({ params }) {
<section className="shell section" aria-labelledby="todos-titulo">
<SectionHead
id="todos-titulo"
eyebrow={isEn ? "All" : "Todos"}
title={isEn ? "All frameworks" : "Todos os frameworks"}
lede={isEn ? "Each page provides features, ideal use cases, code examples, and official links." : "Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais."}
eyebrow={isEs ? "Todos" : isEn ? "All" : "Todos"}
title={isEs ? "Todos los frameworks" : isEn ? "All frameworks" : "Todos os frameworks"}
lede={isEs ? "Cada ficha ofrece funciones, aplicaciones ideales, ejemplos de código y enlaces oficiales." : isEn ? "Each page provides features, ideal use cases, code examples, and official links." : "Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais."}
/>
<div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -181,7 +224,7 @@ export default async function Frameworks({ params }) {
</ul>
<span className="mt-5 inline-flex items-center gap-1 border-t border-line pt-4 text-sm font-medium text-accent">
{isEn ? "View details and example" : "Ver detalhes e exemplo"}
{isEs ? "Ver detalles y ejemplo" : isEn ? "View details and example" : "Ver detalhes e exemplo"}
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
+28 -9
View File
@@ -1,20 +1,36 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
import { categoriasGlossario as categoriasGlossarioPt } from "@/lib/glossarioData";
import { categoriasGlossario as categoriasGlossarioEn } from "@/lib/data/en/glossario.js";
import { categoriasGlossario as categoriasGlossarioEs } from "@/lib/data/es/glossario.js";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn
titulo: isEs
? "Glosario técnico de ESP32: Términos y conceptos"
: isEn
? "ESP32 Technical Glossary: Terms and Concepts"
: "Glossário Técnico do ESP32: Termos e Conceitos",
descricao: isEn
descricao: isEs
? "Más de 50 conceptos esenciales de ESP32 explicados: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter y Thread."
: isEn
? "Over 50 essential ESP32 concepts explained: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter, and Thread."
: "Mais de 50 termos e conceitos essenciais do ecossistema ESP32 explicados em português: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter e Thread.",
caminho: "/glossario",
locale,
keywords: isEn
keywords: isEs
? [
"glosario ESP32",
"qué es GPIO",
"qué son strapping pins",
"qué es ESP32 ULP",
"qué es ESP32 PSRAM",
"términos técnicos microcontrolador",
"conceptos ESP32 hardware",
]
: isEn
? [
"ESP32 glossary",
"what is GPIO",
@@ -39,7 +55,8 @@ export async function generateMetadata({ params }) {
export default async function GlossarioLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const categorias = isEn ? categoriasGlossarioEn : categoriasGlossarioPt;
const isEs = locale === "es";
const categorias = isEs ? categoriasGlossarioEs : isEn ? categoriasGlossarioEn : categoriasGlossarioPt;
return (
<>
@@ -54,8 +71,8 @@ export default async function GlossarioLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Technical Glossary" : "Glossário Técnico", caminho: isEn ? "/en/glossary" : "/glossario" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: isEs ? "Glosario técnico" : isEn ? "Technical Glossary" : "Glossário Técnico", caminho: isEs ? "/es/glosario" : isEn ? "/en/glossary" : "/glossario" },
])
),
}}
@@ -65,11 +82,13 @@ export default async function GlossarioLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
descricao: isEn
nome: isEs ? "Glosario técnico de ESP32" : isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
descricao: isEs
? "Enciclopedia de términos técnicos y conceptos de hardware y software del ecosistema ESP32."
: isEn
? "Encyclopedia of hardware and software technical terms for the ESP32 ecosystem."
: "Enciclopédia de termos técnicos e conceitos de hardware e software do ecossistema ESP32.",
caminho: isEn ? "/en/glossary" : "/glossario",
caminho: isEs ? "/es/glosario" : isEn ? "/en/glossary" : "/glossario",
locale,
})
),
+37 -28
View File
@@ -8,6 +8,7 @@ import PageHero from "@/components/PageHero";
import Link from "next/link";
import * as glossarioPt from "@/lib/glossarioData";
import * as glossarioEn from "@/lib/data/en/glossario";
import * as glossarioEs from "@/lib/data/es/glossario";
import { getLocalizedHref } from "@/i18n/links";
import {
ArrowRight,
@@ -40,8 +41,9 @@ const nomeCurto = (nome) => nome.split(/,| &/)[0].trim();
export default function GlossarioPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const currentLocale = isEn ? "en" : "pt";
const { categoriasGlossario } = isEn ? glossarioEn : glossarioPt;
const isEs = pathname?.startsWith("/es");
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const { categoriasGlossario } = isEs ? glossarioEs : isEn ? glossarioEn : glossarioPt;
const [busca, setBusca] = useState("");
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
@@ -109,22 +111,25 @@ export default function GlossarioPage() {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Glossary" : "Glossário" }]}
isEs={isEs}
trail={[{ label: isEs ? "Glosario" : isEn ? "Glossary" : "Glossário" }]}
icon={BookOpen}
label={isEn ? `${todosOsTermos.length} terms explained` : `${todosOsTermos.length} termos explicados`}
title={isEn ? "ESP32 Technical Glossary" : "Glossário técnico do ESP32"}
label={isEs ? `${todosOsTermos.length} términos explicados` : isEn ? `${todosOsTermos.length} terms explained` : `${todosOsTermos.length} termos explicados`}
title={isEs ? "Glosario técnico de ESP32" : isEn ? "ESP32 Technical Glossary" : "Glossário técnico do ESP32"}
lede={
isEn
isEs
? "Definiciones directas, parámetros de hardware y un ejemplo práctico para cada término del ecosistema, desde ADC hasta Secure Boot."
: isEn
? "Direct definitions, hardware parameters, and practical examples for every ecosystem term, from ADC to Secure Boot."
: "Definições diretas, parâmetros de hardware e um exemplo prático para cada termo do ecossistema, de ADC a Secure Boot."
}
/>
<section className="shell pt-10 sm:pt-12" aria-label={isEn ? "Search and filters" : "Busca e filtros"}>
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Búsqueda y filtros" : isEn ? "Search and filters" : "Busca e filtros"}>
<div className="panel p-5 sm:p-7">
<div className="relative">
<label htmlFor="busca-glossario" className="sr-only">
{isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
{isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
@@ -133,7 +138,7 @@ export default function GlossarioPage() {
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder={isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
placeholder={isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
autoComplete="off"
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
/>
@@ -141,14 +146,14 @@ export default function GlossarioPage() {
{busca && (
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
<X className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "Clear" : "Limpar"}
{isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
</button>
)}
</div>
</div>
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="mr-1 text-sm text-muted">{isEn ? "Popular searches" : "Mais buscados"}</span>
<span className="mr-1 text-sm text-muted">{isEs ? "Más buscados" : isEn ? "Popular searches" : "Mais buscados"}</span>
{BUSCAS_POPULARES.map((rotulo) => (
<button
key={rotulo}
@@ -162,7 +167,7 @@ export default function GlossarioPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">{isEn ? "Category" : "Categoria"}</h2>
<h2 className="text-sm font-semibold text-ink">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</h2>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
@@ -171,7 +176,7 @@ export default function GlossarioPage() {
className="chip-btn"
>
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
{isEn ? "All" : "Todas"}
{isEs ? "Todas" : isEn ? "All" : "Todas"}
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
</button>
{categoriasGlossario.map((cat) => {
@@ -194,10 +199,10 @@ export default function GlossarioPage() {
</div>
<div className="mt-6 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">{isEn ? "Alphabetical order" : "Ordem alfabética"}</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label={isEn ? "Filter by first letter" : "Filtrar pela letra inicial"}>
<h2 className="text-sm font-semibold text-ink">{isEs ? "Orden alfabético" : isEn ? "Alphabetical order" : "Ordem alfabética"}</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label={isEs ? "Filtrar por letra inicial" : isEn ? "Filter by first letter" : "Filtrar pela letra inicial"}>
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
{isEn ? "All" : "Todas"}
{isEs ? "Todas" : isEn ? "All" : "Todas"}
</button>
{alfabeto.map(({ letra, temTermos }) => (
<button
@@ -219,13 +224,13 @@ export default function GlossarioPage() {
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
{total} {total === 1 ? (isEn ? "term" : "termo") : (isEn ? "terms" : "termos")}
{temFiltroAtivo && <span className="font-normal text-muted">{isEn ? " with current filters" : " com os filtros atuais"}</span>}
{total} {total === 1 ? (isEs ? "término" : isEn ? "term" : "termo") : (isEs ? "términos" : isEn ? "terms" : "termos")}
{temFiltroAtivo && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : " com os filtros atuais"}</span>}
</h2>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{isEn ? "Reset filters" : "Redefinir filtros"}
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : "Redefinir filtros"}
</button>
)}
</div>
@@ -234,16 +239,18 @@ export default function GlossarioPage() {
<div className="card mt-6 px-6 py-14 text-center">
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
{isEn ? "No terms found" : "Nenhum termo encontrado"}
{isEs ? "No se encontraron términos" : isEn ? "No terms found" : "Nenhum termo encontrado"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEn
{isEs
? "Nada coincide con la búsqueda y los filtros actuales. Pruebe con una sigla más corta o vuelva a todas las categorías."
: isEn
? "Nothing matches your current search and filters. Try a shorter acronym or reset to all categories."
: "Nada combina com a busca e os filtros atuais. Tente uma sigla mais curta ou volte para todas as categorias."}
</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{isEn ? "Clear filters" : "Limpar filtros"}
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
</button>
</div>
) : (
@@ -260,7 +267,7 @@ export default function GlossarioPage() {
<button
type="button"
onClick={() => copiarLinkTermo(item.slug)}
aria-label={isEn ? `Copy link for ${item.termo}` : `Copiar link para ${item.termo}`}
aria-label={isEs ? `Copiar enlace para ${item.termo}` : isEn ? `Copy link for ${item.termo}` : `Copiar link para ${item.termo}`}
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
>
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
@@ -275,7 +282,7 @@ export default function GlossarioPage() {
<div className="callout mt-5">
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
{isEn ? "In practice" : "Na prática"}
{isEs ? "En la práctica" : isEn ? "In practice" : "Na prática"}
</p>
<p className="mt-1">
<TextoComLinks texto={item.exemplo} currentLocale={currentLocale} />
@@ -291,20 +298,22 @@ export default function GlossarioPage() {
<section className="shell pb-20 md:pb-28">
<div className="panel p-8 text-center sm:p-12">
<h2 className="section-title mx-auto max-w-2xl">
{isEn ? "From concept to the right chip." : "Do conceito ao chip certo."}
{isEs ? "Del concepto al chip adecuado." : isEn ? "From concept to the right chip." : "Do conceito ao chip certo."}
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
{isEn
{isEs
? "Con los términos a mano, compare las series lado a lado o responda al selector para encontrar el modelo que se adapta a su proyecto."
: isEn
? "With the terms in hand, compare series side by side or answer the selector to find the model that fits your project."
: "Com os termos em mãos, compare as séries lado a lado ou responda ao seletor para chegar ao modelo que cabe no seu projeto."}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href={getLocalizedHref("/seletor", currentLocale)} className="btn btn-primary w-full sm:w-auto">
<Sliders className="h-4 w-4" aria-hidden="true" />
{isEn ? "Choose my chip" : "Escolher meu chip"}
{isEs ? "Elegir mi chip" : isEn ? "Choose my chip" : "Escolher meu chip"}
</Link>
<Link href={getLocalizedHref("/series", currentLocale)} className="btn btn-ghost w-full sm:w-auto">
{isEn ? "Explore series" : "Explorar as séries"}
{isEs ? "Explorar las series" : isEn ? "Explore series" : "Explorar as séries"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
+16
View File
@@ -45,6 +45,7 @@ export function generateStaticParams() {
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const ogLocaleMap = {
pt: "pt_BR",
@@ -59,10 +60,14 @@ export async function generateMetadata({ params }) {
const titleDefault = isEn
? "ESPDocs · ESP32 Open Documentation"
: isEs
? "ESPDocs · Documentación abierta de ESP32"
: "ESPDocs · Documentação do ESP32 em português";
const description = isEn
? "Specifications, pinouts, comparison and selection of 12 ESP32 microcontroller series. Datasheets, peripherals, hardware security and connection diagrams."
: isEs
? "Especificaciones, patillaje, comparación y selección de las 12 series de microcontroladores ESP32. Hojas de datos, periféricos, seguridad en hardware y diagramas de conexión."
: "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.";
const keywords = isEn
@@ -72,6 +77,13 @@ export async function generateMetadata({ params }) {
"compare ESP32", "which ESP32 to choose", "ESP32 datasheet",
"microcontroller", "Espressif", "ESP-IDF", "IoT", "embedded systems",
]
: isEs
? [
"ESP32", "ESP32 en español", "documentación ESP32", "patillaje ESP32", "pinout ESP32",
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
"comparar ESP32", "cuál ESP32 elegir", "datasheet ESP32", "hoja de datos ESP32",
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embebidos",
]
: [
"ESP32", "ESP32 português", "documentação ESP32", "pinout ESP32",
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
@@ -112,6 +124,8 @@ export async function generateMetadata({ params }) {
title: titleDefault,
description: isEn
? "Specifications, pinouts, comparison and selection of 12 ESP32 microcontroller series."
: isEs
? "Especificaciones, patillaje, comparación y selección de las 12 series de microcontroladores ESP32."
: "Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro.",
},
twitter: {
@@ -119,6 +133,8 @@ export async function generateMetadata({ params }) {
title: titleDefault,
description: isEn
? "Specifications, pinouts, comparison and selection of 12 ESP32 series."
: isEs
? "Especificaciones, patillaje, comparación y selección de las 12 series de ESP32."
: "Especificações, pinagem, comparação e seleção das 12 séries de ESP32, em português.",
},
robots: {
+16 -6
View File
@@ -24,6 +24,13 @@ const CAMINHOS_EN = [
{ href: "/glossario", icon: BookOpen, titulo: "Glossary", texto: "Hardware terms explained clearly." },
];
const CAMINHOS_ES = [
{ href: "/series", icon: Compass, titulo: "Series ESP32", texto: "Las 12 familias, con patillaje y especificaciones." },
{ href: "/seletor", icon: SlidersHorizontal, titulo: "Selector de chip", texto: "Responda a ocho preguntas y reciba los modelos recomendados." },
{ href: "/comparacao", icon: Scale, titulo: "Comparador", texto: "Hasta cuatro series frente a frente." },
{ href: "/glossario", icon: BookOpen, titulo: "Glosario", texto: "Términos de hardware explicados con claridad." },
];
export default async function NotFound() {
let locale = "pt";
try {
@@ -32,7 +39,8 @@ export default async function NotFound() {
locale = "pt";
}
const isEn = locale === "en";
const caminhos = isEn ? CAMINHOS_EN : CAMINHOS_PT;
const isEs = locale === "es";
const caminhos = isEn ? CAMINHOS_EN : isEs ? CAMINHOS_ES : CAMINHOS_PT;
return (
<div className="site-canvas min-h-screen">
@@ -40,16 +48,18 @@ export default async function NotFound() {
<main id="conteudo" className="shell section">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow justify-center">{isEn ? "Error 404" : "Erro 404"}</p>
<h1 className="page-title mt-4">{isEn ? "This page does not exist." : "Essa página não existe."}</h1>
<p className="eyebrow justify-center">{isEn ? "Error 404" : "Error 404"}</p>
<h1 className="page-title mt-4">{isEn ? "This page does not exist." : isEs ? "Esta página no existe." : "Essa página não existe."}</h1>
<p className="lede mx-auto mt-5 max-w-xl">
{isEn
? "The address may have changed or was mistyped. Choose one of these paths to continue."
: isEs
? "La dirección puede haber cambiado o estar mal escrita. Elija uno de estos caminos para continuar."
: "O endereço pode ter mudado ou estar digitado errado. Escolha um destes caminhos para continuar."}
</p>
<div className="mt-8 flex justify-center">
<Link href={isEn ? "/en" : "/"} className="btn btn-primary">
{isEn ? "Back to home" : "Voltar ao início"}
<Link href={isEn ? "/en" : isEs ? "/es" : "/"} className="btn btn-primary">
{isEn ? "Back to home" : isEs ? "Volver al inicio" : "Voltar ao início"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -57,7 +67,7 @@ export default async function NotFound() {
<ul className="mx-auto mt-14 grid max-w-4xl gap-3 sm:grid-cols-2">
{caminhos.map(({ href, icon: Icon, titulo, texto }) => {
const targetHref = isEn ? getLocalizedHref(href, "en") : href;
const targetHref = getLocalizedHref(href, locale);
return (
<li key={href}>
<Link href={targetHref} className="card card-interactive group flex h-full items-start gap-4 p-5">
+113 -21
View File
@@ -6,6 +6,8 @@ import seriesDataPt from "@/public/series.json";
import frameworksDataPt from "@/public/frameworks.json";
import seriesDataEn from "@/data/en/series.json";
import frameworksDataEn from "@/data/en/frameworks.json";
import seriesDataEs from "@/data/es/series.json";
import frameworksDataEs from "@/data/es/frameworks.json";
import { SITE } from "@/lib/seo";
import { getLocalizedHref } from "@/i18n/links";
import {
@@ -112,6 +114,51 @@ const TOOLS_EN = [
},
];
const TOOLS_ES = [
{
href: "/seletor",
title: "Selector inteligente",
desc: "Responda a ocho preguntas sobre su proyecto y reciba los chips más compatibles, con justificación técnica.",
icon: SlidersHorizontal,
action: "Encontrar mi chip",
},
{
href: "/comparacao",
title: "Comparador de series",
desc: "Compare hasta cuatro modelos frente a frente: núcleos, radio, memoria, periféricos y seguridad.",
icon: Scale,
action: "Comparar ahora",
},
{
href: "/diagnostico",
title: "Diagnóstico de fallos",
desc: "Causas y soluciones para brownout, bootloop, Guru Meditation, watchdog y fallos de flash.",
icon: AlertTriangle,
action: "Abrir diagnóstico",
},
{
href: "/componentes",
title: "Component Registry",
desc: "Drivers, bibliotecas y BSPs del registro oficial de Espressif, con manifiesto listo para su proyecto.",
icon: Box,
action: "Explorar componentes",
},
{
href: "/frameworks",
title: "Frameworks y SDKs",
desc: "ESP-IDF, Arduino, Rust, Matter, MicroPython y las herramientas que rodean al silicio.",
icon: Terminal,
action: "Ver frameworks",
},
{
href: "/catalogo",
title: "Catálogo de placas",
desc: "DevKits y módulos reales, con interfaz USB, sensores integrados y enlace directo a la tienda oficial.",
icon: Cpu,
action: "Ver placas",
},
];
const ERRORS_PT = [
{
code: "Brownout detector was triggered",
@@ -158,15 +205,40 @@ const ERRORS_EN = [
},
];
const ERRORS_ES = [
{
code: "Brownout detector was triggered",
cause: "Caída de tensión al inicializar la etapa de radio.",
fix: "Condensador de 10–100 µF junto al chip y fuente de 5 V con corriente suficiente.",
},
{
code: "Guru Meditation Error",
cause: "Puntero inválido, desbordamiento de pila o uso incorrecto de interrupciones.",
fix: "Analice el backtrace con addr2line y dimensione la pila de la tarea en FreeRTOS.",
},
{
code: "ADC2 no disponible con Wi-Fi",
cause: "El controlador de radio reserva ADC2 mientras permanece activo.",
fix: "Utilice los pines de ADC1 para sensores analógicos que funcionen junto con Wi-Fi.",
},
{
code: "Flash Read err / bootloader",
cause: "Tensión o modo de flash incorrectos, o pin de strapping forzado en el arranque.",
fix: "Compruebe la línea de 3,3 V, los pull-ups de arranque y la configuración de flash en esptool.",
},
];
export default async function Home({ params }) {
const { locale } = await params;
const isPt = locale === "pt";
const isEn = locale === "en";
const isEs = locale === "es";
const getLinkHref = (href) => getLocalizedHref(`/${locale}${href}`, locale);
const seriesData = isPt ? seriesDataPt : seriesDataEn;
const frameworksData = isPt ? frameworksDataPt : frameworksDataEn;
const tools = isPt ? TOOLS_PT : TOOLS_EN;
const errors = isPt ? ERRORS_PT : ERRORS_EN;
const seriesData = isPt ? seriesDataPt : isEs ? seriesDataEs : seriesDataEn;
const frameworksData = isPt ? frameworksDataPt : isEs ? frameworksDataEs : frameworksDataEn;
const tools = isPt ? TOOLS_PT : isEs ? TOOLS_ES : TOOLS_EN;
const errors = isPt ? ERRORS_PT : isEs ? ERRORS_ES : ERRORS_EN;
const seriesCount = Object.keys(seriesData).length;
const frameworkCount = Object.keys(frameworksData).length;
@@ -181,7 +253,7 @@ export default async function Home({ params }) {
<div className="mx-auto max-w-4xl text-center">
<span className="chip chip-accent">
<Cpu className="h-3.5 w-3.5" aria-hidden="true" />
{isPt ? "Documentação aberta do ESP32" : "Open ESP32 documentation"}
{isPt ? "Documentação aberta do ESP32" : isEs ? "Documentación abierta de ESP32" : "Open ESP32 documentation"}
</span>
<h1 className="display mt-6 text-[clamp(2.9rem,7.4vw,5.4rem)]">
{isPt ? (
@@ -189,6 +261,11 @@ export default async function Home({ params }) {
O ESP32,
<br className="sm:hidden" /> sem ruído.
</>
) : isEs ? (
<>
El ESP32,
<br className="sm:hidden" /> sin ruido.
</>
) : (
<>
The ESP32,
@@ -199,17 +276,21 @@ export default async function Home({ params }) {
<p className="lede mx-auto mt-6 max-w-2xl">
{isPt
? "Especificações, pinagem, comparações e diagnóstico em uma leitura só. Menos tradução de datasheet, mais tempo com o protótipo na bancada."
: isEs
? "Especificaciones, patillaje, comparaciones y diagnóstico en una sola lectura. Menos traducción de hojas de datos, más tiempo con el prototipo en el banco de trabajo."
: "Specifications, pinouts, comparisons and diagnostics in a single read. Less datasheet translation, more workbench time."}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href={getLinkHref("/seletor")} className="btn btn-primary w-full sm:w-auto">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
{isPt ? "Escolher meu chip" : "Choose my chip"}
{isPt ? "Escolher meu chip" : isEs ? "Elegir mi chip" : "Choose my chip"}
</Link>
<Link href={getLinkHref("/series")} className="btn btn-ghost w-full sm:w-auto">
{isPt ? (
<>Explorar as {seriesCount} séries</>
) : isEs ? (
<>Explorar las {seriesCount} series</>
) : (
<>Explore the {seriesCount} series</>
)}
@@ -218,7 +299,7 @@ export default async function Home({ params }) {
</div>
<p className="mt-7 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-muted">
<span><strong className="font-semibold text-ink">{seriesCount}</strong> {isPt ? "séries" : "series"}</span>
<span><strong className="font-semibold text-ink">{seriesCount}</strong> {isPt ? "séries" : isEs ? "series" : "series"}</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span>
{isPt ? (
@@ -228,6 +309,11 @@ export default async function Home({ params }) {
<span aria-hidden="true" className="text-line-strong">·</span>
<span>100% em português</span>
</>
) : isEs ? (
<>
<span aria-hidden="true" className="text-line-strong">·</span>
<span>100% en español</span>
</>
) : (
<>
<span aria-hidden="true" className="text-line-strong">·</span>
@@ -240,13 +326,13 @@ export default async function Home({ params }) {
<div className="panel mt-12 overflow-hidden sm:mt-16">
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-4 sm:px-7">
<div>
<h2 className="text-sm font-semibold text-ink">{isPt ? "Séries em destaque" : "Featured series"}</h2>
<h2 className="text-sm font-semibold text-ink">{isPt ? "Séries em destaque" : isEs ? "Series destacadas" : "Featured series"}</h2>
<p className="mt-0.5 text-xs text-muted">
{isPt ? "O essencial de cada família, antes de abrir o datasheet." : "The essentials of each family before opening the datasheet."}
{isPt ? "O essencial de cada família, antes de abrir o datasheet." : isEs ? "Lo esencial de cada familia, antes de abrir la hoja de datos." : "The essentials of each family before opening the datasheet."}
</p>
</div>
<Link href={getLinkHref("/series")} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
{isPt ? "Índice completo" : "Full index"}
{isPt ? "Índice completo" : isEs ? "Índice completo" : "Full index"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -271,7 +357,7 @@ export default async function Home({ params }) {
<div className="mt-5 flex items-center justify-between gap-3 border-t border-line pt-3.5">
<span className="truncate font-mono text-[0.6875rem] text-ink-soft">{serie.frequencia}</span>
<span className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent">
{isPt ? "Ficha" : "Spec sheet"}
{isPt ? "Ficha" : isEs ? "Ficha" : "Spec sheet"}
<ArrowUpRight className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</div>
@@ -352,13 +438,15 @@ export default async function Home({ params }) {
<section className="shell section" aria-labelledby="ferramentas-titulo">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow">{isPt ? "Ferramentas" : "Tools"}</p>
<p className="eyebrow">{isPt ? "Ferramentas" : isEs ? "Herramientas" : "Tools"}</p>
<h2 id="ferramentas-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
{isPt ? "Comece pelo problema." : "Start with the problem."}
{isPt ? "Comece pelo problema." : isEs ? "Empiece por el problema." : "Start with the problem."}
</h2>
<p className="lede mt-4">
{isPt
? "Cada ferramenta resolve uma etapa concreta do projeto, sem cadastro e sem rodeio."
: isEs
? "Cada herramienta resuelve una fase concreta del proyecto, sin registros ni rodeos."
: "Each tool addresses a concrete development phase, with no sign-ups and zero fluff."}
</p>
</div>
@@ -386,18 +474,20 @@ export default async function Home({ params }) {
<section className="shell section pt-0" aria-labelledby="diagnostico-titulo">
<div className="grid items-start gap-10 lg:grid-cols-[0.78fr_1.22fr] lg:gap-16">
<div className="lg:sticky lg:top-28">
<p className="eyebrow">{isPt ? "Diagnóstico" : "Diagnostics"}</p>
<p className="eyebrow">{isPt ? "Diagnóstico" : isEs ? "Diagnóstico" : "Diagnostics"}</p>
<h2 id="diagnostico-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
{isPt ? "O primeiro boot não precisa virar noite." : "First boot doesn't have to take all night."}
{isPt ? "O primeiro boot não precisa virar noite." : isEs ? "El primer arranque no tiene por qué desvelarlo." : "First boot doesn't have to take all night."}
</h2>
<p className="lede mt-4">
{isPt
? "Os erros mais comuns têm causa conhecida. O guia parte da mensagem no terminal e chega ao ajuste de hardware ou firmware."
: isEs
? "Los errores más comunes tienen una causa conocida. Esta guía parte del mensaje en el terminal y llega al ajuste de hardware o firmware."
: "Common errors have known causes. This guide starts with the serial terminal log and leads to the hardware or firmware adjustment."}
</p>
<Link href={getLinkHref("/diagnostico")} className="btn btn-accent mt-7">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
{isPt ? "Abrir os 30+ diagnósticos" : "Open the 30+ diagnostics"}
{isPt ? "Abrir os 30+ diagnósticos" : isEs ? "Abrir los 30+ diagnósticos" : "Open the 30+ diagnostics"}
</Link>
</div>
@@ -407,7 +497,7 @@ export default async function Home({ params }) {
<Terminal className="h-4 w-4" aria-hidden="true" />
boot.log
</span>
<span className="text-xs text-[#a6afbd]">{isPt ? "4 exemplos de 30+" : "4 examples of 30+"}</span>
<span className="text-xs text-[#a6afbd]">{isPt ? "4 exemplos de 30+" : isEs ? "4 ejemplos de 30+" : "4 examples of 30+"}</span>
</div>
<div className="divide-y divide-white/10">
{errors.map((item) => (
@@ -424,22 +514,24 @@ export default async function Home({ params }) {
<section className="shell pb-20 pt-4 md:pb-28">
<div className="mx-auto max-w-3xl text-center">
<p className="eyebrow">{isPt ? "Curadoria independente" : "Independent curation"}</p>
<p className="eyebrow">{isPt ? "Curadoria independente" : isEs ? "Curaduría independiente" : "Independent curation"}</p>
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
{isPt ? "Feito para consultar no meio do projeto." : "Built for quick lookup mid-project."}
{isPt ? "Feito para consultar no meio do projeto." : isEs ? "Diseñado para consultar en mitad del proyecto." : "Built for quick lookup mid-project."}
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
{isPt
? "O ESPDocs organiza dados públicos da Espressif e aponta sempre para a fonte oficial. Para decisões críticas, o datasheet e o Technical Reference Manual continuam sendo a palavra final."
: isEs
? "ESPDocs organiza datos públicos de Espressif y apunta siempre a la fuente oficial. Para decisiones críticas, la hoja de datos y el Technical Reference Manual continúan teniendo la última palabra."
: "ESPDocs organizes public Espressif data and points directly to official sources. For critical engineering decisions, the datasheet and Technical Reference Manual remain the definitive authority."}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href={getLinkHref("/glossario")} className="btn btn-ghost w-full sm:w-auto">
<BookOpen className="h-4 w-4" aria-hidden="true" />
{isPt ? "Glossário" : "Glossary"}
{isPt ? "Glossário" : isEs ? "Glosario" : "Glossary"}
</Link>
<Link href={getLinkHref("/sobre")} className="btn btn-primary w-full sm:w-auto">
{isPt ? "Sobre o projeto" : "About the project"}
{isPt ? "Sobre o projeto" : isEs ? "Acerca del proyecto" : "About the project"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
+36 -9
View File
@@ -3,14 +3,19 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn ? "Which ESP32 to Choose" : "Qual ESP32 escolher",
descricao: isEn
titulo: isEs ? "Qué ESP32 elegir" : isEn ? "Which ESP32 to Choose" : "Qual ESP32 escolher",
descricao: isEs
? "Responda a preguntas sencillas sobre conectividad (Wi-Fi 6, 5GHz, Matter), energía, periféricos y seguridad o use el filtro paramétrico para encontrar el ESP32 perfecto."
: isEn
? "Answer simple questions about connectivity (Wi-Fi 6, 5GHz, Matter), power, peripherals, and security or use the parametric filter to find the perfect ESP32."
: "Responda a perguntas simples sobre conectividade (Wi-Fi 6, 5GHz, Matter), energia, periféricos e segurança ou use o filtro paramétrico para encontrar o ESP32 perfeito.",
caminho: "/seletor",
locale,
keywords: isEn
keywords: isEs
? ["qué ESP32 elegir", "cuál ESP32 usar", "elegir microcontrolador", "ESP32 para mi proyecto", "comparar ESP32"]
: isEn
? ["which ESP32 to choose", "which ESP32 to use", "choose microcontroller", "ESP32 for my project", "compare ESP32"]
: ["qual ESP32 escolher", "qual ESP32 usar", "escolher microcontrolador", "ESP32 para meu projeto", "comparar ESP32"],
});
@@ -54,10 +59,30 @@ const faqsEn = [
},
];
const faqsEs = [
{
pergunta: "¿Cuál ESP32 elegir para principiantes?",
resposta: "Para quienes se inician en el mundo Maker e IoT, el ESP32 clásico (NodeMCU/DevKitC) o el ESP32-C3 son las mejores opciones gracias al amplio soporte de bibliotecas, ejemplos listos en Arduino IDE y abundante documentación.",
},
{
pergunta: "¿Cuál ESP32 soporta Wi-Fi 6, Bluetooth 6 y Thread/Zigbee/Matter?",
resposta: "El ESP32-C6 y el ESP32-C5 son las series destacadas para redes modernas. El ESP32-C6 ofrece Wi-Fi 6 (2.4 GHz) y radio 802.15.4 (Zigbee 3.0 / Thread / Matter). El ESP32-C5 es el primero en ofrecer soporte para Wi-Fi 6 de doble banda operando en 2.4 GHz y 5 GHz simultáneamente.",
},
{
pergunta: "¿Cuál es el ESP32 más potente para Inteligencia Artificial (TinyML) y Pantallas?",
resposta: "El ESP32-P4 (dual-core RISC-V a 400 MHz con extensiones vectoriales de IA, interfaces MIPI-CSI/DSI para cámaras/pantallas y hasta 64 MB de PSRAM) y el ESP32-S3 (dual-core Xtensa a 240 MHz con aceleradores vectoriales para redes neuronales y soporte para LCD RGB) son los más rápidos para IA en el borde e interfaces gráficas avanzadas.",
},
{
pergunta: "¿Cuál ESP32 consume menos energía para proyectos a batería?",
resposta: "Los microcontroladores con arquitectura RISC-V compacta y coprocesador ULP, como el ESP32-C2, ESP32-C3 y ESP32-H2 (dedicado a 802.15.4 sin radio Wi-Fi), ofrecen corrientes de deep sleep extremadamente bajas (en el rango de 5 µA a 10 µA).",
},
];
export default async function Layout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const faqs = isEn ? faqsEn : faqsPt;
const isEs = locale === "es";
const faqs = isEs ? faqsEs : isEn ? faqsEn : faqsPt;
return (
<>
@@ -66,11 +91,13 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEn ? "Intelligent ESP32 Selector" : "Seletor Inteligente de ESP32",
descricao: isEn
nome: isEs ? "Selector Inteligente de ESP32" : isEn ? "Intelligent ESP32 Selector" : "Seletor Inteligente de ESP32",
descricao: isEs
? "Herramienta interactiva que analiza conectividad, consumo, periféricos y seguridad para recomendar la serie ideal de ESP32."
: isEn
? "Interactive tool analyzing connectivity, power consumption, peripherals, and security to recommend the ideal ESP32 series."
: "Ferramenta interativa que analisa conectividade, consumo, periféricos e segurança para indicar a série ideal de ESP32.",
caminho: isEn ? "/en/selector" : "/seletor",
caminho: isEs ? "/es/selector" : isEn ? "/en/selector" : "/seletor",
locale,
})
),
@@ -81,8 +108,8 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "ESP32 Selector" : "Seletor de ESP32", caminho: isEn ? "/en/selector" : "/seletor" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: isEs ? "Selector de ESP32" : isEn ? "ESP32 Selector" : "Seletor de ESP32", caminho: isEs ? "/es/selector" : isEn ? "/en/selector" : "/seletor" },
])
),
}}
File diff suppressed because it is too large. Load diff
+187 -67
View File
@@ -7,6 +7,7 @@ import PageHero from "@/components/PageHero";
import { notFound } from "next/navigation";
import seriesDataPt from "@/public/series.json";
import seriesDataEn from "@/data/en/series.json";
import seriesDataEs from "@/data/es/series.json";
import ConnectionsDiagram from "@/components/ConnectionsDiagram";
import SeriesTabMenu from "@/components/SeriesTabMenu";
import PinRestrictions from "@/components/PinRestrictions";
@@ -35,8 +36,9 @@ import {
export default async function SerieDetail({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const seriesData = isEn ? seriesDataEn : seriesDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const seriesData = isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
const serie = seriesData[key];
if (!serie) {
@@ -45,7 +47,11 @@ export default async function SerieDetail({ params }) {
let conexoes = [];
try {
conexoes = isEn ? require(`@/data/en/conexoes/${key}.json`) : require(`@/public/conexoes/${key}.json`);
conexoes = isEs
? require(`@/data/es/conexoes/${key}.json`)
: isEn
? require(`@/data/en/conexoes/${key}.json`)
: require(`@/public/conexoes/${key}.json`);
} catch (error) {
console.log(`Arquivo de conexões não encontrado para ${key}`);
}
@@ -55,10 +61,18 @@ export default async function SerieDetail({ params }) {
const previousKey = currentIndex > 0 ? seriesKeys[currentIndex - 1] : null;
const nextKey = currentIndex < seriesKeys.length - 1 ? seriesKeys[currentIndex + 1] : null;
const nao = isEn ? "No" : "Não";
const naoInformado = isEn ? "Not informed" : "Não informado";
const nao = isEs || isEn ? "No" : "Não";
const naoInformado = isEs ? "No informado" : isEn ? "Not informed" : "Não informado";
const processadorSpecs = isEn
const processadorSpecs = isEs
? [
{ label: "Arquitectura", value: serie.arquitetura },
{ label: "Núcleos", value: serie.nucleos },
{ label: "Frecuencia", value: serie.frequencia },
{ label: "Coprocesador ULP", value: serie.coprocessador_ulp || nao },
{ label: "Aceleradores de IA", value: serie.aceleradores_ia || nao },
]
: isEn
? [
{ label: "Architecture", value: serie.arquitetura },
{ label: "Cores", value: serie.nucleos },
@@ -75,7 +89,22 @@ export default async function SerieDetail({ params }) {
];
const seg = serie.seguranca || {};
const segurancaSpecs = isEn
const segurancaSpecs = isEs
? [
{ label: "AES", value: seg.aes || nao },
{ label: "SHA", value: seg.sha || nao },
{ label: "RSA / MPI", value: seg.rsa || nao },
{ label: "ECC (curvas elípticas)", value: seg.ecc || nao },
{ label: "ECDSA en hardware", value: seg.ecdsa || nao },
{ label: "HMAC", value: seg.hmac || nao },
{ label: "Firma digital (DS)", value: seg.assinatura_digital || nao },
{ label: "Key Manager", value: seg.key_manager || nao },
{ label: "Generador aleatorio (TRNG)", value: seg.rng || nao },
{ label: "Cifrado de flash", value: seg.criptografia_flash || nao },
{ label: "Secure Boot", value: seg.secure_boot || nao },
{ label: "Protección contra DPA", value: seg.protecao_dpa || nao },
]
: isEn
? [
{ label: "AES", value: seg.aes || nao },
{ label: "SHA", value: seg.sha || nao },
@@ -105,7 +134,15 @@ export default async function SerieDetail({ params }) {
{ label: "Proteção contra DPA", value: seg.protecao_dpa || "Não" },
];
const ferramentasSpecs = isEn
const ferramentasSpecs = isEs
? [
{ label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || naoInformado },
{ label: "Core Arduino", value: serie.arduino_core || naoInformado },
{ label: "Depuración", value: serie.depuracao || naoInformado },
{ label: "Encapsulado", value: serie.encapsulamento || naoInformado },
{ label: "Módulos disponibles", value: serie.modulos || naoInformado },
]
: isEn
? [
{ label: "Minimum ESP-IDF", value: serie.esp_idf_minimo || naoInformado },
{ label: "Arduino Core", value: serie.arduino_core || naoInformado },
@@ -121,7 +158,16 @@ export default async function SerieDetail({ params }) {
{ label: "Módulos disponíveis", value: serie.modulos || "Não informado" },
];
const conectividadeSpecs = isEn
const conectividadeSpecs = isEs
? [
{ label: "Wi-Fi", value: serie.wifi || nao },
{ label: "Bluetooth", value: serie.bluetooth || nao },
{ label: "Zigbee/Thread", value: serie.zigbee_thread || nao },
{ label: "Matter", value: serie.matter || nao },
{ label: "Ethernet", value: serie.ethernet || serie.ethernet_mac || nao },
{ label: "CAN", value: serie.can || nao },
]
: isEn
? [
{ label: "Wi-Fi", value: serie.wifi || nao },
{ label: "Bluetooth", value: serie.bluetooth || nao },
@@ -139,7 +185,15 @@ export default async function SerieDetail({ params }) {
{ label: "CAN", value: serie.can || "Não" },
];
const memoriaSpecs = isEn
const memoriaSpecs = isEs
? [
{ label: "SRAM", value: serie.memoria_sram },
{ label: "SRAM RTC", value: serie.memoria_sram_rtc },
{ label: "ROM", value: serie.memoria_rom },
{ label: "Flash externa", value: serie.flash_externa },
{ label: "PSRAM externa", value: serie.psram_externa || nao },
]
: isEn
? [
{ label: "SRAM", value: serie.memoria_sram },
{ label: "RTC SRAM", value: serie.memoria_sram_rtc },
@@ -155,7 +209,20 @@ export default async function SerieDetail({ params }) {
{ label: "PSRAM Externa", value: serie.psram_externa || "Não" },
];
const perifericos = isEn
const perifericos = isEs
? [
{ label: "GPIO", value: serie.gpio },
{ label: "ADC", value: serie.adc },
{ label: "DAC", value: serie.dac || nao },
{ label: "Touch", value: serie.touch || nao },
{ label: "UART", value: serie.uart },
{ label: "SPI", value: serie.spi },
{ label: "I2C", value: serie.i2c },
{ label: "I2S", value: serie.i2s },
{ label: "PWM", value: serie.pwm },
{ label: "USB", value: serie.usb || nao },
]
: isEn
? [
{ label: "GPIO", value: serie.gpio },
{ label: "ADC", value: serie.adc },
@@ -183,12 +250,12 @@ export default async function SerieDetail({ params }) {
const interfacesEspeciais = [];
if (serie.lcd) interfacesEspeciais.push({ label: "LCD", value: serie.lcd });
if (serie.camera) interfacesEspeciais.push({ label: isEn ? "Camera" : "Câmera", value: serie.camera });
if (serie.camera) interfacesEspeciais.push({ label: isEs ? "Cámara" : isEn ? "Camera" : "Câmera", value: serie.camera });
if (serie.mipi_csi) interfacesEspeciais.push({ label: "MIPI CSI", value: serie.mipi_csi });
if (serie.mipi_dsi) interfacesEspeciais.push({ label: "MIPI DSI", value: serie.mipi_dsi });
if (serie.sdio) interfacesEspeciais.push({ label: "SDIO", value: serie.sdio });
if (serie.sensor_hall) interfacesEspeciais.push({ label: isEn ? "Hall Sensor" : "Sensor Hall", value: serie.sensor_hall });
if (serie.sensor_temperatura) interfacesEspeciais.push({ label: isEn ? "Temperature Sensor" : "Sensor Temperatura", value: serie.sensor_temperatura });
if (serie.sensor_hall) interfacesEspeciais.push({ label: isEs ? "Sensor Hall" : isEn ? "Hall Sensor" : "Sensor Hall", value: serie.sensor_hall });
if (serie.sensor_temperatura) interfacesEspeciais.push({ label: isEs ? "Sensor de temperatura" : isEn ? "Temperature Sensor" : "Sensor Temperatura", value: serie.sensor_temperatura });
// Consumo de Energia
const consumoEnergia = [];
@@ -221,7 +288,21 @@ export default async function SerieDetail({ params }) {
'deep_sleep': 'Deep Sleep',
'hibernation': 'Hibernation'
};
const modoLabels = isEn ? modoLabelsEn : modoLabelsPt;
const modoLabelsEs = {
'active': 'Modo activo',
'active_cpu_240mhz': 'Activo (CPU 240MHz)',
'active_cpu_160mhz': 'Activo (CPU 160MHz)',
'active_wifi_tx': 'Activo (Wi-Fi TX)',
'active_wifi_tx_peak': 'Activo (Wi-Fi TX Pico)',
'active_wifi': 'Activo (Wi-Fi)',
'active_wifi6': 'Activo (Wi-Fi 6)',
'modem_sleep': 'Modem Sleep',
'modem_sleep_cpu_160mhz': 'Modem Sleep (CPU 160MHz)',
'light_sleep': 'Light Sleep',
'deep_sleep': 'Deep Sleep',
'hibernation': 'Hibernación'
};
const modoLabels = isEs ? modoLabelsEs : isEn ? modoLabelsEn : modoLabelsPt;
Object.entries(serie.consumo_energia).forEach(([modo, consumo]) => {
consumoEnergia.push({
@@ -234,17 +315,17 @@ export default async function SerieDetail({ params }) {
const tabs = [
{
id: "devboard",
label: isEn ? "Development board" : "Placa de desenvolvimento",
label: isEs ? "Placa de desarrollo" : isEn ? "Development board" : "Placa de desenvolvimento",
available: serie.placa,
content: (
<div className="card p-5 sm:p-7">
<h2 className="text-lg font-semibold text-ink">
{isEn ? "Official development kit" : "Kit de desenvolvimento oficial"}
{isEs ? "Kit de desarrollo oficial" : isEn ? "Official development kit" : "Kit de desenvolvimento oficial"}
</h2>
<div className="mt-5 overflow-hidden rounded-xl border border-line bg-surface-2 p-4">
<Image
src={serie.placa}
alt={isEn ? `Development board ${key}` : `Placa de desenvolvimento ${key}`}
alt={isEs ? `Placa de desarrollo ${key}` : isEn ? `Development board ${key}` : `Placa de desenvolvimento ${key}`}
width={1200}
height={800}
className="mx-auto h-auto max-h-[28rem] w-full rounded-lg object-contain"
@@ -252,7 +333,9 @@ export default async function SerieDetail({ params }) {
/>
</div>
<p className="mt-3 text-sm text-muted">
{isEn ? (
{isEs ? (
<>Placa estándar de Espressif para la serie {key}.</>
) : isEn ? (
<>Official Espressif board for the {key} series.</>
) : (
<>Placa padrão da Espressif para a série {key}.</>
@@ -263,7 +346,7 @@ export default async function SerieDetail({ params }) {
},
{
id: "connections",
label: isEn ? "Pinout diagram" : "Diagrama de conexões",
label: isEs ? "Diagrama de patillaje" : isEn ? "Pinout diagram" : "Diagrama de conexões",
available: conexoes.length > 0,
content: (
<div className="card p-5 sm:p-7">
@@ -271,10 +354,12 @@ export default async function SerieDetail({ params }) {
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">
{isEn ? "This is the chip diagram, not the development board" : "Este é o diagrama do chip, não da placa"}
{isEs ? "Este es el diagrama del chip, no de la placa" : isEn ? "This is the chip diagram, not the development board" : "Este é o diagrama do chip, não da placa"}
</p>
<p className="mt-1">
{isEn ? (
{isEs ? (
`El mapa muestra las conexiones del silicio del ${key}. Cada placa de desarrollo expone solo parte de ellas o las enruta de otra forma.`
) : isEn ? (
`The map shows the silicon connections for the ${key}. Each development board exposes only a subset or routes them differently.`
) : (
<>
@@ -293,10 +378,26 @@ export default async function SerieDetail({ params }) {
<div className="callout mt-8">
<p className="flex items-center gap-2 font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
{isEn ? "Almost every pin is reconfigurable" : "Quase todo pino é reconfigurável"}
{isEs ? "Casi todos los pines son reconfigurables" : isEn ? "Almost every pin is reconfigurable" : "Quase todo pino é reconfigurável"}
</p>
<p className="mt-1">
{isEn ? (
{isEs ? (
<>
La matriz de E/S del {key} permite mover buses SPI, I2C, UART, PWM y cámara/pantalla
a la mayoría de los pines digitales. Para las restricciones de strapping y pines analógicos,
consulte la{" "}
<a
href={`https://docs.espressif.com/projects/esp-idf/en/latest/${String(key).toLowerCase().replace("-", "")}/api-reference/peripherals/gpio.html`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
>
guía oficial de GPIO
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
.
</>
) : isEn ? (
<>
The {key} IO matrix allows moving SPI, I2C, UART, PWM, and camera/display buses
to most digital pins. For strapping restrictions and analog pins, consult the{" "}
@@ -336,42 +437,44 @@ export default async function SerieDetail({ params }) {
];
const secoes = [
{ id: "processador", titulo: isEn ? "Processor" : "Processador", icon: Cpu, specs: processadorSpecs },
{ id: "conectividade", titulo: isEn ? "Connectivity" : "Conectividade", icon: Wifi, specs: conectividadeSpecs },
{ id: "ferramentas", titulo: isEn ? "Tools and availability" : "Ferramentas e disponibilidade", icon: Package, specs: ferramentasSpecs },
{ id: "seguranca", titulo: isEn ? "Security" : "Segurança", icon: ShieldCheck, specs: segurancaSpecs },
{ id: "memoria", titulo: isEn ? "Memory" : "Memória", icon: Database, specs: memoriaSpecs },
{ id: "processador", titulo: isEs ? "Procesador" : isEn ? "Processor" : "Processador", icon: Cpu, specs: processadorSpecs },
{ id: "conectividade", titulo: isEs ? "Conectividad" : isEn ? "Connectivity" : "Conectividade", icon: Wifi, specs: conectividadeSpecs },
{ id: "ferramentas", titulo: isEs ? "Herramientas y disponibilidad" : isEn ? "Tools and availability" : "Ferramentas e disponibilidade", icon: Package, specs: ferramentasSpecs },
{ id: "seguranca", titulo: isEs ? "Seguridad" : isEn ? "Security" : "Segurança", icon: ShieldCheck, specs: segurancaSpecs },
{ id: "memoria", titulo: isEs ? "Memoria" : isEn ? "Memory" : "Memória", icon: Database, specs: memoriaSpecs },
...(consumoEnergia.length > 0
? [{
id: "energia",
titulo: isEn ? "Power consumption" : "Consumo de energia",
titulo: isEs ? "Consumo de energía" : isEn ? "Power consumption" : "Consumo de energia",
icon: Battery,
specs: consumoEnergia,
nota: isEn
nota: isEs
? "Valores típicos a 3,3 V y 25 °C. El consumo real depende del ciclo de trabajo de la radio y de la frecuencia de reloj configurada."
: isEn
? "Typical values at 3.3 V and 25 °C. Actual power consumption depends on radio duty cycle and configured clock frequency."
: "Valores típicos a 3,3 V e 25 °C. O consumo real depende do ciclo de trabalho do rádio e da frequência de clock configurada.",
}]
: []),
{ id: "perifericos", titulo: isEn ? "Peripherals" : "Periféricos", icon: Sliders, specs: perifericos },
{ id: "perifericos", titulo: isEs ? "Periféricos" : isEn ? "Peripherals" : "Periféricos", icon: Sliders, specs: perifericos },
...(interfacesEspeciais.length > 0
? [{ id: "interfaces", titulo: isEn ? "Special interfaces" : "Interfaces especiais", icon: Sliders, specs: interfacesEspeciais }]
? [{ id: "interfaces", titulo: isEs ? "Interfaces especiales" : isEn ? "Special interfaces" : "Interfaces especiais", icon: Sliders, specs: interfacesEspeciais }]
: []),
{
id: "temperatura",
titulo: isEn ? "Temperature limits" : "Limites de temperatura",
titulo: isEs ? "Límites de temperatura" : isEn ? "Temperature limits" : "Limites de temperatura",
icon: Thermometer,
specs: [
{ label: isEn ? "Operating" : "Operação", value: serie.temperatura_operacao },
{ label: isEn ? "Storage" : "Armazenamento", value: serie.temperatura_armazenamento },
{ label: isEs ? "Operación" : isEn ? "Operating" : "Operação", value: serie.temperatura_operacao },
{ label: isEs ? "Almacenamiento" : isEn ? "Storage" : "Armazenamento", value: serie.temperatura_armazenamento },
],
},
];
const fatos = [
{ label: isEn ? "Processor" : "Processador", value: String(serie.nucleos).includes("2") ? "Dual-core" : "Single-core", detail: serie.frequencia },
{ label: "SRAM", value: serie.memoria_sram, detail: isEn ? `+ ${serie.memoria_sram_rtc} RTC SRAM` : `+ ${serie.memoria_sram_rtc} de SRAM RTC` },
{ label: "GPIO", value: serie.gpio, detail: isEn ? "programmable pins" : "pinos programáveis" },
{ label: isEn ? "Architecture" : "Arquitetura", value: String(serie.arquitetura).includes("RISC-V") ? "RISC-V" : "Xtensa", detail: serie.arquitetura },
{ label: isEs ? "Procesador" : isEn ? "Processor" : "Processador", value: String(serie.nucleos).includes("2") ? "Dual-core" : "Single-core", detail: serie.frequencia },
{ label: "SRAM", value: serie.memoria_sram, detail: isEs ? `+ ${serie.memoria_sram_rtc} de SRAM RTC` : isEn ? `+ ${serie.memoria_sram_rtc} RTC SRAM` : `+ ${serie.memoria_sram_rtc} de SRAM RTC` },
{ label: "GPIO", value: serie.gpio, detail: isEs ? "pines programables" : isEn ? "programmable pins" : "pinos programáveis" },
{ label: isEs ? "Arquitectura" : isEn ? "Architecture" : "Arquitetura", value: String(serie.arquitetura).includes("RISC-V") ? "RISC-V" : "Xtensa", detail: serie.arquitetura },
];
return (
@@ -384,8 +487,8 @@ export default async function SerieDetail({ params }) {
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Series" : "Séries", caminho: getLocalizedHref("/series", currentLocale) },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEn ? "/en" : isEs ? "/es" : "/" },
{ nome: isEs ? "Series" : isEn ? "Series" : "Séries", caminho: getLocalizedHref("/series", currentLocale) },
{ nome: serie.nome_completo, caminho: getLocalizedHref(`/series/${key}`, currentLocale) },
])),
}}
@@ -396,7 +499,8 @@ export default async function SerieDetail({ params }) {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Series" : "Séries", href: getLocalizedHref("/series", currentLocale) }, { label: key }]}
isEs={isEs}
trail={[{ label: isEs ? "Series" : isEn ? "Series" : "Séries", href: getLocalizedHref("/series", currentLocale) }, { label: key }]}
icon={Cpu}
label={serie.arquitetura}
title={key}
@@ -405,32 +509,32 @@ export default async function SerieDetail({ params }) {
{serie.datasheet ? (
<a href={serie.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<FileText className="h-4 w-4" aria-hidden="true" />
{isEn ? "Official datasheet" : "Datasheet oficial"}
{isEs ? "Datasheet oficial" : isEn ? "Official datasheet" : "Datasheet oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
) : (
<span className="btn btn-ghost cursor-not-allowed opacity-70" aria-disabled="true">
<FileText className="h-4 w-4" aria-hidden="true" />
{isEn ? "Datasheet not yet published" : "Datasheet ainda não publicado"}
{isEs ? "Datasheet aún no publicado" : isEn ? "Datasheet not yet published" : "Datasheet ainda não publicado"}
</span>
)}
{serie.guia_de_programacao && (
<a href={serie.guia_de_programacao} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
{isEn ? "Programming guide" : "Guia de programação"}
{isEs ? "Guía de programación" : isEn ? "Programming guide" : "Guia de programação"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{serie.manual_tecnico && (
<a href={serie.manual_tecnico} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<BookOpen className="h-4 w-4" aria-hidden="true" />
{isEn ? "Technical reference manual" : "Manual técnico"}
{isEs ? "Manual de referencia técnica" : isEn ? "Technical reference manual" : "Manual técnico"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
<section className="shell mt-10" aria-label={isEn ? "Summary" : "Resumo"}>
<section className="shell mt-10" aria-label={isEs ? "Resumen" : isEn ? "Summary" : "Resumo"}>
<dl className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{fatos.map((fato) => (
<div key={fato.label} className="tile">
@@ -446,7 +550,7 @@ export default async function SerieDetail({ params }) {
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">
{isEn ? "Preliminary documentation" : "Documentação preliminar"}
{isEs ? "Documentación preliminar" : isEn ? "Preliminary documentation" : "Documentação preliminar"}
</p>
<p className="mt-1">{serie.status_documentacao}</p>
</div>
@@ -455,13 +559,13 @@ export default async function SerieDetail({ params }) {
</section>
<div className="shell mt-10">
<SeriesTabMenu tabs={tabs} isEn={isEn} />
<PinRestrictions conexoes={conexoes} serieKey={key} isEn={isEn} />
<SeriesTabMenu tabs={tabs} isEn={isEn} isEs={isEs} />
<PinRestrictions conexoes={conexoes} serieKey={key} isEn={isEn} isEs={isEs} />
</div>
<div className="shell mt-12">
<nav
aria-label={isEn ? "Datasheet sections" : "Seções da ficha"}
aria-label={isEs ? "Secciones de la ficha" : isEn ? "Datasheet sections" : "Seções da ficha"}
className="nav-pill no-scrollbar sticky top-20 z-30 flex gap-1 overflow-x-auto rounded-full p-1.5"
>
{secoes.map((secao) => (
@@ -482,30 +586,34 @@ export default async function SerieDetail({ params }) {
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">
{isEn ? (
{isEs ? (
<>¿Listo para prototipar con {key}?</>
) : isEn ? (
<>Ready to prototype with {key}?</>
) : (
<>Pronto para prototipar o {key}?</>
)}
</h2>
<p className="mt-3 text-base text-muted">
{isEn
{isEs
? "Explore las placas de desarrollo disponibles para esta serie, con USB, sensores integrados y enlaces a la tienda oficial."
: isEn
? "Explore development boards available for this series, with USB, onboard sensors, and official store links."
: "Veja as placas de desenvolvimento disponíveis para a série, com USB, sensores integrados e link para a loja oficial."}
</p>
</div>
<Link href={getLocalizedHref("/catalogo", currentLocale)} className="btn btn-primary shrink-0">
{isEn ? "View boards" : "Ver placas"}
{isEs ? "Ver placas" : isEn ? "View boards" : "Ver placas"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<nav aria-label={isEn ? "Other series" : "Outras séries"} className="mt-6 grid gap-3 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
<nav aria-label={isEs ? "Otras series" : isEn ? "Other series" : "Outras séries"} className="mt-6 grid gap-3 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
{previousKey ? (
<Link href={getLocalizedHref(`/series/${previousKey}`, currentLocale)} className="card card-interactive group flex items-center gap-3 px-5 py-4">
<ArrowLeft className="h-4 w-4 text-muted transition-transform group-hover:-translate-x-1" aria-hidden="true" />
<span>
<span className="block text-xs text-muted">{isEn ? "Previous series" : "Série anterior"}</span>
<span className="block text-xs text-muted">{isEs ? "Serie anterior" : isEn ? "Previous series" : "Série anterior"}</span>
<span className="block font-display text-lg font-semibold text-ink">{previousKey}</span>
</span>
</Link>
@@ -514,12 +622,12 @@ export default async function SerieDetail({ params }) {
)}
<Link href={getLocalizedHref("/comparacao", currentLocale)} className="btn btn-ghost justify-self-center">
<Scale className="h-4 w-4" aria-hidden="true" />
{isEn ? "Compare series" : "Comparar séries"}
{isEs ? "Comparar series" : isEn ? "Compare series" : "Comparar séries"}
</Link>
{nextKey ? (
<Link href={getLocalizedHref(`/series/${nextKey}`, currentLocale)} className="card card-interactive group flex items-center justify-end gap-3 px-5 py-4 text-right">
<span>
<span className="block text-xs text-muted">{isEn ? "Next series" : "Próxima série"}</span>
<span className="block text-xs text-muted">{isEs ? "Siguiente serie" : isEn ? "Next series" : "Próxima série"}</span>
<span className="block font-display text-lg font-semibold text-ink">{nextKey}</span>
</span>
<ArrowRight className="h-4 w-4 text-muted transition-transform group-hover:translate-x-1" aria-hidden="true" />
@@ -564,12 +672,13 @@ function SpecSection({ id, titulo, icon: Icon, specs, nota }) {
export async function generateMetadata({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const seriesData = isEn ? seriesDataEn : seriesDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const seriesData = isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
const serie = seriesData[key];
if (!serie) {
return { title: isEn ? "Series not found" : "Série não encontrada", robots: { index: false, follow: false } };
return { title: isEs ? "Serie no encontrada" : isEn ? "Series not found" : "Série não encontrada", robots: { index: false, follow: false } };
}
const radio = [
@@ -578,7 +687,12 @@ export async function generateMetadata({ params }) {
serie.zigbee_thread && !String(serie.zigbee_thread).startsWith("Não") && !String(serie.zigbee_thread).startsWith("No") ? "802.15.4" : null,
].filter(Boolean).join(", ");
const descricao = isEn
const descricao = isEs
? `${serie.nome_completo}: ${serie.arquitetura}, ${serie.frequencia}, ` +
`${serie.memoria_sram} de SRAM y ${serie.gpio} GPIO` +
(radio ? `. Radio: ${radio}` : "") +
`. Patillaje completo, periféricos, seguridad por hardware y pines restringidos, en español.`
: isEn
? `${serie.nome_completo}: ${serie.arquitetura}, ${serie.frequencia}, ` +
`${serie.memoria_sram} SRAM, and ${serie.gpio} GPIOs` +
(radio ? `. Radio: ${radio}` : "") +
@@ -589,10 +703,16 @@ export async function generateMetadata({ params }) {
`. Pinagem completa, periféricos, segurança em hardware e pinos com restrição, em português.`;
const imagem = serie.placa
? [{ url: serie.placa, width: 1600, height: 1091, alt: isEn ? `Pinout of ${serie.nome_completo}` : `Pinagem do ${serie.nome_completo}` }]
? [{ url: serie.placa, width: 1600, height: 1091, alt: isEs ? `Patillaje de ${serie.nome_completo}` : isEn ? `Pinout of ${serie.nome_completo}` : `Pinagem do ${serie.nome_completo}` }]
: [{ url: "/opengraph-image.png", width: 1200, height: 630, alt: "ESPDocs" }];
const keywords = isEn
const keywords = isEs
? [
serie.nome_completo, `${serie.nome_completo} pinout`, `${serie.nome_completo} patillaje`,
`${serie.nome_completo} datasheet`, `${serie.nome_completo} especificaciones`,
`${serie.nome_completo} español`, "ESP32", "Espressif", "microcontrolador",
]
: isEn
? [
serie.nome_completo, `${serie.nome_completo} pinout`,
`${serie.nome_completo} datasheet`, `${serie.nome_completo} specifications`,
@@ -606,14 +726,14 @@ export async function generateMetadata({ params }) {
const meta = {
...paginaMeta({
titulo: isEn ? `${serie.nome_completo}: specifications and pinout` : `${serie.nome_completo}: especificações e pinagem`,
titulo: isEs ? `${serie.nome_completo}: especificaciones y patillaje` : isEn ? `${serie.nome_completo}: specifications and pinout` : `${serie.nome_completo}: especificações e pinagem`,
descricao,
caminho: `/series/${key}`,
tipo: "article",
locale: currentLocale,
keywords,
}),
other: { "article:section": isEn ? "ESP32 Series" : "Séries ESP32" },
other: { "article:section": isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32" },
};
meta.openGraph = { ...meta.openGraph, images: imagem };
+24 -7
View File
@@ -3,19 +3,24 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const meta = paginaMeta({
titulo: isEn ? "ESP32 Series: Technical Guide and Pinouts" : "Séries ESP32",
descricao: isEn
titulo: isEs ? "Series ESP32: Guía técnica y patillaje" : isEn ? "ESP32 Series: Technical Guide and Pinouts" : "Séries ESP32",
descricao: isEs
? "Las 12 series de ESP32 lado a lado: ESP32, S2, S3, S31, C2, C3, C5, C6, C61, P4, H2 y H4. Arquitectura, memoria, radio, periféricos y patillaje de cada una."
: isEn
? "All 12 ESP32 series side by side: ESP32, S2, S3, S31, C2, C3, C5, C6, C61, P4, H2 and H4. Architecture, memory, radio, peripherals and pinouts for each one."
: "As 12 séries de ESP32 lado a lado: ESP32, S2, S3, S31, C2, C3, C5, C6, C61, P4, H2 e H4. Arquitetura, memória, rádio, periféricos e pinagem de cada uma, em português.",
caminho: "/series",
locale,
keywords: isEn
keywords: isEs
? ["series ESP32", "modelos de ESP32", "ESP32 S3", "ESP32 C6", "ESP32 P4", "diferencia entre ESP32"]
: isEn
? ["ESP32 series", "ESP32 models", "ESP32 S3", "ESP32 C6", "ESP32 P4", "difference between ESP32"]
: ["séries ESP32", "modelos de ESP32", "ESP32 S3", "ESP32 C6", "ESP32 P4", "diferença entre ESP32"],
});
meta.title = {
default: isEn ? "ESP32 Series" : "Séries ESP32",
default: isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32",
template: "%s · ESPDocs",
};
return meta;
@@ -43,10 +48,22 @@ const faqsEn = [
},
];
const faqsEs = [
{
pergunta: "¿Cuántas series de ESP32 existen actualmente?",
resposta: "Actualmente, el ecosistema de Espressif cuenta con 12 series principales: ESP32 clásico, ESP32-S2, ESP32-S3, ESP32-S31, ESP32-C2, ESP32-C3, ESP32-C5, ESP32-C6, ESP32-C61, ESP32-P4, ESP32-H2 y ESP32-H4.",
},
{
pergunta: "¿Cuáles son las series más recientes de Espressif?",
resposta: "Las series más recientes incluyen el ESP32-P4 (enfocado en alto rendimiento con dual-core RISC-V a 400 MHz), el ESP32-C5 (primer chip con Wi-Fi de doble banda 2.4/5 GHz de Espressif) y el ESP32-C61 (Wi-Fi 6 y Bluetooth 6 de bajo costo).",
},
];
export default async function Layout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const faqs = isEn ? faqsEn : faqsPt;
const isEs = locale === "es";
const faqs = isEs ? faqsEs : isEn ? faqsEn : faqsPt;
return (
<>
@@ -55,8 +72,8 @@ export default async function Layout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "ESP32 Series" : "Séries ESP32", caminho: isEn ? "/en/series" : "/series" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32", caminho: isEs ? "/es/series" : isEn ? "/en/series" : "/series" },
])
),
}}
+51 -26
View File
@@ -8,6 +8,7 @@ import PageHero from "@/components/PageHero";
import Link from "next/link";
import seriesDataPt from "@/public/series.json";
import seriesDataEn from "@/data/en/series.json";
import seriesDataEs from "@/data/es/series.json";
import { getLocalizedHref } from "@/i18n/links";
import {
AlertTriangle,
@@ -36,29 +37,39 @@ const FILTROS_EN = [
{ id: "ia", rotulo: "AI Acceleration", testa: (s) => Boolean(s.aceleradores_ia) },
];
const FILTROS_ES = [
{ id: "todas", rotulo: "Todas", testa: () => true },
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Yes" || s.matter === "Sim" || s.matter === "Sí" },
{ id: "ia", rotulo: "Aceleración de IA", testa: (s) => Boolean(s.aceleradores_ia) },
];
/** Marcador de segurança: o recurso que diferencia a série, não o que todas têm. */
function nivelSeguranca(serie, isEn) {
function nivelSeguranca(serie, isEn, isEs) {
const seg = serie.seguranca || {};
if (seg.key_manager === "Sim" || seg.key_manager === "Yes") return "Key Manager";
if (String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes")) {
return isEn ? "Hardware ECDSA" : "ECDSA em hardware";
if (seg.key_manager === "Sim" || seg.key_manager === "Yes" || seg.key_manager === "Sí") return "Key Manager";
if (String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes") || String(seg.ecdsa).startsWith("Sí")) {
return isEs ? "ECDSA en hardware" : isEn ? "Hardware ECDSA" : "ECDSA em hardware";
}
if (String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes")) {
return isEn ? "Hardware ECC" : "ECC em hardware";
if (String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes") || String(seg.ecc).startsWith("Sí")) {
return isEs ? "ECC en hardware" : isEn ? "Hardware ECC" : "ECC em hardware";
}
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
return isEn ? "SHA only" : "Só SHA";
return isEs ? "Solo SHA" : isEn ? "SHA only" : "Só SHA";
}
const simPt = (v) => !String(v).includes("Não");
const simEn = (v) => !String(v).includes("No") && !String(v).includes("Não");
const simEs = (v) => !String(v).includes("No") && !String(v).includes("Não");
export default function Series() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const currentLocale = isEn ? "en" : "pt";
const seriesData = isEn ? seriesDataEn : seriesDataPt;
const filtros = isEn ? FILTROS_EN : FILTROS_PT;
const isEs = pathname?.startsWith("/es");
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const seriesData = isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
const filtros = isEs ? FILTROS_ES : isEn ? FILTROS_EN : FILTROS_PT;
const [filtro, setFiltro] = useState("todas");
const todas = Object.entries(seriesData);
@@ -72,19 +83,22 @@ export default function Series() {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Series" : "Séries" }]}
isEs={isEs}
trail={[{ label: isEs ? "Series" : isEn ? "Series" : "Séries" }]}
icon={Compass}
label={isEn ? `${todas.length} Espressif families` : `${todas.length} famílias da Espressif`}
title={isEn ? "ESP32 Series" : "Séries ESP32"}
label={isEs ? `${todas.length} familias de Espressif` : isEn ? `${todas.length} Espressif families` : `${todas.length} famílias da Espressif`}
title={isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32"}
lede={
isEn
isEs
? "Cada serie fue diseñada para un tipo de proyecto específico, desde sensores a batería hasta aceleradores de IA. Abra la ficha para ver patillaje, periféricos y seguridad."
: isEn
? "Each series was designed for a specific project type, from battery-powered sensors to AI accelerators. Open the sheet to see pinout, peripherals, and security."
: "Cada série foi desenhada para um tipo de projeto, do sensor a bateria ao acelerador de IA. Abra a ficha para ver pinagem, periféricos e segurança."
}
/>
<section className="shell pt-10 sm:pt-12" aria-label={isEn ? "Series list" : "Lista de séries"}>
<div role="group" aria-label={isEn ? "Filter series" : "Filtrar séries"} className="flex flex-wrap gap-2">
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Lista de series" : isEn ? "Series list" : "Lista de séries"}>
<div role="group" aria-label={isEs ? "Filtrar series" : isEn ? "Filter series" : "Filtrar séries"} className="flex flex-wrap gap-2">
{filtros.map((f) => (
<button key={f.id} type="button" aria-pressed={filtro === f.id} onClick={() => setFiltro(f.id)} className="chip-btn">
{f.rotulo}
@@ -92,13 +106,22 @@ export default function Series() {
))}
</div>
<p role="status" aria-live="polite" className="mt-4 text-sm text-muted">
{series.length} {series.length === 1 ? (isEn ? "series" : "série") : (isEn ? "series" : "séries")}
{series.length} {series.length === 1 ? (isEs ? "serie" : isEn ? "series" : "série") : (isEs ? "series" : isEn ? "series" : "séries")}
</p>
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{series.map(([key, serie]) => {
const riscv = serie.arquitetura.includes("RISC-V");
const fatos = isEn
const fatos = isEs
? [
["Núcleos", serie.nucleos],
["Frecuencia", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", simEs(serie.wifi) ? "Sí" : "No"],
["Bluetooth", String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "No" : "Sí"],
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean)
: isEn
? [
["Cores", serie.nucleos],
["Frequency", serie.frequencia],
@@ -136,14 +159,14 @@ export default function Series() {
{serie.aceleradores_ia && (
<li className="chip chip-accent">
<Sparkles className="h-3 w-3" aria-hidden="true" />
{isEn ? "AI Accelerators" : "Aceleradores de IA"}
{isEs ? "Aceleradores de IA" : isEn ? "AI Accelerators" : "Aceleradores de IA"}
</li>
)}
<li className="chip">
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
{nivelSeguranca(serie, isEn)}
{nivelSeguranca(serie, isEn, isEs)}
</li>
{(serie.matter === "Sim" || serie.matter === "Yes") && (
{(serie.matter === "Sim" || serie.matter === "Yes" || serie.matter === "Sí") && (
<li className="chip">
<Home className="h-3 w-3 text-accent" aria-hidden="true" />
Matter
@@ -152,13 +175,13 @@ export default function Series() {
{serie.status_documentacao && (
<li className="chip">
<AlertTriangle className="h-3 w-3 text-gold" aria-hidden="true" />
{isEn ? "Preliminary documentation" : "Documentação preliminar"}
{isEs ? "Documentación preliminar" : isEn ? "Preliminary documentation" : "Documentação preliminar"}
</li>
)}
</ul>
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
{isEn ? "Open datasheet" : "Abrir ficha"}
{isEs ? "Abrir ficha" : isEn ? "Open datasheet" : "Abrir ficha"}
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
@@ -171,17 +194,19 @@ export default function Series() {
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">
{isEn ? "Unsure between two series?" : "Ficou em dúvida entre duas séries?"}
{isEs ? "¿Tiene dudas entre dos series?" : isEn ? "Unsure between two series?" : "Ficou em dúvida entre duas séries?"}
</h2>
<p className="mt-3 text-base text-muted">
{isEn
{isEs
? "El comparador coloca hasta cuatro modelos lado a lado: núcleos, radio, memoria, periféricos y seguridad."
: isEn
? "The comparator tool puts up to four models side by side: cores, radio, memory, peripherals, and security."
: "O comparador coloca até quatro modelos lado a lado: núcleos, rádio, memória, periféricos e segurança."}
</p>
</div>
<Link href={getLocalizedHref("/comparacao", currentLocale)} className="btn btn-primary shrink-0">
<Scale className="h-4 w-4" aria-hidden="true" />
{isEn ? "Compare series" : "Comparar séries"}
{isEs ? "Comparar series" : isEn ? "Compare series" : "Comparar séries"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
+69 -27
View File
@@ -11,13 +11,18 @@ const REPOSITORIO = SITE.repositorio;
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return paginaMeta({
titulo: isEn ? "About the Project" : "Sobre o projeto",
descricao: isEn
titulo: isEs ? "Acerca del proyecto" : isEn ? "About the Project" : "Sobre o projeto",
descricao: isEs
? "ESPDocs es una plataforma independiente que organiza la documentación técnica del ecosistema ESP32. Conozca el proyecto y cómo contribuir."
: isEn
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem. Learn about the project and how to contribute."
: "O ESPDocs é uma plataforma independente que reúne a documentação do ecossistema ESP32 em português brasileiro. Saiba como o projeto nasceu e como contribuir.",
caminho: "/sobre",
keywords: isEn
keywords: isEs
? ["ESPDocs", "documentación ESP32", "proyecto open source ESP32", "código abierto"]
: isEn
? ["ESPDocs", "ESP32 documentation", "open source ESP32 project"]
: ["ESPDocs", "documentação ESP32 português", "projeto open source ESP32"],
locale,
@@ -27,6 +32,8 @@ export async function generateMetadata({ params }) {
export default async function Sobre({ params }) {
const { locale } = await params;
const isPt = locale === "pt";
const isEn = locale === "en";
const isEs = locale === "es";
const featuresPt = [
{ icon: Languages, title: "Em português", desc: "Dados técnicos organizados e apresentados em português do Brasil." },
@@ -42,7 +49,14 @@ export default async function Sobre({ params }) {
{ icon: Code, title: "Open source", desc: "Contributions are welcome on Gitea." },
];
const features = isPt ? featuresPt : featuresEn;
const featuresEs = [
{ icon: Languages, title: "En español", desc: "Datos técnicos organizados y presentados de forma clara en español neutro." },
{ icon: GitCompare, title: "Comparaciones visuales", desc: "Tablas y diagramas para elegir la serie adecuada sin abrir diez hojas de datos." },
{ icon: Globe, title: "Acceso centralizado", desc: "Enlaces directos a la documentación oficial, repositorios y ejemplos." },
{ icon: Code, title: "Código abierto", desc: "Las contribuciones son bienvenidas en Gitea." },
];
const features = isEs ? featuresEs : isEn ? featuresEn : featuresPt;
return (
<div className="site-canvas min-h-screen">
@@ -56,8 +70,8 @@ export default async function Sobre({ params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isPt ? "Início" : "Home", caminho: isPt ? "/" : "/en" },
{ nome: isPt ? "Sobre o Projeto" : "About the Project", caminho: isPt ? "/sobre" : "/en/about" },
{ nome: isEs ? "Inicio" : isPt ? "Início" : "Home", caminho: isEn ? "/en" : isEs ? "/es" : "/" },
{ nome: isEs ? "Acerca del proyecto" : isPt ? "Sobre o Projeto" : "About the Project", caminho: isEn ? "/en/about" : isEs ? "/es/acerca" : "/sobre" },
])
),
}}
@@ -67,20 +81,23 @@ export default async function Sobre({ params }) {
<main id="conteudo">
<PageHero
isEn={!isPt}
trail={[{ label: isPt ? "Sobre" : "About" }]}
isEn={isEn}
isEs={isEs}
trail={[{ label: isEs ? "Acerca" : isPt ? "Sobre" : "About" }]}
icon={Info}
label={isPt ? "Projeto independente" : "Independent project"}
title={isPt ? "Sobre o ESPDocs" : "About ESPDocs"}
label={isEs ? "Proyecto independiente" : isPt ? "Projeto independente" : "Independent project"}
title={isEs ? "Acerca de ESPDocs" : isPt ? "Sobre o ESPDocs" : "About ESPDocs"}
lede={
isPt
isEs
? "Una plataforma no oficial, en español, que organiza la información técnica del ESP32 y le conecta con la documentación oficial de Espressif. Creada para la comunidad de hardware abierto."
: isPt
? "Uma plataforma não oficial, em português, que organiza as informações técnicas do ESP32 e leva você à documentação oficial da Espressif. Feita para a comunidade de hardware livre."
: "An unofficial platform organizing technical information about the ESP32 and linking directly to official Espressif documentation. Built for the open hardware community."
}
>
<a href={REPOSITORIO} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<GiteaMark className="h-4 w-4" />
{isPt ? "Ver o repositório" : "View repository"}
{isEs ? "Ver el repositorio" : isPt ? "Ver o repositório" : "View repository"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</PageHero>
@@ -88,18 +105,22 @@ export default async function Sobre({ params }) {
<div className="shell section grid grid-cols-1 gap-10 pt-12 sm:pt-16 lg:grid-cols-[1fr_22rem] lg:gap-14">
<div className="min-w-0 max-w-3xl space-y-12">
<section aria-labelledby="missao">
<h2 id="missao" className="section-title">{isPt ? "Missão" : "Mission"}</h2>
<h2 id="missao" className="section-title">{isEs ? "Misión" : isPt ? "Missão" : "Mission"}</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
{isPt
{isEs
? "Facilitar el acceso y la comprensión del ecosistema ESP32 con una interfaz en español que organiza especificaciones, compara series, presenta frameworks y centraliza los enlaces a la documentación oficial. El objetivo es ahorrar tiempo en el desarrollo de firmware e IoT."
: isPt
? "Facilitar o acesso e a compreensão do ecossistema ESP32 com uma interface em português que organiza especificações, compara séries, apresenta frameworks e centraliza os links da documentação oficial. O objetivo é economizar o seu tempo no desenvolvimento de firmware e IoT."
: "Facilitate access and understanding of the ESP32 ecosystem with an interface organizing specifications, comparing series, presenting frameworks and centralizing links to official documentation. The goal is to save development time in firmware and IoT projects."}
</p>
</section>
<section aria-labelledby="porque">
<h2 id="porque" className="section-title">{isPt ? "Por que o ESPDocs existe" : "Why ESPDocs exists"}</h2>
<h2 id="porque" className="section-title">{isEs ? "¿Por qué existe ESPDocs?" : isPt ? "Por que o ESPDocs existe" : "Why ESPDocs exists"}</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
{isPt
{isEs
? "La información sobre el ESP32 suele estar dispersa entre hojas de datos densas, manuales de referencia técnica (TRM) y repositorios secundarios. ESPDocs condensa y estructura estos datos para una evaluación rápida antes de profundizar en la ingeniería oficial."
: isPt
? "As informações sobre os ESP32 costumam estar espalhadas entre datasheets densos, manuais de referência (TRM) e repositórios secundários, tudo em inglês. O ESPDocs condensa e estrutura esses dados para a triagem rápida, antes de você aprofundar na engenharia oficial."
: "Information regarding the ESP32 is often scattered across dense datasheets, Technical Reference Manuals (TRM), and secondary repositories. ESPDocs condenses and structures this data for rapid evaluation before diving into in-depth engineering documentation."}
</p>
@@ -119,14 +140,22 @@ export default async function Sobre({ params }) {
</section>
<section aria-labelledby="contribuir">
<h2 id="contribuir" className="section-title">{isPt ? "Como contribuir" : "How to contribute"}</h2>
<h2 id="contribuir" className="section-title">{isEs ? "Cómo contribuir" : isPt ? "Como contribuir" : "How to contribute"}</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
{isPt
{isEs
? "Las correcciones de traducción, ajustes en tablas y nuevos ejemplos de código son muy bienvenidos. El proyecto crece junto con la comunidad."
: isPt
? "Correções de tradução, ajustes em tabelas e novos exemplos de código são muito bem-vindos. O projeto cresce com a comunidade."
: "Translation fixes, table refinements and new code examples are welcome. The project grows with the community."}
</p>
<ol className="mt-6 space-y-3">
{(isPt
{(isEs
? [
"Haga un fork del repositorio en Gitea.",
"Implemente las mejoras y verifique las referencias técnicas oficiales.",
"Abra una solicitud de extracción (Pull Request) describiendo los cambios realizados.",
]
: isPt
? [
"Faça o fork do repositório no Gitea.",
"Implemente as melhorias e confira as referências técnicas oficiais.",
@@ -149,16 +178,18 @@ export default async function Sobre({ params }) {
</section>
<section aria-labelledby="acessibilidade">
<h2 id="acessibilidade" className="section-title">{isPt ? "Acessibilidade" : "Accessibility"}</h2>
<h2 id="acessibilidade" className="section-title">{isEs ? "Accesibilidad" : isPt ? "Acessibilidade" : "Accessibility"}</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
{isPt
{isEs
? "ESPDocs prioriza la legibilidad, el alto contraste (nivel AAA de WCAG) y la navegación completa por teclado. Si encuentra alguna barrera de lectura o navegación, abra un issue en el repositorio."
: isPt
? "O ESPDocs prioriza a legibilidade, o contraste alto (nível AAA da WCAG) e a navegação completa por teclado. Se encontrar alguma barreira de leitura ou navegação, abra uma issue no repositório."
: "ESPDocs prioritizes readability, high contrast (WCAG AAA level), and full keyboard navigation. If you encounter any accessibility or navigation hurdles, please open an issue in the repository."}
</p>
</section>
</div>
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label={isPt ? "Autor e aviso legal" : "Author and legal notice"}>
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label={isEs ? "Autor y aviso legal" : isPt ? "Autor e aviso legal" : "Author and legal notice"}>
<div className="panel p-6 sm:p-7">
<div className="flex items-center gap-4">
<span className="grid h-16 w-16 shrink-0 place-items-center rounded-2xl border border-line bg-white">
@@ -166,11 +197,20 @@ export default async function Sobre({ params }) {
</span>
<div>
<h2 className="font-display text-lg font-semibold text-ink">Lucas Rayan Guerra</h2>
<p className="text-sm text-muted">{isPt ? "Criador do Ciência Embarcada" : "Creator of Ciência Embarcada"}</p>
<p className="text-sm text-muted">{isEs ? "Creador de Ciência Embarcada" : isPt ? "Criador do Ciência Embarcada" : "Creator of Ciência Embarcada"}</p>
</div>
</div>
<div className="mt-5 space-y-3 text-sm leading-relaxed text-muted">
{isPt ? (
{isEs ? (
<>
<p>
Técnico en Desarrollo de Sistemas, estudiante de Ciencias de la Computación en la UFRPE e investigador en Blockchain y Criptografía.
</p>
<p>
Director de Software en Semine AgriTech y ponente en eventos como REC&apos;n&apos;Play, compartiendo contenido sobre hardware, IoT y ciberseguridad.
</p>
</>
) : isPt ? (
<>
<p>
Técnico em Desenvolvimento de Sistemas, estudante de Ciência da Computação na UFRPE e pesquisador de
@@ -193,15 +233,17 @@ export default async function Sobre({ params }) {
)}
</div>
<a href="https://cienciaembarcada.com.br" target="_blank" rel="noopener noreferrer" className="btn btn-ghost mt-6 w-full">
{isPt ? "Visitar o Ciência Embarcada" : "Visit Ciência Embarcada"}
{isEs ? "Visitar Ciência Embarcada" : isPt ? "Visitar o Ciência Embarcada" : "Visit Ciência Embarcada"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
<div className="callout callout-warn">
<p className="font-semibold text-ink">{isPt ? "Aviso legal" : "Legal notice"}</p>
<p className="font-semibold text-ink">{isEs ? "Aviso legal" : isPt ? "Aviso legal" : "Legal notice"}</p>
<p className="mt-1">
{isPt
{isEs
? "ESPDocs es un agregador y organizador. Los manuales y repositorios referenciados pertenecen a Espressif Systems. En proyectos críticos o comerciales, consulte siempre la documentación oficial."
: isPt
? "O ESPDocs é um agregador e organizador. Os manuais e repositórios referenciados pertencem à Espressif Systems. Em projetos críticos ou comerciais, consulte sempre a documentação oficial."
: "ESPDocs is an aggregator and curation platform. Referenced manuals and repositories are property of Espressif Systems. In mission-critical or commercial designs, always consult official documentation."}
</p>