feat(i18n): Spanish localization (phase 4)
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:
1 parent
0940814522
commit
660b28c09d
72 files changed
+17848
-947
No files matched your search
@@ -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 },
|
||||
])
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user