feat(i18n): English localization (phase 3)
CI / Lint e auditorias (push) Successful in 1m51s
CI / Trivy (dependências e segredos) (push) Successful in 15s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m41s

- Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en
- Implement localized routing, slugs, and language negotiation with fallback to en
- Build dedicated search index for English with strict book exclusion
- Update components and pages to dynamically render localized strings and datasets
- Verify full structural parity and exact 1:1 match on Portuguese pages via comparator

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 14:17:13 -03:00
1 parent 22b1f26aed
commit c1bd2c7e11
61 files changed
+18182 -1225

No files matched your search

+102 -38
View File
@@ -33,6 +33,29 @@ const FILTER_CATEGORIES = [
{ id: "Antena", name: "Antena", color: "#06b6d4" }, // cyan
];
const FILTER_CATEGORIES_EN = [
{ id: "All", name: "All", color: "#8b5cf6" },
{ id: "GND", name: "Ground (GND)", color: "#64748b" },
{ id: "Power", name: "Power", color: "#ef4444" },
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
{ id: "Low Power", name: "LP GPIO", color: "#f59e0b" },
{ id: "ADC", name: "ADC", color: "#ec4899" },
{ id: "DAC", name: "DAC", color: "#a855f7" },
{ id: "Touch", name: "Touch", color: "#06b6d4" },
{ id: "UART", name: "UART", color: "#3b82f6" },
{ id: "SPI", name: "SPI", color: "#f43f5e" },
{ id: "USB", name: "USB", color: "#a855f7" },
{ id: "Clock", name: "Clock", color: "#eab308" },
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
{ id: "Strapping", name: "Boot mode", color: "#dc2626" },
{ id: "DSI", name: "Display (DSI)", color: "#f97316" },
{ id: "CSI", name: "Camera (CSI)", color: "#14b8a6" },
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
{ id: "SD", name: "SD Card", color: "#10b981" },
{ id: "I2C", name: "I2C", color: "#f59e0b" },
{ id: "Antena", name: "Antenna", color: "#06b6d4" },
];
const type_colors = {
power: "#ef4444", // high-voltage red
ground: "#64748b", // slate gray
@@ -58,6 +81,7 @@ export default function ConnectionsDiagram(props) {
function ConnectionsDiagramInner({ connections, serie }) {
const params = useSearchParams();
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const router = useRouter();
const pinosFiltro = useMemo(() => new Set((params.get("pinos") ?? "").split(",").filter(Boolean)), [params]);
const motivoFiltro = params.get("motivo") ?? "";
@@ -69,6 +93,8 @@ function ConnectionsDiagramInner({ connections, serie }) {
const dialogRef = useRef(null);
const gatilhoRef = useRef(null);
const filterCategories = isEn ? FILTER_CATEGORIES_EN : FILTER_CATEGORIES;
useEffect(() => {
setMounted(true);
}, []);
@@ -84,11 +110,18 @@ function ConnectionsDiagramInner({ connections, serie }) {
};
const getCategoryColor = (category) => {
const filter = FILTER_CATEGORIES.find((f) => f.id === category);
const filter = filterCategories.find((f) => f.id === category);
return filter?.color || "#64748b";
};
const nomeCategoria = (cat) => (cat === "Strapping" ? "Inicialização" : cat);
const nomeCategoria = (cat) => {
if (cat === "Strapping") return isEn ? "Boot mode" : "Inicialização";
if (isEn) {
const found = FILTER_CATEGORIES_EN.find((f) => f.id === cat);
if (found) return found.name;
}
return cat;
};
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery, pinosFiltro]);
@@ -142,8 +175,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
return (
<div className="px-4 py-14 text-center">
<Sliders className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<p className="mt-4 font-display text-xl font-semibold text-ink">Diagrama de conexões indisponível</p>
<p className="mt-1 text-sm text-muted">Este modelo ainda não tem um mapa detalhado de conexões e barramentos.</p>
<p className="mt-4 font-display text-xl font-semibold text-ink">
{isEn ? "Pinout diagram unavailable" : "Diagrama de conexões indisponível"}
</p>
<p className="mt-1 text-sm text-muted">
{isEn ? "This model does not yet have a detailed pinout and bus map." : "Este modelo ainda não tem um mapa detalhado de conexões e barramentos."}
</p>
</div>
);
}
@@ -153,59 +190,71 @@ function ConnectionsDiagramInner({ connections, serie }) {
return (
<div className="space-y-6">
<div>
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Diagrama de conexões interativo</h2>
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
{isEn ? "Interactive pinout diagram" : "Diagrama de conexões interativo"}
</h2>
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted">
Todas as conexões e a multiplexação do {serie.nome_completo}. Busque por nome ou número, filtre por função e abra um pino para ver os detalhes.
{isEn ? (
`All connections and multiplexing for the ${serie.nome_completo}. Search by name or number, filter by function, and open a pin to see details.`
) : (
`Todas as conexões e a multiplexação do ${serie.nome_completo}. Busque por nome ou número, filtre por função e abra um pino para ver os detalhes.`
)}
</p>
</div>
{pinosFiltro.size > 0 && (
<div role="status" className="callout flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-ink">
<strong className="font-semibold">Filtrado pela busca:</strong> {motivoFiltro || "pinos selecionados"} ({pinosFiltro.size}{" "}
{pinosFiltro.size === 1 ? "pino" : "pinos"})
<strong className="font-semibold">{isEn ? "Filtered by search:" : "Filtrado pela busca:"}</strong> {motivoFiltro || (isEn ? "selected pins" : "pinos selecionados")} ({pinosFiltro.size}{" "}
{pinosFiltro.size === 1 ? (isEn ? "pin" : "pino") : (isEn ? "pins" : "pinos")})
</p>
<button type="button" onClick={mostrarTodos} className="btn btn-ghost btn-sm">
Mostrar todos os pinos
{isEn ? "Show all pins" : "Mostrar todos os pinos"}
</button>
</div>
)}
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<label htmlFor="busca-pino" className="sr-only">Buscar conexão por nome ou número</label>
<label htmlFor="busca-pino" className="sr-only">
{isEn ? "Search connection by name or number" : "Buscar conexão por nome ou número"}
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-pino"
type="search"
placeholder="Buscar pino (ex.: GPIO1, TXD, XTAL)"
placeholder={isEn ? "Search pin (e.g.: GPIO1, TXD, XTAL)" : "Buscar pino (ex.: GPIO1, TXD, XTAL)"}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
className="field pl-11 pr-4 text-sm"
/>
</div>
<div role="group" aria-label="Modo de exibição" className="flex gap-2">
<div role="group" aria-label={isEn ? "Display mode" : "Modo de exibição"} className="flex gap-2">
<button type="button" onClick={() => setViewMode("grid")} aria-pressed={viewMode === "grid"} className="chip-btn">
<Grid className="h-4 w-4" aria-hidden="true" />
Grade
{isEn ? "Grid" : "Grade"}
</button>
<button type="button" onClick={() => setViewMode("list")} aria-pressed={viewMode === "list"} className="chip-btn">
<List className="h-4 w-4" aria-hidden="true" />
Lista
{isEn ? "List" : "Lista"}
</button>
</div>
</div>
<div>
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-ink">Função do pino</h3>
<h3 className="text-sm font-semibold text-ink">{isEn ? "Pin function" : "Função do pino"}</h3>
<p role="status" aria-live="polite" className="text-sm text-muted">
{visibleConnections.length} de {connections.length} pinos
{isEn ? (
`${visibleConnections.length} of ${connections.length} pins`
) : (
`${visibleConnections.length} de ${connections.length} pinos`
)}
</p>
</div>
<div role="group" aria-label="Filtrar por função" className="mt-3 flex flex-wrap gap-2">
{FILTER_CATEGORIES.map((filter) => {
<div role="group" aria-label={isEn ? "Filter by function" : "Filtrar por função"} className="mt-3 flex flex-wrap gap-2">
{filterCategories.map((filter) => {
const isActive = activeFilter === filter.id;
const count = connections.filter((c) => (filter.id === "All" ? true : c.categorias.includes(filter.id))).length;
if (count === 0 && !isActive) return null; // filtros que não se aplicam a este chip
@@ -223,8 +272,8 @@ function ConnectionsDiagramInner({ connections, serie }) {
{visibleConnections.length === 0 ? (
<div className="rounded-2xl border border-dashed border-line-strong px-6 py-12 text-center">
<Search className="mx-auto h-8 w-8 text-muted" aria-hidden="true" />
<p className="mt-3 font-semibold text-ink">Nenhuma conexão encontrada</p>
<p className="mt-1 text-sm text-muted">Ajuste a busca ou volte para todas as funções.</p>
<p className="mt-3 font-semibold text-ink">{isEn ? "No connections found" : "Nenhuma conexão encontrada"}</p>
<p className="mt-1 text-sm text-muted">{isEn ? "Adjust your search or return to all functions." : "Ajuste a busca ou volte para todas as funções."}</p>
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); if (pinosFiltro.size) mostrarTodos(); }} className="btn btn-primary btn-sm mt-5">
Limpar busca e filtros
</button>
@@ -267,13 +316,15 @@ function ConnectionsDiagramInner({ connections, serie }) {
) : (
<div className="overflow-x-auto rounded-xl border border-line">
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
<caption className="sr-only">Conexões do {serie.nome_completo}</caption>
<caption className="sr-only">
{isEn ? `Connections of ${serie.nome_completo}` : `Conexões do ${serie.nome_completo}`}
</caption>
<thead>
<tr className="border-b border-line bg-surface-2 text-muted">
<th scope="col" className="px-4 py-3 font-medium">Pino</th>
<th scope="col" className="px-4 py-3 font-medium">Nome</th>
<th scope="col" className="px-4 py-3 font-medium">Funções</th>
<th scope="col" className="px-4 py-3 font-medium">Uso</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Pin" : "Pino"}</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Name" : "Nome"}</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Functions" : "Funções"}</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Usage" : "Uso"}</th>
</tr>
</thead>
<tbody className="divide-y divide-line">
@@ -286,7 +337,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
type="button"
onClick={(e) => abrir(connection, e)}
aria-haspopup="dialog"
aria-label={`Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`}
aria-label={isEn ? `Open pin details for ${connection.nome}, number ${connection.numero}` : `Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`}
className="grid h-11 w-11 place-items-center rounded-full border-2 bg-surface text-sm font-semibold tabular-nums text-ink hover:bg-surface-2"
style={{ borderColor: baseColor }}
>
@@ -301,12 +352,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
{connection.avisos ? (
<span className="inline-flex items-center gap-1.5 font-medium text-danger">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
Restrito
{isEn ? "Restricted" : "Restrito"}
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-ok">
<CheckCircle className="h-4 w-4" aria-hidden="true" />
Livre
{isEn ? "Free" : "Livre"}
</span>
)}
</td>
@@ -322,10 +373,19 @@ function ConnectionsDiagramInner({ connections, serie }) {
<div className="callout callout-warn flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">Notas sobre a pinagem</p>
<p className="font-semibold text-ink">{isEn ? "Pinout notes" : "Notas sobre a pinagem"}</p>
<ul className="mt-1 list-disc space-y-1 pl-5">
<li>Pinos com o ícone de alerta têm papel especial no boot ou limitações analógicas. Abra o pino para ver os detalhes.</li>
<li>Isole os circuitos de toque capacitivo de pinos com ruído de RF acoplado.</li>
{isEn ? (
<>
<li>Pins with a warning icon have special boot roles or analog limitations. Open the pin to see details.</li>
<li>Isolate capacitive touch traces from coupled RF noise.</li>
</>
) : (
<>
<li>Pinos com o ícone de alerta têm papel especial no boot ou limitações analógicas. Abra o pino para ver os detalhes.</li>
<li>Isole os circuitos de toque capacitivo de pinos com ruído de RF acoplado.</li>
</>
)}
</ul>
</div>
</div>
@@ -353,10 +413,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
<h3 id="modal-title" className="break-words font-display text-2xl font-semibold tracking-[-0.03em] text-ink">
{selectedConnection.nome}
</h3>
<p className="text-sm text-muted">Pino físico {selectedConnection.numero}</p>
<p className="text-sm text-muted">
{isEn ? `Physical pin ${selectedConnection.numero}` : `Pino físico ${selectedConnection.numero}`}
</p>
</div>
</div>
<button type="button" onClick={fechar} aria-label="Fechar detalhes do pino" className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink">
<button type="button" onClick={fechar} aria-label={isEn ? "Close pin details" : "Fechar detalhes do pino"} className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink">
<X className="h-5 w-5" aria-hidden="true" />
</button>
</div>
@@ -364,7 +426,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
<div className="flex-1 space-y-6 overflow-y-auto p-5 sm:p-7">
{selectedConnection.categorias.length > 0 && (
<section>
<h4 className="text-sm font-semibold text-ink">Categorias</h4>
<h4 className="text-sm font-semibold text-ink">{isEn ? "Categories" : "Categorias"}</h4>
<ul className="mt-3 flex flex-wrap gap-2">
{selectedConnection.categorias.map((cat) => (
<li key={cat} className="chip">
@@ -380,7 +442,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
<section>
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Zap className="h-4 w-4 text-danger" aria-hidden="true" />
Domínio de alimentação
{isEn ? "Power domain" : "Domínio de alimentação"}
</h4>
<ul className="tile mt-3 space-y-2">
{selectedConnection.alimentacao.map((source) => (
@@ -391,7 +453,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
)}
<section>
<h4 className="text-sm font-semibold text-ink">Funções disponíveis</h4>
<h4 className="text-sm font-semibold text-ink">{isEn ? "Available functions" : "Funções disponíveis"}</h4>
<ul className="tile mt-3 grid gap-2.5 sm:grid-cols-2">
{selectedConnection.funcoes.map((func) => (
<li key={func} className="flex items-center gap-2.5 text-sm text-ink-soft">
@@ -406,7 +468,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
<section className="callout callout-danger">
<h4 className="flex items-center gap-2 font-semibold text-ink">
<AlertTriangle className="h-4 w-4 text-danger" aria-hidden="true" />
Avisos de engenharia
{isEn ? "Engineering notes" : "Avisos de engenharia"}
</h4>
<ul className="mt-2 list-disc space-y-1.5 pl-5">
{selectedConnection.avisos.map((aviso) => (
@@ -418,7 +480,9 @@ function ConnectionsDiagramInner({ connections, serie }) {
</div>
<div className="border-t border-line p-4 sm:px-7">
<button type="button" onClick={fechar} className="btn btn-primary w-full">Fechar</button>
<button type="button" onClick={fechar} className="btn btn-primary w-full">
{isEn ? "Close" : "Fechar"}
</button>
</div>
</div>
</div>,