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

+84 -50
View File
@@ -1,15 +1,13 @@
"use client";
import { useState, useMemo } from "react";
import { usePathname } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import {
CATEGORIAS_COMPONENTES,
COMPONENTES_REGISTRY,
GUIA_COMPONENT_MANAGER
} from "@/lib/componentRegistryData";
import * as componentesPt from "@/lib/componentRegistryData";
import * as componentesEn from "@/lib/data/en/componentes";
import {
ArrowUpRight,
Box,
@@ -44,6 +42,10 @@ 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 [busca, setBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
const [chipFiltro, setChipFiltro] = useState("todos");
@@ -74,7 +76,7 @@ export default function ComponentRegistryPage() {
return matchCategoria && matchChip && matchBusca;
});
}, [busca, categoriaAtiva, chipFiltro]);
}, [busca, categoriaAtiva, chipFiltro, COMPONENTES_REGISTRY]);
// Manipuladores de cópia
const copiarComando = (id, comando) => {
@@ -96,15 +98,19 @@ export default function ComponentRegistryPage() {
);
if (selecionados.length === 0) {
return `# Nenhum componente selecionado.\n# Clique em "+ Adicionar ao Manifesto" nos cards abaixo.`;
return 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 = `# main/idf_component.yml\n# Gerado via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/componentes)\n\ndependencies:\n`;
let yaml = 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]);
}, [componentesSelecionados, COMPONENTES_REGISTRY, isEn]);
const copiarManifesto = () => {
navigator.clipboard.writeText(manifestoYaml);
@@ -138,40 +144,46 @@ export default function ComponentRegistryPage() {
<main id="conteudo">
<PageHero
trail={[{ label: "Component Registry" }]}
trail={[{ label: isEn ? "Component Registry" : "Component Registry" }]}
icon={Box}
label="ESP-IDF Component Manager"
title="ESP Component Registry"
lede="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."
lede={
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">
Abrir o registro oficial
{isEn ? "Open official registry" : "Abrir o registro oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
<span className="sr-only"> {isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
</a>
<a href="#manifesto-builder" className="btn btn-ghost">
Gerar idf_component.yml
{isEn ? "Generate idf_component.yml" : "Gerar idf_component.yml"}
</a>
</PageHero>
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
<section className="shell pt-10 sm:pt-12" aria-label={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">Buscar por nome, namespace ou tag</label>
<label htmlFor="busca-componente" className="sr-only">
{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
id="busca-componente"
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar por nome, namespace ou tag"
placeholder={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">Categoria</h2>
<h2 className="text-sm font-semibold text-ink">{isEn ? "Category" : "Categoria"}</h2>
<div className="mt-3 flex flex-wrap gap-2">
{CATEGORIAS_COMPONENTES.map((cat) => {
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
@@ -179,7 +191,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" ? "Todos" : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
{cat.id === "todos" ? (isEn ? "All" : "Todos") : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
<span className="tabular-nums opacity-80">{contagem}</span>
</button>
);
@@ -188,11 +200,11 @@ export default function ComponentRegistryPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">Chip compatível</h2>
<h2 className="text-sm font-semibold text-ink">{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" ? "Todos" : chip}
{chip === "todos" ? (isEn ? "All" : "Todos") : chip}
</button>
))}
</div>
@@ -203,17 +215,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 ? "componente" : "componentes"}
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? (isEn ? "component" : "componente") : (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" />
Limpar 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">
Manifesto: {componentesSelecionados.length} {componentesSelecionados.length === 1 ? "componente" : "componentes"}
{isEn ? "Manifest:" : "Manifesto:"} {componentesSelecionados.length} {componentesSelecionados.length === 1 ? (isEn ? "component" : "componente") : (isEn ? "components" : "componentes")}
</a>
</div>
</div>
@@ -221,9 +233,15 @@ export default function ComponentRegistryPage() {
{componentesFiltrados.length === 0 ? (
<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">Nenhum componente encontrado</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou volte para todas as categorias.</p>
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
{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."}
</p>
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">
{isEn ? "Clear filters" : "Limpar filtros"}
</button>
</div>
) : (
<div className="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -242,7 +260,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" />
Oficial
{isEn ? "Official" : "Oficial"}
</span>
)}
</p>
@@ -266,12 +284,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={`Copiar comando para adicionar ${comp.name}`}
aria-label={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 ? "Comando copiado" : ""}</p>
<p role="status" className="sr-only">{foiCopiado ? (isEn ? "Command copied" : "Comando copiado") : ""}</p>
<p className="mt-3 text-xs text-muted">
<span className="font-medium text-ink-soft">Chips: </span>
@@ -287,14 +305,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 ? "No manifesto" : "Adicionar ao manifesto"}
{selecionado ? (isEn ? "In manifest" : "No manifesto") : (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={`Abrir ${comp.name} no registro oficial (nova aba)`}
aria-label={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>
@@ -310,37 +328,42 @@ 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">Gerador de manifesto</p>
<h2 id="manifesto-titulo" className="section-title mt-3">Seu idf_component.yml</h2>
<p className="eyebrow">{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"}
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
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.
{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.</>
)}
</p>
</div>
<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 ? "Copiado" : "Copiar YAML"}</span>
<span role="status">{manifestoCopiado ? (isEn ? "Copied" : "Copiado") : (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" />
Baixar .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" />
Limpar
{isEn ? "Clear" : "Limpar"}
</button>
)}
</div>
</div>
<div className="code-panel mt-6">
<pre tabIndex={0} aria-label="Conteúdo do idf_component.yml" className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
<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>
</div>
{componentesSelecionados.length > 0 && (
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label="Componentes incluídos">
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label={isEn ? "Included components" : "Componentes incluídos"}>
{componentesSelecionados.map((id) => {
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
if (!c) return null;
@@ -351,7 +374,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={`Remover ${c.name} do manifesto`}
aria-label={isEn ? `Remove ${c.name} from manifest` : `Remover ${c.name} do manifesto`}
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>
@@ -364,7 +387,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="Guia rápido" title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
<SectionHead id="guia-titulo" eyebrow={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) => (
@@ -377,7 +400,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" />
Comandos essenciais
{isEn ? "Essential commands" : "Comandos essenciais"}
</h3>
<ul className="mt-4 space-y-3">
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
@@ -392,7 +415,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}` ? "Copiado" : "Copiar"}
{copiadoId === `cmd-${i}` ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy" : "Copiar")}
</button>
</li>
))}
@@ -401,13 +424,24 @@ 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" />
Versionamento semântico no 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">
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: trava na versão exata.</li>
<li><code>*</code>: aceita qualquer versão. Evite em produção.</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>
<li><code>1.2.0</code>: locks to this exact version.</li>
<li><code>*</code>: accepts any version. Avoid in production.</li>
</>
) : (
<>
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: trava na versão exata.</li>
<li><code>*</code>: aceita qualquer versão. Evite em produção.</li>
</>
)}
</ul>
</div>
</section>