feat(i18n): English localization (phase 3)
CI / Lint e auditorias (push) Successful in 1m51s
CI / Trivy (dependências e segredos) (push) Successful in 15s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m41s

- Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en
- Implement localized routing, slugs, and language negotiation with fallback to en
- Build dedicated search index for English with strict book exclusion
- Update components and pages to dynamically render localized strings and datasets
- Verify full structural parity and exact 1:1 match on Portuguese pages via comparator

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 14:17:13 -03:00
1 parent 22b1f26aed
commit c1bd2c7e11
61 files changed
+18182 -1225

No files matched your search

+141 -40
View File
@@ -2,8 +2,10 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer";
import BookCover from "@/components/BookCover";
import Link from "next/link";
import seriesData from "@/public/series.json";
import frameworksData from "@/public/frameworks.json";
import seriesDataPt from "@/public/series.json";
import frameworksDataPt from "@/public/frameworks.json";
import seriesDataEn from "@/data/en/series.json";
import frameworksDataEn from "@/data/en/frameworks.json";
import { SITE } from "@/lib/seo";
import { getLocalizedHref } from "@/i18n/links";
import {
@@ -20,7 +22,7 @@ import {
const FEATURED = ["ESP32", "ESP32-S3", "ESP32-C6", "ESP32-C3", "ESP32-P4", "ESP32-H2"];
const TOOLS = [
const TOOLS_PT = [
{
href: "/seletor",
title: "Seletor inteligente",
@@ -65,7 +67,52 @@ const TOOLS = [
},
];
const ERRORS = [
const TOOLS_EN = [
{
href: "/seletor",
title: "Smart Selector",
desc: "Answer eight questions about your project and find recommended chips, backed by technical rationale.",
icon: SlidersHorizontal,
action: "Find my chip",
},
{
href: "/comparacao",
title: "Series Comparator",
desc: "Place up to four models side by side and compare cores, radio, memory, peripherals, and security.",
icon: Scale,
action: "Compare now",
},
{
href: "/diagnostico",
title: "Fault Diagnostics",
desc: "Root causes and fixes for brownout, bootloop, Guru Meditation, watchdog, and flash errors.",
icon: AlertTriangle,
action: "Open diagnostics",
},
{
href: "/componentes",
title: "Component Registry",
desc: "Drivers, libraries and BSPs from the official Espressif registry, with ready-to-use manifests.",
icon: Box,
action: "Explore components",
},
{
href: "/frameworks",
title: "Frameworks & SDKs",
desc: "ESP-IDF, Arduino, Rust, Matter, MicroPython and the tools built around the silicon.",
icon: Terminal,
action: "View frameworks",
},
{
href: "/catalogo",
title: "Board Catalog",
desc: "Real DevKits and modules, with USB interfaces, integrated sensors and direct store links.",
icon: Cpu,
action: "View boards",
},
];
const ERRORS_PT = [
{
code: "Brownout detector was triggered",
cause: "Queda de tensão na inicialização do rádio.",
@@ -88,11 +135,39 @@ const ERRORS = [
},
];
const ERRORS_EN = [
{
code: "Brownout detector was triggered",
cause: "Voltage sag upon radio startup.",
fix: "10–100 µF capacitor close to chip and a 5 V supply with sufficient current.",
},
{
code: "Guru Meditation Error",
cause: "Invalid pointer, stack overflow, or improper ISR usage.",
fix: "Inspect backtrace with addr2line and properly size FreeRTOS task stacks.",
},
{
code: "ADC2 indisponível com Wi-Fi",
cause: "Internal radio controller reserves ADC2 while active.",
fix: "Use ADC1 pins for analog sensors operating alongside Wi-Fi.",
},
{
code: "Flash Read err / bootloader",
cause: "Incorrect flash voltage/mode or strapping pin pulled during boot.",
fix: "Check 3.3 V rail, boot pull-ups, and flash configuration in esptool.",
},
];
export default async function Home({ params }) {
const { locale } = await params;
const isPt = locale === "pt";
const getLinkHref = (href) => getLocalizedHref(`/${locale}${href}`, locale);
const seriesData = isPt ? seriesDataPt : seriesDataEn;
const frameworksData = isPt ? frameworksDataPt : frameworksDataEn;
const tools = isPt ? TOOLS_PT : TOOLS_EN;
const errors = isPt ? ERRORS_PT : ERRORS_EN;
const seriesCount = Object.keys(seriesData).length;
const frameworkCount = Object.keys(frameworksData).length;
const featured = FEATURED.map((key) => [key, seriesData[key]]).filter(([, serie]) => Boolean(serie));
@@ -106,39 +181,58 @@ export default async function Home({ params }) {
<div className="mx-auto max-w-4xl text-center">
<span className="chip chip-accent">
<Cpu className="h-3.5 w-3.5" aria-hidden="true" />
Documentação aberta do ESP32
{isPt ? "Documentação aberta do ESP32" : "Open ESP32 documentation"}
</span>
<h1 className="display mt-6 text-[clamp(2.9rem,7.4vw,5.4rem)]">
O ESP32,
<br className="sm:hidden" /> sem ruído.
{isPt ? (
<>
O ESP32,
<br className="sm:hidden" /> sem ruído.
</>
) : (
<>
The ESP32,
<br className="sm:hidden" /> without noise.
</>
)}
</h1>
<p className="lede mx-auto mt-6 max-w-2xl">
Especificações, pinagem, comparações e diagnóstico em uma leitura só.
Menos tradução de datasheet, mais tempo com o protótipo na bancada.
{isPt
? "Especificações, pinagem, comparações e diagnóstico em uma leitura só. Menos tradução de datasheet, mais tempo com o protótipo na bancada."
: "Specifications, pinouts, comparisons and diagnostics in a single read. Less datasheet translation, more workbench time."}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href={getLinkHref("/seletor")} className="btn btn-primary w-full sm:w-auto">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Escolher meu chip
{isPt ? "Escolher meu chip" : "Choose my chip"}
</Link>
<Link href={getLinkHref("/series")} className="btn btn-ghost w-full sm:w-auto">
Explorar as {seriesCount} séries
{isPt ? (
<>Explorar as {seriesCount} séries</>
) : (
<>Explore the {seriesCount} series</>
)}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<p className="mt-7 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-muted">
<span><strong className="font-semibold text-ink">{seriesCount}</strong> séries</span>
<span><strong className="font-semibold text-ink">{seriesCount}</strong> {isPt ? "séries" : "series"}</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span>
{isPt && (
{isPt ? (
<>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">78</strong> capítulos no livro</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span>100% em português</span>
</>
) : (
<>
<span aria-hidden="true" className="text-line-strong">·</span>
<span>100% in English</span>
</>
)}
</p>
</div>
@@ -146,11 +240,13 @@ export default async function Home({ params }) {
<div className="panel mt-12 overflow-hidden sm:mt-16">
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-4 sm:px-7">
<div>
<h2 className="text-sm font-semibold text-ink">Séries em destaque</h2>
<p className="mt-0.5 text-xs text-muted">O essencial de cada família, antes de abrir o datasheet.</p>
<h2 className="text-sm font-semibold text-ink">{isPt ? "Séries em destaque" : "Featured series"}</h2>
<p className="mt-0.5 text-xs text-muted">
{isPt ? "O essencial de cada família, antes de abrir o datasheet." : "The essentials of each family before opening the datasheet."}
</p>
</div>
<Link href="/series" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
Índice completo
<Link href={getLinkHref("/series")} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
{isPt ? "Índice completo" : "Full index"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
@@ -159,7 +255,7 @@ export default async function Home({ params }) {
{featured.map(([key, serie]) => (
<Link
key={key}
href={`/series/${key}`}
href={getLinkHref(`/series/${key}`)}
className="group flex min-w-0 flex-col bg-surface p-5 transition-colors duration-300 hover:bg-surface-2 sm:p-6"
>
<div className="flex items-start justify-between gap-3">
@@ -175,7 +271,7 @@ export default async function Home({ params }) {
<div className="mt-5 flex items-center justify-between gap-3 border-t border-line pt-3.5">
<span className="truncate font-mono text-[0.6875rem] text-ink-soft">{serie.frequencia}</span>
<span className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent">
Ficha
{isPt ? "Ficha" : "Spec sheet"}
<ArrowUpRight className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</div>
@@ -256,20 +352,22 @@ export default async function Home({ params }) {
<section className="shell section" aria-labelledby="ferramentas-titulo">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow">Ferramentas</p>
<p className="eyebrow">{isPt ? "Ferramentas" : "Tools"}</p>
<h2 id="ferramentas-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
Comece pelo problema.
{isPt ? "Comece pelo problema." : "Start with the problem."}
</h2>
<p className="lede mt-4">
Cada ferramenta resolve uma etapa concreta do projeto, sem cadastro e sem rodeio.
{isPt
? "Cada ferramenta resolve uma etapa concreta do projeto, sem cadastro e sem rodeio."
: "Each tool addresses a concrete development phase, with no sign-ups and zero fluff."}
</p>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{TOOLS.map((tool) => {
{tools.map((tool) => {
const Icon = tool.icon;
return (
<Link key={tool.href} href={tool.href} className="card card-interactive group flex flex-col p-5 sm:p-6">
<Link key={tool.href} href={getLinkHref(tool.href)} className="card card-interactive group flex flex-col p-5 sm:p-6">
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
@@ -288,16 +386,18 @@ export default async function Home({ params }) {
<section className="shell section pt-0" aria-labelledby="diagnostico-titulo">
<div className="grid items-start gap-10 lg:grid-cols-[0.78fr_1.22fr] lg:gap-16">
<div className="lg:sticky lg:top-28">
<p className="eyebrow">Diagnóstico</p>
<p className="eyebrow">{isPt ? "Diagnóstico" : "Diagnostics"}</p>
<h2 id="diagnostico-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
O primeiro boot não precisa virar noite.
{isPt ? "O primeiro boot não precisa virar noite." : "First boot doesn't have to take all night."}
</h2>
<p className="lede mt-4">
Os erros mais comuns têm causa conhecida. O guia parte da mensagem no terminal e chega ao ajuste de hardware ou firmware.
{isPt
? "Os erros mais comuns têm causa conhecida. O guia parte da mensagem no terminal e chega ao ajuste de hardware ou firmware."
: "Common errors have known causes. This guide starts with the serial terminal log and leads to the hardware or firmware adjustment."}
</p>
<Link href="/diagnostico" className="btn btn-accent mt-7">
<Link href={getLinkHref("/diagnostico")} className="btn btn-accent mt-7">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
Abrir os 30+ diagnósticos
{isPt ? "Abrir os 30+ diagnósticos" : "Open the 30+ diagnostics"}
</Link>
</div>
@@ -307,11 +407,11 @@ export default async function Home({ params }) {
<Terminal className="h-4 w-4" aria-hidden="true" />
boot.log
</span>
<span className="text-xs text-[#a6afbd]">4 exemplos de 30+</span>
<span className="text-xs text-[#a6afbd]">{isPt ? "4 exemplos de 30+" : "4 examples of 30+"}</span>
</div>
<div className="divide-y divide-white/10">
{ERRORS.map((item) => (
<Link key={item.code} href="/diagnostico" className="group grid gap-2 px-5 py-5 transition-colors duration-300 hover:bg-white/[0.035] sm:px-6">
{errors.map((item) => (
<Link key={item.code} href={getLinkHref("/diagnostico")} className="group grid gap-2 px-5 py-5 transition-colors duration-300 hover:bg-white/[0.035] sm:px-6">
<span className="break-words font-mono text-[0.75rem] font-medium leading-relaxed text-[#ffb39b]">{item.code}</span>
<span className="text-sm font-medium text-[#f4f6f8]">{item.cause}</span>
<span className="text-xs leading-relaxed text-[#a6afbd]">{item.fix}</span>
@@ -324,21 +424,22 @@ export default async function Home({ params }) {
<section className="shell pb-20 pt-4 md:pb-28">
<div className="mx-auto max-w-3xl text-center">
<p className="eyebrow">Curadoria independente</p>
<p className="eyebrow">{isPt ? "Curadoria independente" : "Independent curation"}</p>
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
Feito para consultar no meio do projeto.
{isPt ? "Feito para consultar no meio do projeto." : "Built for quick lookup mid-project."}
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
O ESPDocs organiza dados públicos da Espressif e aponta sempre para a fonte oficial.
Para decisões críticas, o datasheet e o Technical Reference Manual continuam sendo a palavra final.
{isPt
? "O ESPDocs organiza dados públicos da Espressif e aponta sempre para a fonte oficial. Para decisões críticas, o datasheet e o Technical Reference Manual continuam sendo a palavra final."
: "ESPDocs organizes public Espressif data and points directly to official sources. For critical engineering decisions, the datasheet and Technical Reference Manual remain the definitive authority."}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/glossario" className="btn btn-ghost w-full sm:w-auto">
<Link href={getLinkHref("/glossario")} className="btn btn-ghost w-full sm:w-auto">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Glossário
{isPt ? "Glossário" : "Glossary"}
</Link>
<Link href="/sobre" className="btn btn-primary w-full sm:w-auto">
Sobre o projeto
<Link href={getLinkHref("/sobre")} className="btn btn-primary w-full sm:w-auto">
{isPt ? "Sobre o projeto" : "About the project"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>