feat(i18n): French and German localization (phase 5)
CI / Lint e auditorias (push) Successful in 49s
CI / Trivy (dependências e segredos) (push) Successful in 11s
CI / Imagem Docker (linux/arm64) (push) Successful in 3m14s

- Add full message catalogs and content for French (fr) and German (de)
- Implement ASCII canonical slugs for French and German routes
- Add language-specific search endpoints (/api/busca/fr, /api/busca/de)
- Configure strict 404 responses for /fr/livro and /de/livro
- Localize SEO, OpenGraph (fr_FR, de_DE), Twitter and JSON-LD schemas
- Add localized code example comments across 14 examples for fr and de
- Enable 'fr' and 'de' in ENABLED_LOCALES with root negotiation and redirect
- Extend test suite in 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 22:06:36 -03:00
1 parent 7b673aeb4b
commit 7fce46b9e1
104 files changed
+33369 -686

No files matched your search

+84 -9
View File
@@ -4,16 +4,18 @@ export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
return paginaMeta({
titulo: isEs
? "ESP Component Registry & IDF Component Manager · ESPDocs"
: isEn
? "ESP Component Registry & IDF Component Manager · ESPDocs"
: "ESP Component Registry & IDF Component Manager · ESPDocs",
titulo: "ESP Component Registry & IDF Component Manager · ESPDocs",
descricao: isEs
? "Catálogo interactivo del ESP Component Registry: controladores de pantalla, LVGL, reconocimiento de voz ESP-SR, RainMaker, BSPs y generador de idf_component.yml."
: isEn
? "Official interactive catalog of the ESP Component Registry: display drivers, LVGL, ESP-SR voice recognition, RainMaker, BSPs, and idf_component.yml generator."
: isFr
? "Catalogue interactif du ESP Component Registry : pilotes d'écran, LVGL, reconnaissance vocale ESP-SR, RainMaker, BSPs et générateur d'idf_component.yml."
: isDe
? "Interaktiver Katalog der ESP Component Registry: Display-Treiber, LVGL, ESP-SR-Sprachkennung, RainMaker, BSPs und idf_component.yml-Generator."
: "Catálogo oficial e interativo do ESP Component Registry: drivers de displays, LVGL, reconhecimento de voz ESP-SR, RainMaker, BSPs e gerador de idf_component.yml.",
caminho: "/componentes",
locale,
@@ -41,6 +43,30 @@ export async function generateMetadata({ params }) {
"espressif esp-sr",
"ESP-IDF libraries",
]
: isFr
? [
"ESP Component Registry",
"IDF Component Manager",
"idf_component.yml",
"idf.py add-dependency",
"composants ESP32",
"espressif esp_lvgl_port",
"espressif esp_rainmaker",
"espressif esp-sr",
"bibliothèques ESP-IDF",
]
: isDe
? [
"ESP Component Registry",
"IDF Component Manager",
"idf_component.yml",
"idf.py add-dependency",
"ESP32 Komponenten",
"espressif esp_lvgl_port",
"espressif esp_rainmaker",
"espressif esp-sr",
"ESP-IDF Bibliotheken",
]
: [
"ESP Component Registry",
"IDF Component Manager",
@@ -112,11 +138,52 @@ const faqsEs = [
},
];
const faqsFr = [
{
pergunta: "Qu'est-ce que l'ESP Component Registry et l'IDF Component Manager ?",
resposta: "L'ESP Component Registry (components.espressif.com) est le dépôt officiel de paquets modulaires d'Espressif. L'IDF Component Manager est l'outil intégré à ESP-IDF (dès la v4.4 et standard en v5.x+) qui télécharge, compile et résout automatiquement les versions des dépendances à partir du manifeste idf_component.yml.",
},
{
pergunta: "Comment ajouter un composant à mon projet ESP-IDF ?",
resposta: "Vous pouvez exécuter la commande 'idf.py add-dependency \"namespace/composant^version\"' dans le terminal de votre projet (par exemple : idf.py add-dependency \"espressif/esp_lvgl_port^2.4.0\") ou modifier directement le fichier main/idf_component.yml.",
},
{
pergunta: "Quelle est la différence entre le Component Registry et les sous-modules git ?",
resposta: "Le Component Registry élimine la complexité de gestion des sous-modules Git volumineux et des versions manuelles. Il apporte le versionnement sémantique (^ et ~), la résolution automatique des dépendances transitives et la compilation modulaire à la demande.",
},
{
pergunta: "Puis-je créer et publier mes propres composants ?",
resposta: "Oui, tout développeur ou entreprise peut publier des composants publics ou privés sur l'ESP Component Registry à l'aide de l'outil officiel Compote CLI (pip install idf-component-manager).",
},
];
const faqsDe = [
{
pergunta: "Was sind die ESP Component Registry und der IDF Component Manager?",
resposta: "Die ESP Component Registry (components.espressif.com) ist das offizielle Repository für modulare Espressif-Pakete. Der IDF Component Manager ist das in ESP-IDF (ab v4.4 und Standard in v5.x+) integrierte Werkzeug, das Abhängigkeiten anhand des idf_component.yml-Manifests automatisch herunterlädt, kompiliert und auflöst.",
},
{
pergunta: "Wie füge ich eine Komponente zu meinem ESP-IDF-Projekt hinzu?",
resposta: "Sie können den Befehl 'idf.py add-dependency \"namespace/komponente^version\"' im Projektterminal ausführen (z. B.: idf.py add-dependency \"espressif/esp_lvgl_port^2.4.0\") oder direkt die Datei main/idf_component.yml bearbeiten.",
},
{
pergunta: "Was ist der Unterschied zwischen der Component Registry und Git-Submodulen?",
resposta: "Die Component Registry beseitigt die Komplexität schwerfälliger Git-Submodule und manueller Versionsverwaltung. Sie bietet semantische Versionierung (^ und ~), automatische transitive Abhängigkeitsauflösung und modulare On-Demand-Kompilierung.",
},
{
pergunta: "Kann ich eigene Komponenten erstellen und veröffentlichen?",
resposta: "Ja, Entwickler und Unternehmen können öffentliche oder private Komponenten mit dem offiziellen Compote-CLI-Tool (pip install idf-component-manager) in der ESP Component Registry veröffentlichen.",
},
];
export default async function ComponentesLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const faqs = isEs ? faqsEs : isEn ? faqsEn : faqsPt;
const isFr = locale === "fr";
const isDe = locale === "de";
const faqs = isEs ? faqsEs : isEn ? faqsEn : isFr ? faqsFr : isDe ? faqsDe : faqsPt;
const canonPath = isEs ? "/es/componentes" : isEn ? "/en/components" : isFr ? "/fr/composants" : isDe ? "/de/komponenten" : "/componentes";
return (
<>
@@ -125,8 +192,8 @@ export default async function ComponentesLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: "ESP Component Registry", caminho: isEs ? "/es/componentes" : isEn ? "/en/components" : "/componentes" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : isFr ? "Accueil" : isDe ? "Startseite" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : isFr ? "/fr" : isDe ? "/de" : "/" },
{ nome: "ESP Component Registry", caminho: canonPath },
])
),
}}
@@ -146,13 +213,21 @@ export default async function ComponentesLayout({ children, params }) {
? "Hub de ESP Component Registry & Generador de Manifiesto"
: isEn
? "ESP Component Registry Hub & Manifest Builder"
: isFr
? "Hub de l'ESP Component Registry & Générateur de Manifeste"
: isDe
? "ESP Component Registry Hub & Manifest-Generator"
: "Hub do ESP Component Registry & Construtor de Manifesto",
descricao: isEs
? "Catálogo curado y generador de manifiesto idf_component.yml para componentes modulares de ESP-IDF."
: isEn
? "Curated catalog and idf_component.yml manifest generator for modular ESP-IDF components."
: isFr
? "Catalogue sélectionné et générateur de manifeste idf_component.yml pour les composants modulaires d'ESP-IDF."
: isDe
? "Kuratierter Katalog und idf_component.yml-Manifest-Generator für modulare ESP-IDF-Komponenten."
: "Catálogo curado e gerador de manifesto idf_component.yml para componentes modulares do ESP-IDF.",
caminho: isEs ? "/es/componentes" : isEn ? "/en/components" : "/componentes",
caminho: canonPath,
locale,
})
),
+75 -35
View File
@@ -9,6 +9,8 @@ import SectionHead from "@/components/SectionHead";
import * as componentesPt from "@/lib/componentRegistryData";
import * as componentesEn from "@/lib/data/en/componentes";
import * as componentesEs from "@/lib/data/es/componentes";
import * as componentesFr from "@/lib/data/fr/componentes";
import * as componentesDe from "@/lib/data/de/componentes";
import {
ArrowUpRight,
Box,
@@ -46,7 +48,9 @@ export default function ComponentRegistryPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const isEs = pathname?.startsWith("/es");
const { CATEGORIAS_COMPONENTES, COMPONENTES_REGISTRY, GUIA_COMPONENT_MANAGER } = isEs ? componentesEs : isEn ? componentesEn : componentesPt;
const isFr = pathname?.startsWith("/fr");
const isDe = pathname?.startsWith("/de");
const { CATEGORIAS_COMPONENTES, COMPONENTES_REGISTRY, GUIA_COMPONENT_MANAGER } = isEs ? componentesEs : isEn ? componentesEn : isFr ? componentesFr : isDe ? componentesDe : componentesPt;
const [busca, setBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
@@ -104,6 +108,10 @@ export default function ComponentRegistryPage() {
? `# No hay componentes seleccionados.\n# Haga clic en "+ Agregar al manifiesto" en las tarjetas de abajo.`
: isEn
? `# No components selected.\n# Click "+ Add to Manifest" on the cards below.`
: isFr
? `# Aucun composant sélectionné.\n# Cliquez sur "+ Ajouter au manifeste" sur les cartes ci-dessous.`
: isDe
? `# Keine Komponenten ausgewählt.\n# Klicken Sie auf den Karten unten auf "+ Zum Manifest hinzufügen".`
: `# Nenhum componente selecionado.\n# Clique em "+ Adicionar ao Manifesto" nos cards abaixo.`;
}
@@ -111,12 +119,16 @@ export default function ComponentRegistryPage() {
? `# main/idf_component.yml\n# Generado mediante ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/es/componentes)\n\ndependencies:\n`
: isEn
? `# main/idf_component.yml\n# Generated via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/en/components)\n\ndependencies:\n`
: isFr
? `# main/idf_component.yml\n# Généré via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/fr/composants)\n\ndependencies:\n`
: isDe
? `# main/idf_component.yml\n# Erstellt über den ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/de/komponenten)\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, COMPONENTES_REGISTRY, isEn, isEs]);
}, [componentesSelecionados, COMPONENTES_REGISTRY, isEn, isEs, isFr, isDe]);
const copiarManifesto = () => {
navigator.clipboard.writeText(manifestoYaml);
@@ -152,7 +164,9 @@ export default function ComponentRegistryPage() {
<PageHero
isEn={isEn}
isEs={isEs}
trail={[{ label: isEs ? "Component Registry" : isEn ? "Component Registry" : "Component Registry" }]}
isFr={isFr}
isDe={isDe}
trail={[{ label: isEs ? "Component Registry" : isEn ? "Component Registry" : isFr ? "Component Registry" : isDe ? "Component Registry" : "Component Registry" }]}
icon={Box}
label="ESP-IDF Component Manager"
title="ESP Component Registry"
@@ -161,24 +175,28 @@ export default function ComponentRegistryPage() {
? "El catálogo oficial de paquetes reutilizables de Espressif para ESP-IDF. Elija controladores, voz y pilas industriales, copie el comando y genere el manifiesto del proyecto."
: isEn
? "Official Espressif catalog of reusable packages for ESP-IDF. Choose drivers, voice stacks, and industrial protocols, copy commands, and generate project manifests."
: isFr
? "Le catalogue officiel de paquets réutilisables d'Espressif pour ESP-IDF. Choisissez vos pilotes, piles vocales et protocoles industriels, copiez la commande et générez le manifeste du projet."
: isDe
? "Der offizielle Espressif-Katalog für wiederverwendbare Pakete für ESP-IDF. Wählen Sie Treiber, Sprach- und Industrieprotokolle, kopieren Sie den Befehl und erstellen Sie das Projektmanifest."
: "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">
{isEs ? "Abrir el registro oficial" : isEn ? "Open official registry" : "Abrir o registro oficial"}
{isEs ? "Abrir el registro oficial" : isEn ? "Open official registry" : isFr ? "Ouvrir le registre officiel" : isDe ? "Offizielles Repository öffnen" : "Abrir o registro oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : 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)" : isFr ? "(ouvre un nouvel onglet)" : isDe ? "(öffnet in neuem Tab)" : "(abre em nova aba)"}</span>
</a>
<a href="#manifesto-builder" className="btn btn-ghost">
{isEs ? "Generar idf_component.yml" : isEn ? "Generate idf_component.yml" : "Gerar idf_component.yml"}
{isEs ? "Generar idf_component.yml" : isEn ? "Generate idf_component.yml" : isFr ? "Générer idf_component.yml" : isDe ? "idf_component.yml erstellen" : "Gerar idf_component.yml"}
</a>
</PageHero>
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Búsqueda y filtros" : isEn ? "Search and filters" : "Busca e filtros"}>
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Búsqueda y filtros" : isEn ? "Search and filters" : isFr ? "Recherche et filtres" : isDe ? "Suche und Filter" : "Busca e filtros"}>
<div className="panel p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-componente" className="sr-only">
{isEs ? "Buscar por nombre, namespace o etiqueta" : isEn ? "Search by name, namespace, or tag" : "Buscar por nome, namespace ou tag"}
{isEs ? "Buscar por nombre, namespace o etiqueta" : isEn ? "Search by name, namespace, or tag" : isFr ? "Rechercher par nom, namespace ou tag" : isDe ? "Nach Name, Namespace oder Tag suchen" : "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
@@ -186,14 +204,14 @@ export default function ComponentRegistryPage() {
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder={isEs ? "Buscar por nombre, namespace o etiqueta" : isEn ? "Search by name, namespace, or tag" : "Buscar por nome, namespace ou tag"}
placeholder={isEs ? "Buscar por nombre, namespace o etiqueta" : isEn ? "Search by name, namespace, or tag" : isFr ? "Rechercher par nom, namespace ou tag" : isDe ? "Nach Name, Namespace oder Tag suchen" : "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">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</h2>
<h2 className="text-sm font-semibold text-ink">{isEs ? "Categoría" : isEn ? "Category" : isFr ? "Catégorie" : isDe ? "Kategorie" : "Categoria"}</h2>
<div className="mt-3 flex flex-wrap gap-2">
{CATEGORIAS_COMPONENTES.map((cat) => {
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
@@ -201,7 +219,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" ? (isEs ? "Todos" : isEn ? "All" : "Todos") : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
{cat.id === "todos" ? (isEs ? "Todos" : isEn ? "All" : isFr ? "Tous" : isDe ? "Alle" : "Todos") : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
<span className="tabular-nums opacity-80">{contagem}</span>
</button>
);
@@ -210,11 +228,11 @@ export default function ComponentRegistryPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">{isEs ? "Chip compatible" : isEn ? "Compatible chip" : "Chip compatível"}</h2>
<h2 className="text-sm font-semibold text-ink">{isEs ? "Chip compatible" : isEn ? "Compatible chip" : isFr ? "Puce compatible" : isDe ? "Kompatibler 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" ? (isEs ? "Todos" : isEn ? "All" : "Todos") : chip}
{chip === "todos" ? (isEs ? "Todos" : isEn ? "All" : isFr ? "Tous" : isDe ? "Alle" : "Todos") : chip}
</button>
))}
</div>
@@ -225,17 +243,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 ? (isEs ? "componente" : isEn ? "component" : "componente") : (isEs ? "componentes" : isEn ? "components" : "componentes")}
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? (isEs ? "componente" : isEn ? "component" : isFr ? "composant" : isDe ? "Komponente" : "componente") : (isEs ? "componentes" : isEn ? "components" : isFr ? "composants" : isDe ? "Komponenten" : "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" />
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : isFr ? "Effacer les filtres" : isDe ? "Filter zurücksetzen" : "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">
{isEs ? "Manifiesto:" : isEn ? "Manifest:" : "Manifesto:"} {componentesSelecionados.length} {componentesSelecionados.length === 1 ? (isEs ? "componente" : isEn ? "component" : "componente") : (isEs ? "componentes" : isEn ? "components" : "componentes")}
{isEs ? "Manifiesto:" : isEn ? "Manifest:" : isFr ? "Manifeste :" : isDe ? "Manifest :" : "Manifesto:"} {componentesSelecionados.length} {componentesSelecionados.length === 1 ? (isEs ? "componente" : isEn ? "component" : isFr ? "composant" : isDe ? "Komponente" : "componente") : (isEs ? "componentes" : isEn ? "components" : isFr ? "composants" : isDe ? "Komponenten" : "componentes")}
</a>
</div>
</div>
@@ -244,17 +262,21 @@ export default function ComponentRegistryPage() {
<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">
{isEs ? "No se encontraron componentes" : isEn ? "No components found" : "Nenhum componente encontrado"}
{isEs ? "No se encontraron componentes" : isEn ? "No components found" : isFr ? "Aucun composant trouvé" : isDe ? "Keine Komponenten gefunden" : "Nenhum componente encontrado"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEs
? "Busque por un término más general o regrese a todas las categorías."
: isEn
? "Search for a more general term or return to all categories."
: isFr
? "Recherchez un terme plus général ou revenez à toutes les catégories."
: isDe
? "Suchen Sie nach einem allgemeineren Begriff oder kehren Sie zu allen Kategorien zurück."
: "Busque por um termo mais geral ou volte para todas as categorias."}
</p>
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : isFr ? "Effacer les filtres" : isDe ? "Filter zurücksetzen" : "Limpar filtros"}
</button>
</div>
) : (
@@ -274,7 +296,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" />
{isEs ? "Oficial" : isEn ? "Official" : "Oficial"}
{isEs ? "Oficial" : isEn ? "Official" : isFr ? "Officiel" : isDe ? "Offiziell" : "Oficial"}
</span>
)}
</p>
@@ -298,12 +320,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={isEs ? `Copiar comando para agregar ${comp.name}` : isEn ? `Copy command to add ${comp.name}` : `Copiar comando para adicionar ${comp.name}`}
aria-label={isEs ? `Copiar comando para agregar ${comp.name}` : isEn ? `Copy command to add ${comp.name}` : isFr ? `Copier la commande pour ajouter ${comp.name}` : isDe ? `Befehl zum Hinzufügen von ${comp.name} kopieren` : `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 ? (isEs ? "Comando copiado" : isEn ? "Command copied" : "Comando copiado") : ""}</p>
<p role="status" className="sr-only">{foiCopiado ? (isEs ? "Comando copiado" : isEn ? "Command copied" : isFr ? "Commande copiée" : isDe ? "Befehl kopiert" : "Comando copiado") : ""}</p>
<p className="mt-3 text-xs text-muted">
<span className="font-medium text-ink-soft">Chips: </span>
@@ -319,14 +341,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 ? (isEs ? "En manifiesto" : isEn ? "In manifest" : "No manifesto") : (isEs ? "Agregar al manifiesto" : isEn ? "Add to manifest" : "Adicionar ao manifesto")}
{selecionado ? (isEs ? "En manifiesto" : isEn ? "In manifest" : isFr ? "Dans le manifeste" : isDe ? "Im Manifest" : "No manifesto") : (isEs ? "Agregar al manifiesto" : isEn ? "Add to manifest" : isFr ? "Ajouter au manifeste" : isDe ? "Zum Manifest hinzufügen" : "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={isEs ? `Abrir ${comp.name} en el registro oficial (nueva pestaña)` : isEn ? `Open ${comp.name} in official registry (new tab)` : `Abrir ${comp.name} no registro oficial (nova aba)`}
aria-label={isEs ? `Abrir ${comp.name} en el registro oficial (nueva pestaña)` : isEn ? `Open ${comp.name} in official registry (new tab)` : isFr ? `Ouvrir ${comp.name} sur le registre officiel (nouvel onglet)` : isDe ? `${comp.name} in der offiziellen Registry öffnen (neuer Tab)` : `Abrir ${comp.name} no registro oficial (nova aba)`}
>
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
@@ -342,15 +364,19 @@ 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">{isEs ? "Generador de manifiesto" : isEn ? "Manifest generator" : "Gerador de manifesto"}</p>
<p className="eyebrow">{isEs ? "Generador de manifiesto" : isEn ? "Manifest generator" : isFr ? "Générateur de manifeste" : isDe ? "Manifest-Generator" : "Gerador de manifesto"}</p>
<h2 id="manifesto-titulo" className="section-title mt-3">
{isEs ? "Su idf_component.yml" : isEn ? "Your idf_component.yml" : "Seu idf_component.yml"}
{isEs ? "Su idf_component.yml" : isEn ? "Your idf_component.yml" : isFr ? "Votre idf_component.yml" : isDe ? "Ihre idf_component.yml" : "Seu idf_component.yml"}
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
{isEs ? (
<>Guarde el archivo en la carpeta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> del proyecto y ESP-IDF descargará todas las bibliotecas durante la compilación.</>
) : 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.</>
) : isFr ? (
<>Enregistrez le fichier dans le dossier <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> du projet et ESP-IDF téléchargera toutes les bibliothèques lors de la compilation.</>
) : isDe ? (
<>Speichern Sie die Datei im Ordner <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> Ihres Projekts, und ESP-IDF lädt beim Build alle Abhängigkeiten herunter.</>
) : (
<>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.</>
)}
@@ -359,27 +385,27 @@ export default function ComponentRegistryPage() {
<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 ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar YAML" : isEn ? "Copy YAML" : "Copiar YAML")}</span>
<span role="status">{manifestoCopiado ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar YAML" : isEn ? "Copy YAML" : isFr ? "Copier le YAML" : isDe ? "YAML kopieren" : "Copiar YAML")}</span>
</button>
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
<Download className="h-4 w-4" aria-hidden="true" />
{isEs ? "Descargar .yml" : isEn ? "Download .yml" : "Baixar .yml"}
{isEs ? "Descargar .yml" : isEn ? "Download .yml" : isFr ? "Télécharger .yml" : isDe ? ".yml herunterladen" : "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" />
{isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
{isEs ? "Limpiar" : isEn ? "Clear" : isFr ? "Effacer" : isDe ? "Leeren" : "Limpar"}
</button>
)}
</div>
</div>
<div className="code-panel mt-6">
<pre tabIndex={0} aria-label={isEs ? "Contenido de idf_component.yml" : isEn ? "Content of idf_component.yml" : "Conteúdo do idf_component.yml"} className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
<pre tabIndex={0} aria-label={isEs ? "Contenido de idf_component.yml" : isEn ? "Content of idf_component.yml" : isFr ? "Contenu de idf_component.yml" : isDe ? "Inhalt von 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={isEs ? "Componentes incluidos" : isEn ? "Included components" : "Componentes incluídos"}>
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label={isEs ? "Componentes incluidos" : isEn ? "Included components" : isFr ? "Composants inclus" : isDe ? "Enthaltene Komponenten" : "Componentes incluídos"}>
{componentesSelecionados.map((id) => {
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
if (!c) return null;
@@ -390,7 +416,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={isEs ? `Eliminar ${c.name} del manifiesto` : isEn ? `Remove ${c.name} from manifest` : `Remover ${c.name} do manifesto`}
aria-label={isEs ? `Eliminar ${c.name} del manifiesto` : isEn ? `Remove ${c.name} from manifest` : isFr ? `Retirer ${c.name} du manifeste` : isDe ? `${c.name} aus dem Manifest entfernen` : `Remover ${c.name} do manifesto`}
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>
@@ -403,7 +429,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={isEs ? "Guía rápida" : isEn ? "Quick guide" : "Guia rápido"} title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
<SectionHead id="guia-titulo" eyebrow={isEs ? "Guía rápida" : isEn ? "Quick guide" : isFr ? "Guide rapide" : isDe ? "Kurzanleitung" : "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) => (
@@ -416,7 +442,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" />
{isEs ? "Comandos esenciales" : isEn ? "Essential commands" : "Comandos essenciais"}
{isEs ? "Comandos esenciales" : isEn ? "Essential commands" : isFr ? "Commandes essentielles" : isDe ? "Wichtige Befehle" : "Comandos essenciais"}
</h3>
<ul className="mt-4 space-y-3">
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
@@ -431,7 +457,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}` ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar" : isEn ? "Copy" : "Copiar")}
{copiadoId === `cmd-${i}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar" : isEn ? "Copy" : isFr ? "Copier" : isDe ? "Kopieren" : "Copiar")}
</button>
</li>
))}
@@ -440,7 +466,7 @@ 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" />
{isEs ? "Versionamiento semántico en ESP-IDF" : isEn ? "Semantic versioning in ESP-IDF" : "Versionamento semântico no ESP-IDF"}
{isEs ? "Versionamiento semántico en ESP-IDF" : isEn ? "Semantic versioning in ESP-IDF" : isFr ? "Versionnement sémantique dans ESP-IDF" : isDe ? "Semantische Versionierung in ESP-IDF" : "Versionamento semântico no ESP-IDF"}
</h3>
<ul className="mt-2 list-disc space-y-1.5 pl-5">
{isEs ? (
@@ -457,6 +483,20 @@ export default function ComponentRegistryPage() {
<li><code>1.2.0</code>: locks to this exact version.</li>
<li><code>*</code>: accepts any version. Avoid in production.</li>
</>
) : isFr ? (
<>
<li><code>^1.2.0</code>: accepte les mises à jour mineures et les correctifs (1.2.1, 1.3.0), mais bloque les changements majeurs d'API (2.0.0). <strong>Recommandé.</strong></li>
<li><code>~1.2.0</code>: accepte uniquement les correctifs de bugs (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: verrouille sur cette version exacte.</li>
<li><code>*</code>: accepte n'importe quelle version. À éviter en production.</li>
</>
) : isDe ? (
<>
<li><code>^1.2.0</code>: akzeptiert Minor-Updates und Bugfixes (1.2.1, 1.3.0), blockiert aber Breaking Changes (2.0.0). <strong>Empfohlen.</strong></li>
<li><code>~1.2.0</code>: akzeptiert nur Patch-Releases (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: bindet fest an diese Version.</li>
<li><code>*</code>: akzeptiert jede Version. In Produktion vermeiden.</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>