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

+43 -25
View File
@@ -7,14 +7,16 @@ import { notFound } from "next/navigation";
import CodeExample from "@/components/CodeExample";
import frameworksDataPt from "@/public/frameworks.json";
import frameworksDataEn from "@/data/en/frameworks.json";
import frameworksDataEs from "@/data/es/frameworks.json";
import { getLocalizedHref } from "@/i18n/links";
import { ArrowRight, ArrowUpRight, BookOpen, Box, Check, Code, Layers } from "lucide-react";
export default async function FrameworkDetail({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const frameworksData = isEn ? frameworksDataEn : frameworksDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const framework = frameworksData[key];
if (!framework) {
@@ -36,8 +38,8 @@ export default async function FrameworkDetail({ params }) {
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: isEn ? "Frameworks" : "Frameworks", caminho: getLocalizedHref("/frameworks", currentLocale) },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: "Frameworks", caminho: getLocalizedHref("/frameworks", currentLocale) },
{ nome: framework.nome, caminho: getLocalizedHref(`/frameworks/${encodeURIComponent(key)}`, currentLocale) },
])),
}}
@@ -48,7 +50,8 @@ export default async function FrameworkDetail({ params }) {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
isEs={isEs}
trail={[{ label: isEs ? "Frameworks y SDKs" : isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
icon={Layers}
label={framework.tipo}
title={framework.nome}
@@ -57,41 +60,41 @@ export default async function FrameworkDetail({ params }) {
{framework.documentacao && (
<a href={framework.documentacao} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<BookOpen className="h-4 w-4" aria-hidden="true" />
{isEn ? "Official documentation" : "Documentação oficial"}
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : "Documentação oficial"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{framework.repositorio && (
<a href={framework.repositorio} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
{isEn ? "GitHub repository" : "Repositório no GitHub"}
{isEs ? "Repositorio en GitHub" : isEn ? "GitHub repository" : "Repositório no GitHub"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
<section className="shell mt-10" aria-label={isEn ? "Summary" : "Resumo"}>
<section className="shell mt-10" aria-label={isEs ? "Resumen" : isEn ? "Summary" : "Resumo"}>
<dl className="grid gap-3 md:grid-cols-3">
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEn ? "Language" : "Linguagem"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Lenguaje" : isEn ? "Language" : "Linguagem"}</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.linguagem}</dd>
<dd className="mt-0.5 text-xs text-muted">{framework.nome_completo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEn ? "Category" : "Categoria"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.tipo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEn ? "Main purpose" : "Objetivo principal"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Objetivo principal" : isEn ? "Main purpose" : "Objetivo principal"}</dt>
<dd className="mt-1 text-sm font-medium leading-relaxed text-ink">{framework.funcao_principal}</dd>
</div>
</dl>
</section>
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label={isEn ? "Features and applications" : "Recursos e aplicações"}>
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label={isEs ? "Funciones y aplicaciones" : isEn ? "Features and applications" : "Recursos e aplicações"}>
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
{isEn ? "Key features" : "Recursos de destaque"}
{isEs ? "Funciones destacadas" : isEn ? "Key features" : "Recursos de destaque"}
</h2>
<ul className="mt-5 space-y-3">
{features.map((feature) => (
@@ -105,7 +108,7 @@ export default async function FrameworkDetail({ params }) {
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
{isEn ? "When to use" : "Quando usar"}
{isEs ? "Cuándo usar" : isEn ? "When to use" : "Quando usar"}
</h2>
<ul className="mt-5 space-y-2.5">
{useCases.map((useCase) => (
@@ -123,13 +126,19 @@ export default async function FrameworkDetail({ params }) {
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "ESP-IDF 4.4 or higher" : "ESP-IDF 4.4 ou superior"}
{isEs ? "ESP-IDF 4.4 o superior" : isEn ? "ESP-IDF 4.4 or higher" : "ESP-IDF 4.4 ou superior"}
</span>
<h2 className="section-title mt-4">
{isEn ? "Modules with the ESP Component Registry" : "Módulos com o ESP Component Registry"}
{isEs ? "Módulos con ESP Component Registry" : isEn ? "Modules with the ESP Component Registry" : "Módulos com o ESP Component Registry"}
</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
{isEn ? (
{isEs ? (
<>
En los proyectos ESP-IDF v4.4+ y v5.x, las bibliotecas ya no necesitan agregarse como submódulos Git. Utilice el{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> y el comando{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> para descargar controladores de pantalla (LVGL), pilas industriales (Modbus), RainMaker y más.
</>
) : isEn ? (
<>
In ESP-IDF v4.4+ and v5.x projects, libraries no longer need to be added as Git submodules. Use{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> and the command{" "}
@@ -145,7 +154,7 @@ export default async function FrameworkDetail({ params }) {
</p>
</div>
<Link href={getLocalizedHref("/componentes", currentLocale)} className="btn btn-primary shrink-0">
{isEn ? "Explore components" : "Explorar componentes"}
{isEs ? "Explorar componentes" : isEn ? "Explore components" : "Explorar componentes"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -161,7 +170,7 @@ export default async function FrameworkDetail({ params }) {
<div className="shell section pt-10">
<Link href={getLocalizedHref("/frameworks", currentLocale)} className="inline-flex min-h-11 items-center gap-2 text-sm font-medium text-accent hover:text-accent-strong">
<ArrowRight className="h-4 w-4 rotate-180" aria-hidden="true" />
{isEn ? "View all frameworks" : "Ver todos os frameworks"}
{isEs ? "Ver todos los frameworks" : isEn ? "View all frameworks" : "Ver todos os frameworks"}
</Link>
</div>
</main>
@@ -174,24 +183,33 @@ export default async function FrameworkDetail({ params }) {
export async function generateMetadata({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const frameworksData = isEn ? frameworksDataEn : frameworksDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const framework = frameworksData[key];
if (!framework) {
return { title: isEn ? "Framework not found" : "Framework não encontrado", robots: { index: false, follow: false } };
return { title: isEs ? "Framework no encontrado" : isEn ? "Framework not found" : "Framework não encontrado", robots: { index: false, follow: false } };
}
return {
...paginaMeta({
titulo: isEn ? `${framework.nome} for ESP32 · Technical Guide` : `${framework.nome} para ESP32 · Guia Técnico`,
descricao: isEn
titulo: isEs ? `${framework.nome} para ESP32 · Guía técnica` : isEn ? `${framework.nome} for ESP32 · Technical Guide` : `${framework.nome} para ESP32 · Guia Técnico`,
descricao: isEs
? `${framework.nome_completo}: ${framework.descricao} Guía con ejemplos prácticos de código y casos de uso.`
: isEn
? `${framework.nome_completo}: ${framework.descricao} Guide with practical code examples and use cases.`
: `${framework.nome_completo}: ${framework.descricao} Guia com exemplos práticos de código e casos de uso.`,
caminho: `/frameworks/${encodeURIComponent(key)}`,
tipo: "article",
locale: currentLocale,
keywords: isEn
keywords: isEs
? [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `programar ESP32 con ${framework.nome}`,
"ESP32", "framework ESP32",
]
: isEn
? [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `program ESP32 with ${framework.nome}`,
+26 -6
View File
@@ -3,17 +3,36 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
export async function generateMetadata({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return {
...paginaMeta({
titulo: isEn
titulo: isEs
? "Frameworks y SDKs para ESP32: Guía completa y comparativa"
: isEn
? "ESP32 Frameworks & SDKs: Complete Guide and Comparison"
: "Frameworks & SDKs para ESP32: Guia Completo e Comparativo",
descricao: isEn
descricao: isEs
? "Guía completa de desarrollo: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino y MicroPython con ejemplos de código."
: isEn
? "Complete development guide: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino, and MicroPython with code examples."
: "Guia completo de desenvolvimento: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino e MicroPython com exemplos de código.",
caminho: "/frameworks",
locale: isEn ? "en" : "pt",
keywords: isEn
locale,
keywords: isEs
? [
"ESP-IDF",
"ESP-RainMaker",
"ESP-Matter",
"Rust ESP32",
"ESP-SR",
"ESP-ADF",
"ESP-IoT-Solution",
"Arduino ESP32",
"MicroPython ESP32",
"frameworks ESP32",
"programar ESP32",
]
: isEn
? [
"ESP-IDF",
"ESP-RainMaker",
@@ -47,6 +66,7 @@ export async function generateMetadata({ params }) {
export default async function FrameworksLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
return (
<>
<script
@@ -54,8 +74,8 @@ export default async function FrameworksLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEn ? "Home" : "Início", caminho: isEn ? "/en" : "/" },
{ nome: "Frameworks", caminho: isEn ? "/en/frameworks" : "/frameworks" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: "Frameworks", caminho: isEs ? "/es/frameworks" : isEn ? "/en/frameworks" : "/frameworks" },
])
),
}}
+58 -15
View File
@@ -5,6 +5,7 @@ import SectionHead from "@/components/SectionHead";
import Link from "next/link";
import frameworksDataPt from "@/public/frameworks.json";
import frameworksDataEn from "@/data/en/frameworks.json";
import frameworksDataEs from "@/data/es/frameworks.json";
import { paginaMeta } from "@/lib/seo";
import { getLocalizedHref } from "@/i18n/links";
import { ArrowRight, ArrowUpRight, Box, Brain, Cloud, GraduationCap, Layers, Mic, Music, Sparkles } from "lucide-react";
@@ -75,13 +76,47 @@ const GUIAS_EN = [
},
];
const GUIAS_ES = [
{
icon: GraduationCap,
titulo: "Estoy comenzando",
texto: "Arduino o MicroPython son los caminos más rápidos para probar sensores y APIs. Para producto, elija ESP-IDF o Rust.",
links: [
["Arduino", "/frameworks/Arduino"],
["MicroPython", "/frameworks/MicroPython"],
["Rust", "/frameworks/Rust-ESP"],
],
},
{
icon: Cloud,
titulo: "Nube y hogar conectado",
texto: "ESP-Matter para interoperabilidad, ESP-RainMaker para nube lista y Zigbee para redes locales de bajo consumo.",
links: [
["RainMaker", "/frameworks/ESP-RainMaker"],
["Matter", "/frameworks/ESP-Matter"],
["Zigbee", "/frameworks/ESP-Zigbee-SDK"],
],
},
{
icon: Sparkles,
titulo: "Audio, voz e IA",
texto: "ESP-ADF para procesamiento de audio, ESP-SR para reconocimiento de voz offline y ESP-NN para acelerar TinyML.",
links: [
["Audio", "/frameworks/ESP-ADF", Music],
["Voz", "/frameworks/ESP-SR", Mic],
["IA", "/frameworks/ESP-NN", Brain],
],
},
];
export default async function Frameworks({ params }) {
const { locale } = await params;
const isEn = locale === "en";
const currentLocale = isEn ? "en" : "pt";
const frameworksData = isEn ? frameworksDataEn : frameworksDataPt;
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const frameworks = Object.entries(frameworksData);
const guias = isEn ? GUIAS_EN : GUIAS_PT;
const guias = isEs ? GUIAS_ES : isEn ? GUIAS_EN : GUIAS_PT;
return (
<div className="site-canvas min-h-screen">
@@ -90,18 +125,21 @@ export default async function Frameworks({ params }) {
<main id="conteudo">
<PageHero
isEn={isEn}
trail={[{ label: isEn ? "Frameworks & SDKs" : "Frameworks e SDKs" }]}
isEs={isEs}
trail={[{ label: isEs ? "Frameworks y SDKs" : isEn ? "Frameworks & SDKs" : "Frameworks e SDKs" }]}
icon={Layers}
label={isEn ? `${frameworks.length} frameworks and SDKs` : `${frameworks.length} frameworks e SDKs`}
title={isEn ? "ESP32 Frameworks and SDKs" : "Frameworks e SDKs do ESP32"}
label={isEs ? `${frameworks.length} frameworks y SDKs` : isEn ? `${frameworks.length} frameworks and SDKs` : `${frameworks.length} frameworks e SDKs`}
title={isEs ? "Frameworks y SDKs de ESP32" : isEn ? "ESP32 Frameworks and SDKs" : "Frameworks e SDKs do ESP32"}
lede={
isEn
isEs
? "Desde C/C++ nativo con FreeRTOS hasta Rust, nube IoT, reconocimiento de voz, audio y Matter. Elija según lo que desee construir."
: isEn
? "From native C/C++ with FreeRTOS to Rust, IoT cloud, speech recognition, audio, and Matter. Choose by what you want to build."
: "Do C/C++ nativo com FreeRTOS ao Rust, passando por nuvem IoT, reconhecimento de voz, áudio e Matter. Escolha pelo que você quer construir."
}
/>
<section className="shell pt-10 sm:pt-12" aria-label={isEn ? "Where to begin" : "Por onde começar"}>
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Por dónde empezar" : isEn ? "Where to begin" : "Por onde começar"}>
<div className="grid gap-4 md:grid-cols-3">
{guias.map(({ icon: Icon, titulo, texto, links }) => (
<div key={titulo} className="card flex flex-col p-5 sm:p-6">
@@ -130,11 +168,16 @@ export default async function Frameworks({ params }) {
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
{isEn ? "Modular architecture" : "Arquitetura modular"}
{isEs ? "Arquitectura modular" : isEn ? "Modular architecture" : "Arquitetura modular"}
</span>
<h2 className="section-title mt-4">ESP Component Registry</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
{isEn ? (
{isEs ? (
<>
Instale controladores de pantalla (LVGL), pilas industriales (Modbus), integraciones en la nube y paquetes de soporte para placas
(BSPs) con una sola línea en su manifiesto <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code>.
</>
) : isEn ? (
<>
Install display drivers (LVGL), industrial stacks (Modbus), cloud integrations, and board support packages
(BSPs) with a single line in your <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> manifest.
@@ -148,7 +191,7 @@ export default async function Frameworks({ params }) {
</p>
</div>
<Link href={getLocalizedHref("/componentes", currentLocale)} className="btn btn-primary shrink-0">
{isEn ? "Explore components" : "Explorar componentes"}
{isEs ? "Explorar componentes" : isEn ? "Explore components" : "Explorar componentes"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -157,9 +200,9 @@ export default async function Frameworks({ params }) {
<section className="shell section" aria-labelledby="todos-titulo">
<SectionHead
id="todos-titulo"
eyebrow={isEn ? "All" : "Todos"}
title={isEn ? "All frameworks" : "Todos os frameworks"}
lede={isEn ? "Each page provides features, ideal use cases, code examples, and official links." : "Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais."}
eyebrow={isEs ? "Todos" : isEn ? "All" : "Todos"}
title={isEs ? "Todos los frameworks" : isEn ? "All frameworks" : "Todos os frameworks"}
lede={isEs ? "Cada ficha ofrece funciones, aplicaciones ideales, ejemplos de código y enlaces oficiales." : isEn ? "Each page provides features, ideal use cases, code examples, and official links." : "Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais."}
/>
<div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -181,7 +224,7 @@ export default async function Frameworks({ params }) {
</ul>
<span className="mt-5 inline-flex items-center gap-1 border-t border-line pt-4 text-sm font-medium text-accent">
{isEn ? "View details and example" : "Ver detalhes e exemplo"}
{isEs ? "Ver detalles y ejemplo" : isEn ? "View details and example" : "Ver detalhes e exemplo"}
<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>