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

+134 -59
View File
@@ -1,15 +1,12 @@
"use client";
import { useState, useMemo, useRef } from "react";
import { usePathname } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import {
categoriasDiagnostico,
sintomasRapidos,
errosCatalogo,
regrasDecodificadorLog,
} from "@/lib/diagnosticoData";
import * as diagnosticoPt from "@/lib/diagnosticoData";
import * as diagnosticoEn from "@/lib/data/en/diagnostico";
import {
Activity,
AlertTriangle,
@@ -38,19 +35,28 @@ const semEmoji = (texto) => texto.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+
const ICONES = { Zap, Cpu, Layers, Activity, Terminal, Wifi };
const EXEMPLOS_LOG = [
const EXEMPLOS_LOG_PT = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const EXEMPLOS_LOG_EN = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["TLS Handshake", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const GRAVIDADE = {
"Crítica": "text-danger",
"Critical": "text-danger",
Alta: "text-gold",
High: "text-gold",
};
const CHECKLISTS = [
const CHECKLISTS_PT = [
{
icon: Zap,
titulo: "Hardware e alimentação",
@@ -73,7 +79,36 @@ const CHECKLISTS = [
},
];
const CHECKLISTS_EN = [
{
icon: Zap,
titulo: "Hardware and Power Supply",
itens: [
"10 µF to 100 µF low-ESR capacitor on VCC, plus a 100 nF ceramic capacitor close to the chip.",
"LDO regulator sized for 600 mA to 1 A current spikes, with adequate thermal margin.",
"No external peripherals forcing logic levels on strapping pins during reset and boot.",
"10 µF or 1 µF capacitor on the EN (RST) pin for stable auto-reset flashing.",
],
},
{
icon: Layers,
titulo: "Firmware and FreeRTOS",
itens: [
"Every FreeRTOS task yields CPU time with vTaskDelay, queues, or semaphores.",
"Every ISR marked with IRAM_ATTR with zero blocking calls.",
"Avoid continuous heap reallocations using Arduino Strings in tight loops.",
"Clock synchronized via SNTP before attempting TLS/HTTPS handshakes.",
],
},
];
export default function DiagnosticoPage() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEn ? diagnosticoEn : diagnosticoPt;
const exemplosLog = isEn ? EXEMPLOS_LOG_EN : EXEMPLOS_LOG_PT;
const checklists = isEn ? CHECKLISTS_EN : CHECKLISTS_PT;
const [termoBusca, setTermoBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
const [sintomaAtivo, setSintomaAtivo] = useState(null);
@@ -101,14 +136,25 @@ export default function DiagnosticoPage() {
}
}
setResultadoLog({
titulo: "Padrão de erro não categorizado automaticamente",
tipo: "Desconhecido",
explicacao:
"Não identificamos uma assinatura clássica de Guru Meditation ou Brownout neste trecho. Verifique se a taxa de baud rate (ex: 115200) está correta e se o cabeçalho 'rst:' ou 'panic' está presente.",
acaoRecomendada:
"Procure por palavras-chave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' no console serial.",
});
setResultadoLog(
isEn
? {
titulo: "Error pattern not automatically classified",
tipo: "Unknown",
explicacao:
"We could not identify a classic Guru Meditation or Brownout signature in this snippet. Verify that the baud rate (e.g. 115200) is correct and whether the 'rst:' or 'panic' header is present.",
acaoRecomendada:
"Look for keywords such as 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog', or 'panic' in the serial output.",
}
: {
titulo: "Padrão de erro não categorizado automaticamente",
tipo: "Desconhecido",
explicacao:
"Não identificamos uma assinatura clássica de Guru Meditation ou Brownout neste trecho. Verifique se a taxa de baud rate (ex: 115200) está correta e se o cabeçalho 'rst:' ou 'panic' está presente.",
acaoRecomendada:
"Procure por palavras-chave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' no console serial.",
}
);
};
const copiarParaClipboard = (texto, id) => {
@@ -134,7 +180,7 @@ export default function DiagnosticoPage() {
return matchCategoria && matchSintoma && matchBusca;
});
}, [categoriaAtiva, sintomaAtivo, termoBusca]);
}, [categoriaAtiva, sintomaAtivo, termoBusca, errosCatalogo]);
const temFiltroAtivo =
termoBusca.trim() !== "" ||
@@ -156,7 +202,6 @@ export default function DiagnosticoPage() {
setLinguagemCodigo((prev) => ({ ...prev, [id]: lang }));
};
const totalFiltros = (termoBusca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (sintomaAtivo ? 1 : 0);
return (
@@ -165,11 +210,15 @@ export default function DiagnosticoPage() {
<main id="conteudo">
<PageHero
trail={[{ label: "Diagnóstico" }]}
trail={[{ label: isEn ? "Diagnostics" : "Diagnóstico" }]}
icon={AlertTriangle}
label="Depuração de firmware"
title="Diagnóstico de falhas do ESP32"
lede="Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
label={isEn ? "Firmware Debugging" : "Depuração de firmware"}
title={isEn ? "ESP32 Diagnostics and Crash Decoder" : "Diagnóstico de falhas do ESP32"}
lede={
isEn
? "Understand the root cause of Brownout, Guru Meditation, Watchdog, and Stack Overflow crashes, and apply engineering solutions instead of workarounds."
: "Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
}
>
<a
href="https://cienciaembarcada.com.br/publicacoes/esp32-5-erros-comuns-e-como-resolve-los-profissionalmente/"
@@ -177,9 +226,9 @@ export default function DiagnosticoPage() {
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong"
>
Baseado no artigo do Ciência Embarcada
{isEn ? "Based on article by Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
<span className="sr-only"> {isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
</a>
</PageHero>
@@ -187,12 +236,16 @@ export default function DiagnosticoPage() {
<div className="panel p-5 sm:p-8">
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="max-w-xl">
<h2 id="decodificador-titulo" className="section-title">Cole o log e veja a causa</h2>
<p className="mt-2 text-sm text-muted">Copie o trecho do monitor serial. O diagnóstico aparece na hora.</p>
<h2 id="decodificador-titulo" className="section-title">
{isEn ? "Paste the log to see the cause" : "Cole o log e veja a causa"}
</h2>
<p className="mt-2 text-sm text-muted">
{isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : "Copie o trecho do monitor serial. O diagnóstico aparece na hora."}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted">Testar com</span>
{EXEMPLOS_LOG.map(([rotulo, texto]) => (
<span className="text-sm text-muted">{isEn ? "Test with" : "Testar com"}</span>
{exemplosLog.map(([rotulo, texto]) => (
<button key={rotulo} type="button" onClick={() => analisarLog(texto)} className="chip-btn">
{rotulo}
</button>
@@ -201,20 +254,22 @@ export default function DiagnosticoPage() {
</div>
<div className="relative mt-5">
<label htmlFor="log-serial" className="sr-only">Log do monitor serial</label>
<label htmlFor="log-serial" className="sr-only">
{isEn ? "Serial monitor log" : "Log do monitor serial"}
</label>
<textarea
id="log-serial"
rows={4}
value={logInput}
onChange={(e) => analisarLog(e.target.value)}
placeholder="Cole o log do monitor serial aqui"
placeholder={isEn ? "Paste serial monitor log here" : "Cole o log do monitor serial aqui"}
spellCheck={false}
className="code-panel field !min-h-32 w-full resize-y !rounded-xl !border-white/10 !bg-[var(--code-bg)] p-4 font-mono text-sm !text-[#e9eef4] placeholder:!text-[#a6afbd]"
/>
{logInput && (
<button type="button" onClick={() => analisarLog("")} className="absolute right-3 top-3 inline-flex min-h-11 items-center gap-1 rounded-full border border-white/15 px-3 text-xs font-medium text-[#e9eef4] hover:bg-white/10">
<X className="h-3.5 w-3.5" aria-hidden="true" />
Limpar
{isEn ? "Clear" : "Limpar"}
</button>
)}
</div>
@@ -228,8 +283,8 @@ export default function DiagnosticoPage() {
<h3 className="font-semibold text-ink">{resultadoLog.titulo}</h3>
<span className="chip">{resultadoLog.tipo}</span>
</div>
<p><strong className="font-semibold text-ink">Causa provável:</strong> {resultadoLog.explicacao}</p>
<p><strong className="font-semibold text-ink">O que fazer:</strong> {resultadoLog.acaoRecomendada}</p>
<p><strong className="font-semibold text-ink">{isEn ? "Likely cause:" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
<p><strong className="font-semibold text-ink">{isEn ? "What to do:" : "O que fazer:"}</strong> {resultadoLog.acaoRecomendada}</p>
</div>
</div>
)}
@@ -238,11 +293,15 @@ export default function DiagnosticoPage() {
</section>
<section className="shell section pt-12 sm:pt-14" aria-labelledby="catalogo-titulo">
<h2 id="catalogo-titulo" className="section-title">Catálogo de falhas</h2>
<h2 id="catalogo-titulo" className="section-title">
{isEn ? "Fault catalog" : "Catálogo de falhas"}
</h2>
<div className="panel mt-6 p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-erro" className="sr-only">Buscar por erro, sintoma ou mensagem serial</label>
<label htmlFor="busca-erro" className="sr-only">
{isEn ? "Search by error, symptom, or serial message" : "Buscar por erro, sintoma ou mensagem serial"}
</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
id="busca-erro"
@@ -250,14 +309,14 @@ export default function DiagnosticoPage() {
type="search"
value={termoBusca}
onChange={(e) => setTermoBusca(e.target.value)}
placeholder="Buscar erro, sintoma ou mensagem"
placeholder={isEn ? "Search error, symptom, or message" : "Buscar erro, sintoma ou mensagem"}
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
</div>
<div className="mt-5">
<h3 className="text-sm font-semibold text-ink">Sintoma</h3>
<h3 className="text-sm font-semibold text-ink">{isEn ? "Symptom" : "Sintoma"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
{sintomasRapidos.map((sintoma) => (
<button key={sintoma.id} type="button" onClick={() => setSintomaAtivo(sintomaAtivo === sintoma.id ? null : sintoma.id)} aria-pressed={sintomaAtivo === sintoma.id} className="chip-btn">
@@ -268,11 +327,11 @@ export default function DiagnosticoPage() {
</div>
<div className="mt-5 border-t border-line pt-5">
<h3 className="text-sm font-semibold text-ink">Área</h3>
<h3 className="text-sm font-semibold text-ink">{isEn ? "Area" : "Área"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaAtiva("todos")} aria-pressed={categoriaAtiva === "todos"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
Todas
{isEn ? "All" : "Todas"}
<span className="tabular-nums opacity-80">{errosCatalogo.length}</span>
</button>
{categoriasDiagnostico.map((cat) => {
@@ -292,13 +351,13 @@ export default function DiagnosticoPage() {
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
<p role="status" aria-live="polite" className="text-sm font-semibold text-ink">
{errosFiltrados.length} {errosFiltrados.length === 1 ? "falha" : "falhas"}
{totalFiltros > 0 && <span className="font-normal text-muted"> com os filtros atuais</span>}
{errosFiltrados.length} {errosFiltrados.length === 1 ? (isEn ? "fault" : "falha") : (isEn ? "faults" : "falhas")}
{totalFiltros > 0 && <span className="font-normal text-muted">{isEn ? " with current filters" : " com os filtros atuais"}</span>}
</p>
{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" />
Redefinir filtros
{isEn ? "Reset filters" : "Redefinir filtros"}
</button>
)}
</div>
@@ -307,16 +366,22 @@ export default function DiagnosticoPage() {
{errosFiltrados.length === 0 ? (
<div className="card 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">Nenhuma falha com esses filtros</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou remova o sintoma e a área selecionados.</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
{isEn ? "No faults matching these filters" : "Nenhuma falha com esses filtros"}
</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
{isEn ? "Search for a more general term or remove selected symptom and area." : "Busque por um termo mais geral ou remova o sintoma e a área selecionados."}
</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
{isEn ? "Clear filters" : "Limpar filtros"}
</button>
</div>
) : (
errosFiltrados.map((erro) => {
const estaAberto = cardAberto === erro.id;
const lang = linguagemCodigo[erro.id] || "arduino";
const codigoAtual =
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || "# Exemplo não aplicável";
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || (isEn ? "# Example not applicable" : "# Exemplo não aplicável");
const catInfo = categoriasDiagnostico.find((c) => c.id === erro.categoria);
const IconeCat = ICONES[catInfo?.icone] ?? AlertTriangle;
@@ -336,7 +401,13 @@ export default function DiagnosticoPage() {
<IconeCat className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
{catInfo?.nome}
</span>
<span className={`chip ${GRAVIDADE[erro.gravidade] ?? "text-accent"}`}>Gravidade {erro.gravidade.toLowerCase()}</span>
<span className={`chip ${GRAVIDADE[erro.gravidade] ?? "text-accent"}`}>
{isEn ? (
<>Severity: {erro.gravidade.toLowerCase()}</>
) : (
<>Gravidade {erro.gravidade.toLowerCase()}</>
)}
</span>
</span>
<span className="mt-3 block font-display text-xl font-semibold tracking-[-0.03em] text-ink">{erro.titulo}</span>
<span className="mt-1.5 block text-sm font-normal leading-relaxed text-muted">{erro.resumo}</span>
@@ -353,11 +424,11 @@ export default function DiagnosticoPage() {
<div className="flex items-center justify-between gap-3">
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
Como aparece no monitor serial
{isEn ? "How it appears in serial monitor" : "Como aparece no monitor serial"}
</h4>
<button type="button" onClick={() => copiarParaClipboard(erro.logSerial, `log-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `log-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `log-${erro.id}` ? "Copiado" : "Copiar log"}
{copiadoId === `log-${erro.id}` ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy log" : "Copiar log")}
</button>
</div>
<div className="code-panel mt-1">
@@ -368,7 +439,7 @@ export default function DiagnosticoPage() {
<div>
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Cpu className="h-4 w-4 text-accent" aria-hidden="true" />
Causa raiz
{isEn ? "Root cause" : "Causa raiz"}
</h4>
<p className="callout mt-2">{erro.causaRaiz}</p>
</div>
@@ -377,17 +448,17 @@ export default function DiagnosticoPage() {
<div className="callout callout-danger">
<p className="flex items-center gap-2 font-semibold text-ink">
<XCircle className="h-4 w-4 shrink-0 text-danger" aria-hidden="true" />
O remendo comum
{isEn ? "The common workaround" : "O remendo comum"}
</p>
<p className="mt-2 font-medium text-ink">{erro.gambiarra.titulo}</p>
<p className="mt-1">{erro.gambiarra.descricao}</p>
<p className="mt-2"><strong className="font-semibold text-danger">Consequência:</strong> {erro.gambiarra.consequencia}</p>
<p className="mt-2"><strong className="font-semibold text-danger">{isEn ? "Consequence:" : "Consequência:"}</strong> {erro.gambiarra.consequencia}</p>
</div>
<div className="callout callout-ok">
<p className="flex items-center gap-2 font-semibold text-ink">
<CheckCircle2 className="h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
A solução de engenharia
{isEn ? "The engineering solution" : "A solução de engenharia"}
</p>
<p className="mt-2 font-medium text-ink">{erro.solucaoEngenharia.titulo}</p>
<ul className="mt-2 space-y-1.5">
@@ -403,7 +474,7 @@ export default function DiagnosticoPage() {
<div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div role="group" aria-label="Linguagem do exemplo" className="flex flex-wrap gap-2">
<div role="group" aria-label={isEn ? "Example language" : "Linguagem do exemplo"} className="flex flex-wrap gap-2">
{[
["arduino", "Arduino C++"],
["espidf", "ESP-IDF (C)"],
@@ -416,7 +487,7 @@ export default function DiagnosticoPage() {
</div>
<button type="button" onClick={() => copiarParaClipboard(codigoAtual, `code-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `code-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `code-${erro.id}` ? "Copiado" : "Copiar código"}
{copiadoId === `code-${erro.id}` ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy code" : "Copiar código")}
</button>
</div>
<div className="code-panel mt-3">
@@ -426,7 +497,7 @@ export default function DiagnosticoPage() {
<div className="callout callout-warn flex items-start gap-3">
<Wrench className="mt-0.5 h-4 w-4 shrink-0 text-gold" aria-hidden="true" />
<p><strong className="font-semibold text-ink">Dica de engenharia:</strong> {erro.dicaPro}</p>
<p><strong className="font-semibold text-ink">{isEn ? "Engineering tip:" : "Dica de engenharia:"}</strong> {erro.dicaPro}</p>
</div>
</div>
)}
@@ -438,10 +509,14 @@ export default function DiagnosticoPage() {
</section>
<section className="shell pb-20 md:pb-28" aria-labelledby="checklist-titulo">
<h2 id="checklist-titulo" className="section-title">Checklist antes de ir para campo</h2>
<p className="mt-3 max-w-2xl text-base text-muted">Itens essenciais para validar no hardware e no firmware antes de entregar o produto.</p>
<h2 id="checklist-titulo" className="section-title">
{isEn ? "Pre-deployment checklist" : "Checklist antes de ir para campo"}
</h2>
<p className="mt-3 max-w-2xl text-base text-muted">
{isEn ? "Essential items to validate on hardware and firmware before shipping a product." : "Itens essenciais para validar no hardware e no firmware antes de entregar o produto."}
</p>
<div className="mt-8 grid gap-4 md:grid-cols-2">
{CHECKLISTS.map(({ icon: Icon, titulo, itens }) => (
{checklists.map(({ icon: Icon, titulo, itens }) => (
<div key={titulo} className="card p-5 sm:p-7">
<h3 className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<span className="grid h-9 w-9 place-items-center rounded-full bg-accent-soft text-accent">