Files
ESPDocs/components/BoardCard.jsx
T
Lucas RayanandAntigravity 660b28c09d
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
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>
2026-11-01 19:50:20 -03:00

112 lines
3.8 KiB
React

"use client";
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
import { usePathname } from "next/navigation";
const HEADER_MAP_EN = {
"Nome": "Name",
"Descrição": "Description",
"Série": "Series",
"Loja Oficial": "Official Store",
"Fabricante": "Manufacturer",
"Processador": "Processor",
"Memória Flash": "Flash Memory",
"PSRAM": "PSRAM",
"Conector USB": "USB Connector",
"Interface USB": "USB Interface",
"Antena": "Antenna",
"Bateria": "Battery",
"Display": "Display",
"Sensores": "Sensors",
"Botões": "Buttons",
"LEDs": "LEDs",
};
const 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",
};
export default function BoardCard({ board }) {
const pathname = usePathname() || "";
const isEn = pathname === "/en" || pathname.startsWith("/en/");
const isEs = pathname === "/es" || pathname.startsWith("/es/");
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
const technicalFields = Object.keys(board).filter((key) => !mainFields.includes(key) && board[key]);
const valor = (campo) => board[campo]?.toString().toLowerCase().trim();
const recursos = technicalFields.filter((campo) => ["sim", "opcional"].includes(valor(campo)));
const lojaOficial = valor("Loja Oficial") === "sim";
return (
<article className="card card-interactive flex h-full flex-col overflow-hidden">
{board.Imagem && (
<div
className="flex shrink-0 justify-center border-b border-line bg-surface-2 p-4 [&_img]:h-44 [&_img]:w-auto [&_img]:max-w-full [&_img]:object-contain"
dangerouslySetInnerHTML={{ __html: board.Imagem }}
/>
)}
<div className="flex grow flex-col p-5">
{lojaOficial && (
<span className="chip chip-accent mb-3 w-fit">
<Award className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "Official store" : isEs ? "Tienda oficial" : "Loja oficial"}
</span>
)}
<h3 className="line-clamp-2 font-display text-lg font-semibold leading-snug tracking-[-0.02em] text-ink">
{board.Nome || (isEn ? "Generic board" : isEs ? "Placa genérica" : "Placa genérica")}
</h3>
{recursos.length > 0 && (
<div className="mt-4">
<p className="text-xs font-medium text-muted">{isEn ? "On-board features" : isEs ? "Recursos integrados" : "Recursos embarcados"}</p>
<ul className="mt-2 flex flex-wrap gap-1.5">
{recursos.map((campo) => {
const opcional = valor(campo) === "opcional";
const label = isEn ? (HEADER_MAP_EN[campo] || campo) : isEs ? (HEADER_MAP_ES[campo] || campo) : campo;
return (
<li key={campo} className={`chip ${opcional ? "text-gold" : "text-ink-soft"}`}>
{opcional ? (
<CircleHelp className="h-3 w-3 text-gold" aria-hidden="true" />
) : (
<Check className="h-3 w-3 text-ok" aria-hidden="true" />
)}
{label}
{opcional && <span className="sr-only">{isEn ? " (optional)" : " (opcional)"}</span>}
</li>
);
})}
</ul>
</div>
)}
{board.Link && (
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : "Ver no AliExpress"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only">{isEn ? " (opens in new tab)" : isEs ? " (abre en una pestaña nueva)" : " (abre em nova aba)"}</span>
</a>
)}
</div>
</article>
);
}