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

+57 -21
View File
@@ -8,6 +8,8 @@ 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 frameworksDataFr from "@/data/fr/frameworks.json";
import frameworksDataDe from "@/data/de/frameworks.json";
import { getLocalizedHref } from "@/i18n/links";
import { ArrowRight, ArrowUpRight, BookOpen, Box, Check, Code, Layers } from "lucide-react";
@@ -15,8 +17,10 @@ export default async function FrameworkDetail({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const isFr = locale === "fr";
const isDe = locale === "de";
const currentLocale = isEs ? "es" : isEn ? "en" : isFr ? "fr" : isDe ? "de" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : isFr ? frameworksDataFr : isDe ? frameworksDataDe : frameworksDataPt;
const framework = frameworksData[key];
if (!framework) {
@@ -38,7 +42,7 @@ export default async function FrameworkDetail({ params }) {
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.trilha([
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
{ nome: isEs ? "Inicio" : isEn ? "Home" : isFr ? "Accueil" : isDe ? "Startseite" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : isFr ? "/fr" : isDe ? "/de" : "/" },
{ nome: "Frameworks", caminho: getLocalizedHref("/frameworks", currentLocale) },
{ nome: framework.nome, caminho: getLocalizedHref(`/frameworks/${encodeURIComponent(key)}`, currentLocale) },
])),
@@ -51,7 +55,9 @@ export default async function FrameworkDetail({ params }) {
<PageHero
isEn={isEn}
isEs={isEs}
trail={[{ label: isEs ? "Frameworks y SDKs" : isEn ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
isFr={isFr}
isDe={isDe}
trail={[{ label: isEs ? "Frameworks y SDKs" : isEn ? "Frameworks & SDKs" : isFr ? "Frameworks et SDKs" : isDe ? "Frameworks & SDKs" : "Frameworks e SDKs", href: getLocalizedHref("/frameworks", currentLocale) }, { label: framework.nome }]}
icon={Layers}
label={framework.tipo}
title={framework.nome}
@@ -60,41 +66,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" />
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : "Documentação oficial"}
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : isFr ? "Documentation officielle" : isDe ? "Offizielle Dokumentation" : "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" />
{isEs ? "Repositorio en GitHub" : isEn ? "GitHub repository" : "Repositório no GitHub"}
{isEs ? "Repositorio en GitHub" : isEn ? "GitHub repository" : isFr ? "Dépôt sur GitHub" : isDe ? "GitHub-Repository" : "Repositório no GitHub"}
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
<section className="shell mt-10" aria-label={isEs ? "Resumen" : isEn ? "Summary" : "Resumo"}>
<section className="shell mt-10" aria-label={isEs ? "Resumen" : isEn ? "Summary" : isFr ? "Résumé" : isDe ? "Übersicht" : "Resumo"}>
<dl className="grid gap-3 md:grid-cols-3">
<div className="tile">
<dt className="text-xs font-medium text-muted">{isEs ? "Lenguaje" : isEn ? "Language" : "Linguagem"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Lenguaje" : isEn ? "Language" : isFr ? "Langage" : isDe ? "Sprache" : "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">{isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Categoría" : isEn ? "Category" : isFr ? "Catégorie" : isDe ? "Kategorie" : "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">{isEs ? "Objetivo principal" : isEn ? "Main purpose" : "Objetivo principal"}</dt>
<dt className="text-xs font-medium text-muted">{isEs ? "Objetivo principal" : isEn ? "Main purpose" : isFr ? "Objectif principal" : isDe ? "Hauptzweck" : "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={isEs ? "Funciones y aplicaciones" : 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" : isFr ? "Fonctionnalités et applications" : isDe ? "Features und Anwendungen" : "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">
{isEs ? "Funciones destacadas" : isEn ? "Key features" : "Recursos de destaque"}
{isEs ? "Funciones destacadas" : isEn ? "Key features" : isFr ? "Fonctionnalités clés" : isDe ? "Wichtigste Features" : "Recursos de destaque"}
</h2>
<ul className="mt-5 space-y-3">
{features.map((feature) => (
@@ -108,7 +114,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">
{isEs ? "Cuándo usar" : isEn ? "When to use" : "Quando usar"}
{isEs ? "Cuándo usar" : isEn ? "When to use" : isFr ? "Quand l'utiliser" : isDe ? "Einsatzbereiche" : "Quando usar"}
</h2>
<ul className="mt-5 space-y-2.5">
{useCases.map((useCase) => (
@@ -126,10 +132,10 @@ 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" />
{isEs ? "ESP-IDF 4.4 o superior" : 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" : isFr ? "ESP-IDF 4.4 ou supérieur" : isDe ? "ESP-IDF 4.4 oder höher" : "ESP-IDF 4.4 ou superior"}
</span>
<h2 className="section-title mt-4">
{isEs ? "Módulos con ESP Component Registry" : 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" : isFr ? "Modules avec l'ESP Component Registry" : isDe ? "Module mit der ESP Component Registry" : "Módulos com o ESP Component Registry"}
</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
{isEs ? (
@@ -144,6 +150,18 @@ export default async function FrameworkDetail({ params }) {
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> and the command{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> to download display drivers (LVGL), industrial stacks (Modbus), RainMaker, and more.
</>
) : isFr ? (
<>
Dans les projets ESP-IDF v4.4+ et v5.x, les bibliothèques n'ont plus besoin d'être ajoutées comme sous-modules Git. Utilisez le fichier{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> et la commande{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> pour télécharger des pilotes d'écran (LVGL), des piles industrielles (Modbus), RainMaker et bien plus encore.
</>
) : isDe ? (
<>
In Projekten mit ESP-IDF v4.4+ und v5.x müssen Bibliotheken nicht mehr als Git-Submodule eingebunden werden. Nutzen Sie{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> und den Befehl{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code>, um Display-Treiber (LVGL), Industrieprotokolle (Modbus), RainMaker und mehr herunterzuladen.
</>
) : (
<>
Nos projetos ESP-IDF v4.4+ e v5.x, as bibliotecas não precisam mais entrar como submódulos Git. Use o{" "}
@@ -154,7 +172,7 @@ export default async function FrameworkDetail({ params }) {
</p>
</div>
<Link href={getLocalizedHref("/componentes", currentLocale)} className="btn btn-primary shrink-0">
{isEs ? "Explorar componentes" : isEn ? "Explore components" : "Explorar componentes"}
{isEs ? "Explorar componentes" : isEn ? "Explore components" : isFr ? "Explorer les composants" : isDe ? "Komponenten erkunden" : "Explorar componentes"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -170,7 +188,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" />
{isEs ? "Ver todos los frameworks" : isEn ? "View all frameworks" : "Ver todos os frameworks"}
{isEs ? "Ver todos los frameworks" : isEn ? "View all frameworks" : isFr ? "Voir tous les frameworks" : isDe ? "Alle Frameworks ansehen" : "Ver todos os frameworks"}
</Link>
</div>
</main>
@@ -184,21 +202,27 @@ export async function generateMetadata({ params }) {
const { locale, key } = await params;
const isEn = locale === "en";
const isEs = locale === "es";
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : frameworksDataPt;
const isFr = locale === "fr";
const isDe = locale === "de";
const currentLocale = isEs ? "es" : isEn ? "en" : isFr ? "fr" : isDe ? "de" : "pt";
const frameworksData = isEs ? frameworksDataEs : isEn ? frameworksDataEn : isFr ? frameworksDataFr : isDe ? frameworksDataDe : frameworksDataPt;
const framework = frameworksData[key];
if (!framework) {
return { title: isEs ? "Framework no encontrado" : isEn ? "Framework not found" : "Framework não encontrado", robots: { index: false, follow: false } };
return { title: isEs ? "Framework no encontrado" : isEn ? "Framework not found" : isFr ? "Framework introuvable" : isDe ? "Framework nicht gefunden" : "Framework não encontrado", robots: { index: false, follow: false } };
}
return {
...paginaMeta({
titulo: isEs ? `${framework.nome} para ESP32 · Guía técnica` : isEn ? `${framework.nome} for ESP32 · Technical Guide` : `${framework.nome} para ESP32 · Guia Técnico`,
titulo: isEs ? `${framework.nome} para ESP32 · Guía técnica` : isEn ? `${framework.nome} for ESP32 · Technical Guide` : isFr ? `${framework.nome} pour ESP32 · Guide technique` : isDe ? `${framework.nome} für ESP32 · Technischer Leitfaden` : `${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.`
: isFr
? `${framework.nome_completo} : ${framework.descricao} Guide avec exemples pratiques de code et cas d'usage.`
: isDe
? `${framework.nome_completo}: ${framework.descricao} Leitfaden mit praktischen Codebeispielen und Anwendungsfällen.`
: `${framework.nome_completo}: ${framework.descricao} Guia com exemplos práticos de código e casos de uso.`,
caminho: `/frameworks/${encodeURIComponent(key)}`,
tipo: "article",
@@ -215,6 +239,18 @@ export async function generateMetadata({ params }) {
`${framework.nome} ESP32`, `program ESP32 with ${framework.nome}`,
"ESP32", "ESP32 framework",
]
: isFr
? [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `programmer ESP32 avec ${framework.nome}`,
"ESP32", "framework ESP32",
]
: isDe
? [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `ESP32 programmieren mit ${framework.nome}`,
"ESP32", "ESP32 Framework",
]
: [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `programar ESP32 com ${framework.nome}`,