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

+51 -29
View File
@@ -40,7 +40,7 @@ export function paginaMeta({ titulo, descricao, caminho, keywords = [], tipo = "
for (const loc of ENABLED_LOCALES) {
if (basePath === "/livro" && loc !== "pt") continue;
const locHref = getLocalizedHref(`/pt${basePath}`, loc);
const langKey = loc === "pt" ? "pt-BR" : loc;
const langKey = loc === "pt" ? "pt-BR" : loc === "es" ? "es-419" : loc;
languages[langKey] = `${SITE.url}${locHref}`;
}
if (ENABLED_LOCALES.includes("en") && basePath !== "/livro") {
@@ -94,6 +94,9 @@ export function paginaMeta({ titulo, descricao, caminho, keywords = [], tipo = "
export const jsonLd = {
siteESociedade(locale = "pt") {
const isEn = locale === "en";
const isEs = locale === "es";
const orgAnchor = isEn ? "organization" : isEs ? "organizacion" : "organizacao";
const authorAnchor = isEn ? "author" : "autor";
return {
"@context": "https://schema.org",
"@graph": [
@@ -102,15 +105,17 @@ export const jsonLd = {
"@id": `${SITE.url}/#site`,
url: `${SITE.url}/${locale}`,
name: SITE.nome,
inLanguage: isEn ? "en" : "pt-BR",
inLanguage: isEn ? "en" : isEs ? "es-419" : "pt-BR",
description: isEn
? "ESP32 ecosystem documentation: specifications, pinouts, comparison, and series selection."
: isEs
? "Documentación del ecosistema ESP32: especificaciones, patillaje, comparación y selección de series."
: "Documentação do ecossistema ESP32 em português brasileiro: especificações, pinagem, comparação e seleção de séries.",
publisher: { "@id": `${SITE.url}/#${isEn ? "organization" : "organizacao"}` },
publisher: { "@id": `${SITE.url}/#${orgAnchor}` },
},
{
"@type": "Organization",
"@id": `${SITE.url}/#${isEn ? "organization" : "organizacao"}`,
"@id": `${SITE.url}/#${orgAnchor}`,
name: "Ciência Embarcada",
url: SITE.blog,
logo: {
@@ -124,7 +129,7 @@ export const jsonLd = {
},
{
"@type": "Person",
"@id": `${SITE.url}/#${isEn ? "author" : "autor"}`,
"@id": `${SITE.url}/#${authorAnchor}`,
name: SITE.autor,
url: SITE.autorUrl,
email: SITE.autorEmail,
@@ -151,45 +156,51 @@ export const jsonLd = {
/** Documentação e ficha técnica da série como TechArticle para o Google. */
serie(chave, serie, locale = "pt") {
const isEn = locale === "en";
const isEs = locale === "es";
const prop = (nome, valor) =>
valor ? { "@type": "PropertyValue", name: nome, value: String(valor) } : null;
const propriedades = [
prop(isEn ? "Manufacturer" : "Fabricante", "Espressif Systems"),
prop(isEn ? "Architecture" : "Arquitetura", serie.arquitetura),
prop(isEn ? "Cores" : "Núcleos", serie.nucleos),
prop(isEn ? "Max frequency" : "Frequência máxima", serie.frequencia),
prop(isEn ? "Manufacturer" : isEs ? "Fabricante" : "Fabricante", "Espressif Systems"),
prop(isEn ? "Architecture" : isEs ? "Arquitectura" : "Arquitetura", serie.arquitetura),
prop(isEn ? "Cores" : isEs ? "Núcleos" : "Núcleos", serie.nucleos),
prop(isEn ? "Max frequency" : isEs ? "Frecuencia máxima" : "Frequência máxima", serie.frequencia),
prop("SRAM", serie.memoria_sram),
prop("RTC SRAM", serie.memoria_sram_rtc),
prop(isEn ? "Supported Flash" : "Flash suportada", serie.flash_suportada),
prop(isEn ? "Supported PSRAM" : "PSRAM suportada", serie.psram_suportada),
prop(isEn ? "Supported Flash" : isEs ? "Flash soportada" : "Flash suportada", serie.flash_suportada),
prop(isEn ? "Supported PSRAM" : isEs ? "PSRAM soportada" : "PSRAM suportada", serie.psram_suportada),
prop("GPIO", serie.gpio),
prop("Wi-Fi", serie.wifi),
prop("Bluetooth", serie.bluetooth),
prop("Zigbee / Thread / 802.15.4", serie.zigbee_thread),
prop(isEn ? "Package" : "Encapsulamento", serie.encapsulamento),
prop(isEn ? "Minimum ESP-IDF" : "ESP-IDF mínimo", serie.esp_idf_minimo),
prop(isEn ? "Package" : isEs ? "Encapsulado" : "Encapsulamento", serie.encapsulamento),
prop(isEn ? "Minimum ESP-IDF" : isEs ? "ESP-IDF mínimo" : "ESP-IDF mínimo", serie.esp_idf_minimo),
].filter(Boolean);
const imagem = serie.placa
? `${SITE.url}${serie.placa}`
: `${SITE.url}/opengraph-image.png`;
const basePath = isEn ? `/en/series/${chave}` : `/series/${chave}`;
const basePath = isEn ? `/en/series/${chave}` : isEs ? `/es/series/${chave}` : `/series/${chave}`;
const anchor = isEn ? "article" : isEs ? "articulo" : "artigo";
return {
"@context": "https://schema.org",
"@type": "TechArticle",
"@id": `${SITE.url}${basePath}#${isEn ? "article" : "artigo"}`,
"@id": `${SITE.url}${basePath}#${anchor}`,
headline: isEn
? `${serie.nome_completo || chave}: specifications and pinout`
: isEs
? `${serie.nome_completo || chave}: especificaciones y patillaje`
: `${serie.nome_completo || chave}: especificações e pinagem`,
name: isEn
? `${serie.nome_completo || chave} - Technical Documentation and Specifications`
: isEs
? `${serie.nome_completo || chave} - Documentación técnica y especificaciones`
: `${serie.nome_completo || chave} - Documentação Técnica e Especificações`,
description: serie.descricao,
image: [imagem],
inLanguage: isEn ? "en" : "pt-BR",
inLanguage: isEn ? "en" : isEs ? "es-419" : "pt-BR",
datePublished: "2026-08-25T00:00:00.000Z",
dateModified: new Date().toISOString(),
mainEntityOfPage: `${SITE.url}${basePath}`,
@@ -222,20 +233,24 @@ export const jsonLd = {
/** Documentação técnica de framework. */
framework(chave, framework, locale = "pt") {
const isEn = locale === "en";
const basePath = isEn ? `/en/frameworks/${encodeURIComponent(chave)}` : `/frameworks/${encodeURIComponent(chave)}`;
const isEs = locale === "es";
const basePath = isEn ? `/en/frameworks/${encodeURIComponent(chave)}` : isEs ? `/es/frameworks/${encodeURIComponent(chave)}` : `/frameworks/${encodeURIComponent(chave)}`;
const anchor = isEn ? "article" : isEs ? "articulo" : "artigo";
return {
"@context": "https://schema.org",
"@type": "TechArticle",
"@id": `${SITE.url}${basePath}#${isEn ? "article" : "artigo"}`,
"@id": `${SITE.url}${basePath}#${anchor}`,
headline: isEn
? `${framework.nome} for ESP32: Guide and Documentation`
: isEs
? `${framework.nome} para ESP32: Guía y documentación`
: `${framework.nome} para ESP32: Guia e Documentação`,
name: isEn
? `${framework.nome_completo || framework.nome} for ESP32`
: `${framework.nome_completo || framework.nome} para ESP32`,
description: framework.descricao,
image: [`${SITE.url}/opengraph-image.png`],
inLanguage: isEn ? "en" : "pt-BR",
inLanguage: isEn ? "en" : isEs ? "es-419" : "pt-BR",
datePublished: "2026-08-25T00:00:00.000Z",
dateModified: new Date().toISOString(),
mainEntityOfPage: `${SITE.url}${basePath}`,
@@ -270,19 +285,21 @@ export const jsonLd = {
/** Glossário: marca cada verbete como termo definido. */
glossario(categorias, locale = "pt") {
const isEn = locale === "en";
const basePath = isEn ? "/en/glossary" : "/glossario";
const isEs = locale === "es";
const basePath = isEn ? "/en/glossary" : isEs ? "/es/glosario" : "/glossario";
const anchor = isEn ? "glossary" : isEs ? "glosario" : "glossario";
return {
"@context": "https://schema.org",
"@type": "DefinedTermSet",
"@id": `${SITE.url}${basePath}#${isEn ? "glossary" : "glossario"}`,
name: isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
inLanguage: isEn ? "en" : "pt-BR",
"@id": `${SITE.url}${basePath}#${anchor}`,
name: isEn ? "ESP32 Technical Glossary" : isEs ? "Glosario técnico de ESP32" : "Glossário Técnico do ESP32",
inLanguage: isEn ? "en" : isEs ? "es-419" : "pt-BR",
hasDefinedTerm: categorias.flatMap((cat) =>
cat.termos.map((t) => ({
"@type": "DefinedTerm",
name: t.termo,
description: t.definicao,
inDefinedTermSet: `${SITE.url}${basePath}#${isEn ? "glossary" : "glossario"}`,
inDefinedTermSet: `${SITE.url}${basePath}#${anchor}`,
}))
),
};
@@ -304,6 +321,7 @@ export const jsonLd = {
/** Aplicação web / ferramenta interativa (Seletor, Comparador, Catálogo). */
aplicacaoWeb({ nome, descricao, caminho, locale = "pt" }) {
const isEn = locale === "en";
const isEs = locale === "es";
return {
"@context": "https://schema.org",
"@type": "WebApplication",
@@ -314,7 +332,7 @@ export const jsonLd = {
applicationCategory: "DeveloperApplication",
operatingSystem: "All",
browserRequirements: "Requires JavaScript",
inLanguage: isEn ? "en" : "pt-BR",
inLanguage: isEn ? "en" : isEs ? "es-419" : "pt-BR",
author: {
"@type": "Person",
name: SITE.autor,
@@ -396,17 +414,21 @@ export const jsonLd = {
/** Página Sobre com metadados E-E-A-T de autoria e reputação. */
paginaSobre(locale = "pt") {
const isEn = locale === "en";
const basePath = isEn ? "/en/about" : "/sobre";
const isEs = locale === "es";
const basePath = isEn ? "/en/about" : isEs ? "/es/acerca" : "/sobre";
const anchor = isEn ? "about" : isEs ? "acerca" : "sobre";
return {
"@context": "https://schema.org",
"@type": "AboutPage",
"@id": `${SITE.url}${basePath}#${isEn ? "about" : "sobre"}`,
name: isEn ? "About ESPDocs" : "Sobre o ESPDocs",
"@id": `${SITE.url}${basePath}#${anchor}`,
name: isEn ? "About ESPDocs" : isEs ? "Acerca de ESPDocs" : "Sobre o ESPDocs",
description: isEn
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem."
: isEs
? "ESPDocs es una plataforma independiente que reúne la documentación técnica del ecosistema ESP32."
: "O ESPDocs é uma plataforma independente que reúne a documentação do ecossistema ESP32 em português brasileiro.",
url: `${SITE.url}${basePath}`,
inLanguage: isEn ? "en" : "pt-BR",
inLanguage: isEn ? "en" : isEs ? "es-419" : "pt-BR",
mainEntity: {
"@type": "Person",
name: SITE.autor,