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

+44 -5
View File
@@ -2,21 +2,33 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
import { categoriasGlossario as categoriasGlossarioPt } from "@/lib/glossarioData";
import { categoriasGlossario as categoriasGlossarioEn } from "@/lib/data/en/glossario.js";
import { categoriasGlossario as categoriasGlossarioEs } from "@/lib/data/es/glossario.js";
import { categoriasGlossario as categoriasGlossarioFr } from "@/lib/data/fr/glossario.js";
import { categoriasGlossario as categoriasGlossarioDe } from "@/lib/data/de/glossario.js";
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
? "Glosario técnico de ESP32: Términos y conceptos"
: isEn
? "ESP32 Technical Glossary: Terms and Concepts"
: isFr
? "Glossaire technique de l'ESP32 : termes et concepts"
: isDe
? "Technisches ESP32-Glossar: Begriffe und Konzepte"
: "Glossário Técnico do ESP32: Termos e Conceitos",
descricao: isEs
? "Más de 50 conceptos esenciales de ESP32 explicados: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter y Thread."
: isEn
? "Over 50 essential ESP32 concepts explained: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter, and Thread."
: isFr
? "Plus de 50 concepts essentiels de l'ESP32 expliqués : GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter et Thread."
: isDe
? "Über 50 wichtige ESP32-Konzepte verständlich erklärt: GPIO, Strapping-Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter und Thread."
: "Mais de 50 termos e conceitos essenciais do ecossistema ESP32 explicados em português: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter e Thread.",
caminho: "/glossario",
locale,
@@ -40,6 +52,26 @@ export async function generateMetadata({ params }) {
"microcontroller technical terms",
"ESP32 hardware concepts",
]
: isFr
? [
"glossaire ESP32",
"qu'est-ce que le GPIO",
"que sont les strapping pins",
"qu'est-ce que l'ESP32 ULP",
"qu'est-ce que l'ESP32 PSRAM",
"termes techniques microcontrôleur",
"concepts matériels ESP32",
]
: isDe
? [
"ESP32 Glossar",
"was ist GPIO",
"was sind Strapping-Pins",
"was ist ESP32 ULP",
"was ist ESP32 PSRAM",
"technische Mikrocontroller-Begriffe",
"ESP32 Hardware-Konzepte",
]
: [
"glossário ESP32",
"o que é GPIO",
@@ -56,7 +88,10 @@ export default async function GlossarioLayout({ children, params }) {
const { locale } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const categorias = isEs ? categoriasGlossarioEs : isEn ? categoriasGlossarioEn : categoriasGlossarioPt;
const isFr = locale === "fr";
const isDe = locale === "de";
const categorias = isEs ? categoriasGlossarioEs : isEn ? categoriasGlossarioEn : isFr ? categoriasGlossarioFr : isDe ? categoriasGlossarioDe : categoriasGlossarioPt;
const canonPath = isEs ? "/es/glosario" : isEn ? "/en/glossary" : isFr ? "/fr/glossaire" : isDe ? "/de/glossar" : "/glossario";
return (
<>
@@ -71,8 +106,8 @@ export default async function GlossarioLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: isEs ? "Glosario técnico" : isEn ? "Technical Glossary" : "Glossário Técnico", caminho: isEs ? "/es/glosario" : isEn ? "/en/glossary" : "/glossario" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : isFr ? "Accueil" : isDe ? "Startseite" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : isFr ? "/fr" : isDe ? "/de" : "/" },
{ nome: isEs ? "Glosario técnico" : isEn ? "Technical Glossary" : isFr ? "Glossaire technique" : isDe ? "Technisches Glossar" : "Glossário Técnico", caminho: canonPath },
])
),
}}
@@ -82,13 +117,17 @@ export default async function GlossarioLayout({ children, params }) {
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: isEs ? "Glosario técnico de ESP32" : isEn ? "ESP32 Technical Glossary" : "Glossário Técnico do ESP32",
nome: isEs ? "Glosario técnico de ESP32" : isEn ? "ESP32 Technical Glossary" : isFr ? "Glossaire technique de l'ESP32" : isDe ? "Technisches ESP32-Glossar" : "Glossário Técnico do ESP32",
descricao: isEs
? "Enciclopedia de términos técnicos y conceptos de hardware y software del ecosistema ESP32."
: isEn
? "Encyclopedia of hardware and software technical terms for the ESP32 ecosystem."
: isFr
? "Encyclopédie des termes techniques et des concepts matériels et logiciels de l'écosystème ESP32."
: isDe
? "Enzyklopädie technischer Begriffe und Hardware-/Softwarekonzepte für das ESP32-Ökosystem."
: "Enciclopédia de termos técnicos e conceitos de hardware e software do ecossistema ESP32.",
caminho: isEs ? "/es/glosario" : isEn ? "/en/glossary" : "/glossario",
caminho: canonPath,
locale,
})
),
+107 -28
View File
@@ -9,6 +9,8 @@ import Link from "next/link";
import * as glossarioPt from "@/lib/glossarioData";
import * as glossarioEn from "@/lib/data/en/glossario";
import * as glossarioEs from "@/lib/data/es/glossario";
import * as glossarioFr from "@/lib/data/fr/glossario";
import * as glossarioDe from "@/lib/data/de/glossario";
import { getLocalizedHref } from "@/i18n/links";
import {
ArrowRight,
@@ -42,8 +44,10 @@ export default function GlossarioPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const isEs = pathname?.startsWith("/es");
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const { categoriasGlossario } = isEs ? glossarioEs : isEn ? glossarioEn : glossarioPt;
const isFr = pathname?.startsWith("/fr");
const isDe = pathname?.startsWith("/de");
const currentLocale = isDe ? "de" : isFr ? "fr" : isEs ? "es" : isEn ? "en" : "pt";
const { categoriasGlossario } = isDe ? glossarioDe : isFr ? glossarioFr : isEs ? glossarioEs : isEn ? glossarioEn : glossarioPt;
const [busca, setBusca] = useState("");
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
@@ -112,12 +116,38 @@ export default function GlossarioPage() {
<PageHero
isEn={isEn}
isEs={isEs}
trail={[{ label: isEs ? "Glosario" : isEn ? "Glossary" : "Glossário" }]}
isFr={isFr}
isDe={isDe}
trail={[{ label: isDe ? "Glossar" : isFr ? "Glossaire" : isEs ? "Glosario" : isEn ? "Glossary" : "Glossário" }]}
icon={BookOpen}
label={isEs ? `${todosOsTermos.length} términos explicados` : isEn ? `${todosOsTermos.length} terms explained` : `${todosOsTermos.length} termos explicados`}
title={isEs ? "Glosario técnico de ESP32" : isEn ? "ESP32 Technical Glossary" : "Glossário técnico do ESP32"}
label={
isDe
? `${todosOsTermos.length} Begriffe erklärt`
: isFr
? `${todosOsTermos.length} termes expliqués`
: isEs
? `${todosOsTermos.length} términos explicados`
: isEn
? `${todosOsTermos.length} terms explained`
: `${todosOsTermos.length} termos explicados`
}
title={
isDe
? "Technisches Glossar für ESP32"
: isFr
? "Glossaire technique ESP32"
: isEs
? "Glosario técnico de ESP32"
: isEn
? "ESP32 Technical Glossary"
: "Glossário técnico do ESP32"
}
lede={
isEs
isDe
? "Präzise Definitionen, Hardware-Parameter und praktische Beispiele für jeden Begriff des Ökosystems – von ADC bis Secure Boot."
: isFr
? "Définitions précises, paramètres matériels et exemples pratiques pour chaque terme de l'écosystème, de l'ADC au Secure Boot."
: isEs
? "Definiciones directas, parámetros de hardware y un ejemplo práctico para cada término del ecosistema, desde ADC hasta Secure Boot."
: isEn
? "Direct definitions, hardware parameters, and practical examples for every ecosystem term, from ADC to Secure Boot."
@@ -125,11 +155,11 @@ export default function GlossarioPage() {
}
/>
<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={isDe ? "Suche und Filter" : isFr ? "Recherche et filtres" : isEs ? "Búsqueda y filtros" : isEn ? "Search and filters" : "Busca e filtros"}>
<div className="panel p-5 sm:p-7">
<div className="relative">
<label htmlFor="busca-glossario" className="sr-only">
{isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
{isDe ? "Nach Begriff, Abkürzung oder Konzept suchen" : isFr ? "Rechercher un terme, un sigle ou un concept" : isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
</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
@@ -138,7 +168,7 @@ export default function GlossarioPage() {
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder={isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
placeholder={isDe ? "Nach Begriff, Abkürzung oder Konzept suchen" : isFr ? "Rechercher un terme, un sigle ou un concept" : isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
autoComplete="off"
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
/>
@@ -146,14 +176,14 @@ export default function GlossarioPage() {
{busca && (
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
<X className="h-3.5 w-3.5" aria-hidden="true" />
{isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
{isDe ? "Löschen" : isFr ? "Effacer" : isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
</button>
)}
</div>
</div>
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="mr-1 text-sm text-muted">{isEs ? "Más buscados" : isEn ? "Popular searches" : "Mais buscados"}</span>
<span className="mr-1 text-sm text-muted">{isDe ? "Häufig gesucht" : isFr ? "Plus recherchés" : isEs ? "Más buscados" : isEn ? "Popular searches" : "Mais buscados"}</span>
{BUSCAS_POPULARES.map((rotulo) => (
<button
key={rotulo}
@@ -167,7 +197,7 @@ export default function GlossarioPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</h2>
<h2 className="text-sm font-semibold text-ink">{isDe ? "Kategorie" : isFr ? "Catégorie" : isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</h2>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
@@ -176,7 +206,7 @@ export default function GlossarioPage() {
className="chip-btn"
>
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
{isEs ? "Todas" : isEn ? "All" : "Todas"}
{isDe ? "Alle" : isFr ? "Toutes" : isEs ? "Todas" : isEn ? "All" : "Todas"}
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
</button>
{categoriasGlossario.map((cat) => {
@@ -199,10 +229,10 @@ export default function GlossarioPage() {
</div>
<div className="mt-6 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">{isEs ? "Orden alfabético" : isEn ? "Alphabetical order" : "Ordem alfabética"}</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label={isEs ? "Filtrar por letra inicial" : isEn ? "Filter by first letter" : "Filtrar pela letra inicial"}>
<h2 className="text-sm font-semibold text-ink">{isDe ? "Alphabetische Reihenfolge" : isFr ? "Ordre alphabétique" : isEs ? "Orden alfabético" : isEn ? "Alphabetical order" : "Ordem alfabética"}</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label={isDe ? "Nach Anfangsbuchstaben filtern" : isFr ? "Filtrer par lettre initiale" : isEs ? "Filtrar por letra inicial" : isEn ? "Filter by first letter" : "Filtrar pela letra inicial"}>
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
{isEs ? "Todas" : isEn ? "All" : "Todas"}
{isDe ? "Alle" : isFr ? "Toutes" : isEs ? "Todas" : isEn ? "All" : "Todas"}
</button>
{alfabeto.map(({ letra, temTermos }) => (
<button
@@ -224,13 +254,44 @@ export default function GlossarioPage() {
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
{total} {total === 1 ? (isEs ? "término" : isEn ? "term" : "termo") : (isEs ? "términos" : isEn ? "terms" : "termos")}
{temFiltroAtivo && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : " com os filtros atuais"}</span>}
{total}{" "}
{total === 1
? isDe
? "Begriff"
: isFr
? "terme"
: isEs
? "término"
: isEn
? "term"
: "termo"
: isDe
? "Begriffe"
: isFr
? "termes"
: isEs
? "términos"
: isEn
? "terms"
: "termos"}
{temFiltroAtivo && (
<span className="font-normal text-muted">
{isDe
? " mit den aktuellen Filtern"
: isFr
? " avec les filtres actuels"
: isEs
? " con los filtros actuales"
: isEn
? " with current filters"
: " com os filtros atuais"}
</span>
)}
</h2>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : "Redefinir filtros"}
{isDe ? "Filter zurücksetzen" : isFr ? "Réinitialiser les filtres" : isEs ? "Restablecer filtros" : isEn ? "Reset filters" : "Redefinir filtros"}
</button>
)}
</div>
@@ -239,10 +300,14 @@ export default function GlossarioPage() {
<div className="card mt-6 px-6 py-14 text-center">
<HelpCircle 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 términos" : isEn ? "No terms found" : "Nenhum termo encontrado"}
{isDe ? "Keine Begriffe gefunden" : isFr ? "Aucun terme trouvé" : isEs ? "No se encontraron términos" : isEn ? "No terms found" : "Nenhum termo encontrado"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEs
{isDe
? "Nichts entspricht Ihrer aktuellen Suche und den Filtern. Versuchen Sie eine kürzere Abkürzung oder wählen Sie alle Kategorien."
: isFr
? "Rien ne correspond à votre recherche et à vos filtres actuels. Essayez avec un sigle plus court ou revenez à toutes les catégories."
: isEs
? "Nada coincide con la búsqueda y los filtros actuales. Pruebe con una sigla más corta o vuelva a todas las categorías."
: isEn
? "Nothing matches your current search and filters. Try a shorter acronym or reset to all categories."
@@ -250,7 +315,7 @@ export default function GlossarioPage() {
</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
{isDe ? "Filter zurücksetzen" : isFr ? "Réinitialiser les filtres" : isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
</button>
</div>
) : (
@@ -267,7 +332,17 @@ export default function GlossarioPage() {
<button
type="button"
onClick={() => copiarLinkTermo(item.slug)}
aria-label={isEs ? `Copiar enlace para ${item.termo}` : isEn ? `Copy link for ${item.termo}` : `Copiar link para ${item.termo}`}
aria-label={
isDe
? `Link für ${item.termo} kopieren`
: isFr
? `Copier le lien pour ${item.termo}`
: isEs
? `Copiar enlace para ${item.termo}`
: isEn
? `Copy link for ${item.termo}`
: `Copiar link para ${item.termo}`
}
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"
>
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
@@ -282,7 +357,7 @@ export default function GlossarioPage() {
<div className="callout mt-5">
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
{isEs ? "En la práctica" : isEn ? "In practice" : "Na prática"}
{isDe ? "In der Praxis" : isFr ? "En pratique" : isEs ? "En la práctica" : isEn ? "In practice" : "Na prática"}
</p>
<p className="mt-1">
<TextoComLinks texto={item.exemplo} currentLocale={currentLocale} />
@@ -298,10 +373,14 @@ export default function GlossarioPage() {
<section className="shell pb-20 md:pb-28">
<div className="panel p-8 text-center sm:p-12">
<h2 className="section-title mx-auto max-w-2xl">
{isEs ? "Del concepto al chip adecuado." : isEn ? "From concept to the right chip." : "Do conceito ao chip certo."}
{isDe ? "Vom Konzept zum richtigen Chip." : isFr ? "Du concept à la bonne puce." : isEs ? "Del concepto al chip adecuado." : isEn ? "From concept to the right chip." : "Do conceito ao chip certo."}
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
{isEs
{isDe
? "Mit den Begriffen zur Hand vergleichen Sie die Serien Seite an Seite oder nutzen Sie die Auswahlhilfe, um das Modell für Ihr Projekt zu finden."
: isFr
? "Avec les termes sous la main, comparez les séries côte à côte ou répondez au sélecteur pour trouver le modèle adapté à votre projet."
: isEs
? "Con los términos a mano, compare las series lado a lado o responda al selector para encontrar el modelo que se adapta a su proyecto."
: isEn
? "With the terms in hand, compare series side by side or answer the selector to find the model that fits your project."
@@ -310,10 +389,10 @@ export default function GlossarioPage() {
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href={getLocalizedHref("/seletor", currentLocale)} className="btn btn-primary w-full sm:w-auto">
<Sliders className="h-4 w-4" aria-hidden="true" />
{isEs ? "Elegir mi chip" : isEn ? "Choose my chip" : "Escolher meu chip"}
{isDe ? "Meinen Chip wählen" : isFr ? "Choisir ma puce" : isEs ? "Elegir mi chip" : isEn ? "Choose my chip" : "Escolher meu chip"}
</Link>
<Link href={getLocalizedHref("/series", currentLocale)} className="btn btn-ghost w-full sm:w-auto">
{isEs ? "Explorar las series" : isEn ? "Explore series" : "Explorar as séries"}
{isDe ? "Serien erkunden" : isFr ? "Explorer les séries" : isEs ? "Explorar las series" : isEn ? "Explore series" : "Explorar as séries"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>