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

+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>