feat(i18n): French and German localization (phase 5)
- 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:
1 parent
7b673aeb4b
commit
7fce46b9e1
104 files changed
+33369
-686
No files matched your search
@@ -4,16 +4,26 @@ 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
|
||||
? "Diagnóstico de errores comunes en ESP32: Guía profesional"
|
||||
: isEn
|
||||
? "Common ESP32 Errors & Diagnostics: Professional Guide"
|
||||
: isFr
|
||||
? "Diagnostic des erreurs courantes sur ESP32 : guide professionnel"
|
||||
: isDe
|
||||
? "Diagnose häufiger ESP32-Fehler: Professioneller Leitfaden"
|
||||
: "Diagnóstico de Erros Comuns no ESP32: Guia Profissional",
|
||||
descricao: isEs
|
||||
? "Guía técnica definitiva para identificar y solucionar Brownout, Guru Meditation, Task Watchdog, Stack Overflow, fugas de memoria y fallas de arranque en ESP32 (Arduino y ESP-IDF)."
|
||||
: isEn
|
||||
? "Definitive technical guide to identify and fix Brownout, Guru Meditation, Task Watchdog, Stack Overflow, memory leaks, and boot issues on ESP32 (Arduino & ESP-IDF)."
|
||||
: isFr
|
||||
? "Guide technique pour identifier et résoudre Brownout, Guru Meditation, Task Watchdog, Stack Overflow, fuites de mémoire et pannes de démarrage sur ESP32 (Arduino et ESP-IDF)."
|
||||
: isDe
|
||||
? "Technischer Leitfaden zur Behebung von Brownout, Guru Meditation, Task Watchdog, Stack Overflow, Speicherlecks und Boot-Problemen beim ESP32 (Arduino & ESP-IDF)."
|
||||
: "Guia técnico definitivo para identificar e resolver Brownout, Guru Meditation, Task Watchdog, Stack Overflow, vazamento de memória e falhas de boot no ESP32 (Arduino e ESP-IDF).",
|
||||
caminho: "/diagnostico",
|
||||
locale,
|
||||
@@ -39,6 +49,28 @@ export async function generateMetadata({ params }) {
|
||||
"ESP32 bootloop rebooting",
|
||||
"failed to connect to esp32",
|
||||
]
|
||||
: isFr
|
||||
? [
|
||||
"erreurs courantes ESP32",
|
||||
"brownout detector was triggered",
|
||||
"guru meditation error",
|
||||
"task watchdog got triggered",
|
||||
"stack canary watchpoint triggered",
|
||||
"comment déboguer ESP32",
|
||||
"ESP32 redémarre en boucle",
|
||||
"failed to connect to esp32",
|
||||
]
|
||||
: isDe
|
||||
? [
|
||||
"häufige ESP32-Fehler",
|
||||
"brownout detector was triggered",
|
||||
"guru meditation error",
|
||||
"task watchdog got triggered",
|
||||
"stack canary watchpoint triggered",
|
||||
"ESP32 debuggen",
|
||||
"ESP32 Bootloop Neustart",
|
||||
"failed to connect to esp32",
|
||||
]
|
||||
: [
|
||||
"erros comuns ESP32",
|
||||
"brownout detector was triggered",
|
||||
@@ -109,11 +141,52 @@ const faqsEs = [
|
||||
},
|
||||
];
|
||||
|
||||
const faqsFr = [
|
||||
{
|
||||
pergunta: "Quelle est la cause de l'erreur 'Brownout detector was triggered' sur l'ESP32 ?",
|
||||
resposta: "Elle survient lorsque la tension sur la broche 3V3 chute sous 2.8V ~ 2.43V en raison des pics rapides de courant de la radio Wi-Fi/BLE (dépassant 500mA), de câbles USB trop fins ou d'un régulateur LDO sous-dimensionné. La solution d'ingénierie consiste à ajouter des condensateurs Low-ESR de 10µF à 100µF près du VCC et à utiliser un régulateur LDO robuste de 600mA+.",
|
||||
},
|
||||
{
|
||||
pergunta: "Comment résoudre le 'Guru Meditation Error (LoadProhibited)' ?",
|
||||
resposta: "LoadProhibited indique une tentative d'accès à un pointeur nul (NULL) ou invalide en mémoire. La solution consiste à utiliser l'Espressif Exception Decoder ou addr2line pour identifier la ligne exacte du code et à appliquer des vérifications défensives telles que if (pointeur != NULL).",
|
||||
},
|
||||
{
|
||||
pergunta: "Pourquoi le Task Watchdog Timer (TWDT) se déclenche-t-il sous FreeRTOS ?",
|
||||
resposta: "Le TWDT se déclenche lorsqu'une tâche de priorité plus élevée monopolise le CPU dans une boucle bloquante (par exemple : while(1) sans vTaskDelay), empêchant la tâche IDLE de nourrir le watchdog du système d'exploitation.",
|
||||
},
|
||||
{
|
||||
pergunta: "Pourquoi les connexions HTTPS et TLS échouent-elles avec l'erreur -0x2700 sur l'ESP32 ?",
|
||||
resposta: "Au démarrage, l'horloge interne de l'ESP32 démarre à l'année 1970. Le validateur de certificats TLS considère le certificat du serveur invalide car situé dans le 'futur'. Synchroniser l'horloge via SNTP (configTime) avant la connexion résout le problème.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqsDe = [
|
||||
{
|
||||
pergunta: "Was verursacht den Fehler 'Brownout detector was triggered' beim ESP32?",
|
||||
resposta: "Er tritt auf, wenn die Spannung an der 3V3-Leitung durch schnelle Stromspitzen des Wi-Fi/BLE-Funkmoduls (über 500mA), dünne USB-Kabel oder einen unterdimensionierten LDO-Regler unter 2.8V ~ 2.43V abfällt. Die Lösung besteht darin, Low-ESR-Kondensatoren von 10µF bis 100µF nahe an VCC zu platzieren und robuste LDO-Regler mit 600mA+ einzusetzen.",
|
||||
},
|
||||
{
|
||||
pergunta: "Wie behebt man den 'Guru Meditation Error (LoadProhibited)'?",
|
||||
resposta: "LoadProhibited weist auf den Versuch hin, auf einen Null-Zeiger (NULL) oder eine ungültige Speicheradresse zuzugreifen. Die Lösung besteht darin, den Espressif Exception Decoder oder addr2line zu verwenden, um die genaue Quellcodezeile zu lokalisieren, und defensive Prüfungen wie if (ptr != NULL) einzufügen.",
|
||||
},
|
||||
{
|
||||
pergunta: "Warum löst der Task Watchdog Timer (TWDT) in FreeRTOS aus?",
|
||||
resposta: "Der TWDT löst aus, wenn ein Task mit höherer Priorität die CPU in einer blockierenden Schleife monopolisiert (z. B. while(1) ohne vTaskDelay) und so verhindert, dass der IDLE-Task den Watchdog des Betriebssystems bedient.",
|
||||
},
|
||||
{
|
||||
pergunta: "Warum schlagen HTTPS- und TLS-Verbindungen beim ESP32 mit Fehler -0x2700 fehl?",
|
||||
resposta: "Beim Start beginnt die interne Systemuhr des ESP32 im Jahr 1970. Die TLS-Zertifikatsprüfung stuft das Serverzertifikat als ungültig ein, da es in der fernen 'Zukunft' liegt. Eine Zeitsynchronisation über SNTP (configTime) vor dem Verbindungsaufbau behebt das Problem.",
|
||||
},
|
||||
];
|
||||
|
||||
export default async function DiagnosticoLayout({ children, params }) {
|
||||
const { locale } = await params;
|
||||
const isEn = locale === "en";
|
||||
const isEs = locale === "es";
|
||||
const faqs = isEs ? faqsEs : isEn ? faqsEn : faqsPt;
|
||||
const isFr = locale === "fr";
|
||||
const isDe = locale === "de";
|
||||
const faqs = isEs ? faqsEs : isEn ? faqsEn : isFr ? faqsFr : isDe ? faqsDe : faqsPt;
|
||||
const canonPath = isEs ? "/es/diagnostico" : isEn ? "/en/diagnostics" : isFr ? "/fr/diagnostic" : isDe ? "/de/diagnose" : "/diagnostico";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -122,8 +195,8 @@ export default async function DiagnosticoLayout({ children, params }) {
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: isEs ? "Inicio" : isEn ? "Home" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : "/" },
|
||||
{ nome: isEs ? "Diagnóstico y errores comunes" : isEn ? "Diagnostics & Common Errors" : "Diagnóstico & Erros Comuns", caminho: isEs ? "/es/diagnostico" : isEn ? "/en/diagnostics" : "/diagnostico" },
|
||||
{ nome: isEs ? "Inicio" : isEn ? "Home" : isFr ? "Accueil" : isDe ? "Startseite" : "Início", caminho: isEs ? "/es" : isEn ? "/en" : isFr ? "/fr" : isDe ? "/de" : "/" },
|
||||
{ nome: isEs ? "Diagnóstico y errores comunes" : isEn ? "Diagnostics & Common Errors" : isFr ? "Diagnostic et erreurs courantes" : isDe ? "Diagnose und häufige Fehler" : "Diagnóstico & Erros Comuns", caminho: canonPath },
|
||||
])
|
||||
),
|
||||
}}
|
||||
@@ -141,13 +214,17 @@ export default async function DiagnosticoLayout({ children, params }) {
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: isEs ? "Guía de diagnóstico y errores en ESP32" : isEn ? "ESP32 Error Diagnostics & Debugging Guide" : "Guia de Diagnóstico e Erros no ESP32",
|
||||
nome: isEs ? "Guía de diagnóstico y errores en ESP32" : isEn ? "ESP32 Error Diagnostics & Debugging Guide" : isFr ? "Guide de diagnostic et erreurs sur ESP32" : isDe ? "ESP32-Fehlerdiagnose und Debugging-Leitfaden" : "Guia de Diagnóstico e Erros no ESP32",
|
||||
descricao: isEs
|
||||
? "Herramienta y guía técnica de análisis de fallas, pánicos de CPU y decodificación de registros en ESP32."
|
||||
: isEn
|
||||
? "Technical guide and log decoder for troubleshooting CPU panics, crashes, and exceptions on ESP32."
|
||||
: isFr
|
||||
? "Outil et guide technique d'analyse des pannes, des paniques CPU et du décodage de logs sur ESP32."
|
||||
: isDe
|
||||
? "Werkzeug und technischer Leitfaden zur Fehleranalyse, Behebung von CPU-Panics und Dekodierung von Logs auf dem ESP32."
|
||||
: "Ferramenta e guia técnico de análise de falhas, pânicos de CPU e decodificação de logs no ESP32.",
|
||||
caminho: isEs ? "/es/diagnostico" : isEn ? "/en/diagnostics" : "/diagnostico",
|
||||
caminho: canonPath,
|
||||
locale,
|
||||
})
|
||||
),
|
||||
|
||||
@@ -8,6 +8,8 @@ import PageHero from "@/components/PageHero";
|
||||
import * as diagnosticoPt from "@/lib/diagnosticoData";
|
||||
import * as diagnosticoEn from "@/lib/data/en/diagnostico";
|
||||
import * as diagnosticoEs from "@/lib/data/es/diagnostico";
|
||||
import * as diagnosticoFr from "@/lib/data/fr/diagnostico";
|
||||
import * as diagnosticoDe from "@/lib/data/de/diagnostico";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
@@ -57,14 +59,34 @@ const EXEMPLOS_LOG_ES = [
|
||||
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
|
||||
];
|
||||
|
||||
const EXEMPLOS_LOG_FR = [
|
||||
["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_DE = [
|
||||
["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",
|
||||
"Critique": "text-danger",
|
||||
"Kritisch": "text-danger",
|
||||
Alta: "text-gold",
|
||||
High: "text-gold",
|
||||
"Élevée": "text-gold",
|
||||
Hoch: "text-gold",
|
||||
"Média": "text-accent",
|
||||
Medium: "text-accent",
|
||||
Media: "text-accent",
|
||||
Moyenne: "text-accent",
|
||||
Mittel: "text-accent",
|
||||
};
|
||||
|
||||
const CHECKLISTS_PT = [
|
||||
@@ -136,13 +158,61 @@ const CHECKLISTS_ES = [
|
||||
},
|
||||
];
|
||||
|
||||
const CHECKLISTS_FR = [
|
||||
{
|
||||
icon: Zap,
|
||||
titulo: "Matériel et alimentation",
|
||||
itens: [
|
||||
"Condensateur de 10 µF à 100 µF à faible ESR sur VCC, plus un céramique de 100 nF près de la puce.",
|
||||
"Régulateur LDO dimensionné pour des pointes de 600 mA à 1 A, avec marge thermique.",
|
||||
"Aucun périphérique ne forçant de niveau logique sur les broches de strapping pendant le boot.",
|
||||
"Condensateur de 10 µF ou 1 µF sur la broche EN (RST) pour un auto-reset stable au flashage.",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
titulo: "Firmware et FreeRTOS",
|
||||
itens: [
|
||||
"Chaque tâche FreeRTOS cède la CPU avec vTaskDelay, des files ou des sémaphores.",
|
||||
"Chaque ISR marquée avec IRAM_ATTR et exempte d'appels bloquants.",
|
||||
"Pas d'usage continu de String d'Arduino dans les boucles, afin d'éviter la fragmentation du tas.",
|
||||
"Horloge synchronisée par SNTP avant d'ouvrir des connexions TLS/HTTPS.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const CHECKLISTS_DE = [
|
||||
{
|
||||
icon: Zap,
|
||||
titulo: "Hardware und Spannungsversorgung",
|
||||
itens: [
|
||||
"10 µF bis 100 µF Low-ESR-Kondensator an VCC, plus ein 100-nF-Keramikkondensator nah am Chip.",
|
||||
"LDO-Regler ausgelegt für 600 mA bis 1 A Stromspitzen mit thermischer Reserve.",
|
||||
"Keine Peripheriegeräte, die beim Booten Logikpegel an Strapping-Pins erzwingen.",
|
||||
"10 µF- oder 1 µF-Kondensator am EN-Pin (RST) für zuverlässigen Auto-Reset beim Flashen.",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
titulo: "Firmware und FreeRTOS",
|
||||
itens: [
|
||||
"Jeder FreeRTOS-Task gibt CPU-Zeit mit vTaskDelay, Queues oder Semaphoren ab.",
|
||||
"Jede ISR mit IRAM_ATTR versehen und frei von blockierenden Aufrufen.",
|
||||
"Keine fortlaufende Verwendung von Arduino-Strings in Schleifen zur Vermeidung von Heap-Fragmentierung.",
|
||||
"Uhrzeit vor dem Aufbau von TLS/HTTPS-Verbindungen per SNTP synchronisiert.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function DiagnosticoPage() {
|
||||
const pathname = usePathname();
|
||||
const isEn = pathname?.startsWith("/en");
|
||||
const isEs = pathname?.startsWith("/es");
|
||||
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEs ? diagnosticoEs : isEn ? diagnosticoEn : diagnosticoPt;
|
||||
const exemplosLog = isEs ? EXEMPLOS_LOG_ES : isEn ? EXEMPLOS_LOG_EN : EXEMPLOS_LOG_PT;
|
||||
const checklists = isEs ? CHECKLISTS_ES : isEn ? CHECKLISTS_EN : CHECKLISTS_PT;
|
||||
const isFr = pathname?.startsWith("/fr");
|
||||
const isDe = pathname?.startsWith("/de");
|
||||
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEs ? diagnosticoEs : isEn ? diagnosticoEn : isFr ? diagnosticoFr : isDe ? diagnosticoDe : diagnosticoPt;
|
||||
const exemplosLog = isEs ? EXEMPLOS_LOG_ES : isEn ? EXEMPLOS_LOG_EN : isFr ? EXEMPLOS_LOG_FR : isDe ? EXEMPLOS_LOG_DE : EXEMPLOS_LOG_PT;
|
||||
const checklists = isEs ? CHECKLISTS_ES : isEn ? CHECKLISTS_EN : isFr ? CHECKLISTS_FR : isDe ? CHECKLISTS_DE : CHECKLISTS_PT;
|
||||
|
||||
const [termoBusca, setTermoBusca] = useState("");
|
||||
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
|
||||
@@ -190,6 +260,24 @@ export default function DiagnosticoPage() {
|
||||
acaoRecomendada:
|
||||
"Look for keywords such as 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog', or 'panic' in the serial output.",
|
||||
}
|
||||
: isFr
|
||||
? {
|
||||
titulo: "Modèle d'erreur non catégorisé automatiquement",
|
||||
tipo: "Inconnu",
|
||||
explicacao:
|
||||
"Nous n'avons pas identifié de signature classique de Guru Meditation ou Brownout dans cet extrait. Vérifiez que la vitesse en bauds (ex. : 115200) est correcte et si l'en-tête 'rst:' ou 'panic' est présent.",
|
||||
acaoRecomendada:
|
||||
"Recherchez des mots-clés tels que 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' dans la console série.",
|
||||
}
|
||||
: isDe
|
||||
? {
|
||||
titulo: "Fehlermuster nicht automatisch kategorisiert",
|
||||
tipo: "Unbekannt",
|
||||
explicacao:
|
||||
"Wir konnten in diesem Ausschnitt keine klassische Guru-Meditation- oder Brownout-Signatur erkennen. Prüfen Sie, ob die Baudrate (z. B. 115200) korrekt ist und ob der Header 'rst:' oder 'panic' vorhanden ist.",
|
||||
acaoRecomendada:
|
||||
"Suchen Sie in der seriellen Konsole nach Begriffen wie 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' oder 'panic'.",
|
||||
}
|
||||
: {
|
||||
titulo: "Padrão de erro não categorizado automaticamente",
|
||||
tipo: "Desconhecido",
|
||||
@@ -256,15 +344,21 @@ export default function DiagnosticoPage() {
|
||||
<PageHero
|
||||
isEn={isEn}
|
||||
isEs={isEs}
|
||||
trail={[{ label: isEs ? "Diagnóstico" : isEn ? "Diagnostics" : "Diagnóstico" }]}
|
||||
isFr={isFr}
|
||||
isDe={isDe}
|
||||
trail={[{ label: isEs ? "Diagnóstico" : isEn ? "Diagnostics" : isFr ? "Diagnostic" : isDe ? "Diagnose" : "Diagnóstico" }]}
|
||||
icon={AlertTriangle}
|
||||
label={isEs ? "Depuración de firmware" : isEn ? "Firmware Debugging" : "Depuração de firmware"}
|
||||
title={isEs ? "Diagnóstico de fallas del ESP32" : isEn ? "ESP32 Diagnostics and Crash Decoder" : "Diagnóstico de falhas do ESP32"}
|
||||
label={isEs ? "Depuración de firmware" : isEn ? "Firmware Debugging" : isFr ? "Débogage du firmware" : isDe ? "Firmware-Debugging" : "Depuração de firmware"}
|
||||
title={isEs ? "Diagnóstico de fallas del ESP32" : isEn ? "ESP32 Diagnostics and Crash Decoder" : isFr ? "Diagnostic des pannes de l'ESP32" : isDe ? "ESP32-Fehlerdiagnose und Crash-Decoder" : "Diagnóstico de falhas do ESP32"}
|
||||
lede={
|
||||
isEs
|
||||
? "Comprenda la causa raíz de las fallas por Brownout, Guru Meditation, Watchdog y Stack Overflow, y aplique la corrección de ingeniería en lugar de soluciones improvisadas."
|
||||
: isEn
|
||||
? "Understand the root cause of Brownout, Guru Meditation, Watchdog, and Stack Overflow crashes, and apply engineering solutions instead of workarounds."
|
||||
: isFr
|
||||
? "Comprenez la cause racine des pannes de Brownout, Guru Meditation, Watchdog et Stack Overflow, et appliquez la correction d'ingénierie plutôt qu'un bricolage."
|
||||
: isDe
|
||||
? "Verstehen Sie die Ursache von Brownout, Guru Meditation, Watchdog und Stack Overflow und wenden Sie saubere Ingenieurlösungen statt Notbehelfen an."
|
||||
: "Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
|
||||
}
|
||||
>
|
||||
@@ -274,9 +368,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"
|
||||
>
|
||||
{isEs ? "Basado en el artículo de Ciência Embarcada" : isEn ? "Based on article by Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
|
||||
{isEs ? "Basado en el artículo de Ciência Embarcada" : isEn ? "Based on article by Ciência Embarcada" : isFr ? "D'après l'article de Ciência Embarcada" : isDe ? "Basierend auf dem Artikel von Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : "(abre em nova aba)"}</span>
|
||||
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : isFr ? "(ouvre un nouvel onglet)" : isDe ? "(öffnet in neuem Tab)" : "(abre em nova aba)"}</span>
|
||||
</a>
|
||||
</PageHero>
|
||||
|
||||
@@ -285,14 +379,14 @@ export default function DiagnosticoPage() {
|
||||
<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">
|
||||
{isEs ? "Pegue el registro y descubra la causa" : isEn ? "Paste the log to see the cause" : "Cole o log e veja a causa"}
|
||||
{isEs ? "Pegue el registro y descubra la causa" : isEn ? "Paste the log to see the cause" : isFr ? "Collez le journal et découvrez la cause" : isDe ? "Log einfügen und Ursache ermitteln" : "Cole o log e veja a causa"}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-muted">
|
||||
{isEs ? "Copie el fragmento del monitor serie. El diagnóstico aparece al instante." : isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : "Copie o trecho do monitor serial. O diagnóstico aparece na hora."}
|
||||
{isEs ? "Copie el fragmento del monitor serie. El diagnóstico aparece al instante." : isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : isFr ? "Copiez l'extrait du moniteur série. Le diagnostic apparaît immédiatement." : isDe ? "Kopieren Sie den Ausschnitt aus dem seriellen Monitor. Die Diagnose erscheint sofort." : "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">{isEs ? "Probar con" : isEn ? "Test with" : "Testar com"}</span>
|
||||
<span className="text-sm text-muted">{isEs ? "Probar con" : isEn ? "Test with" : isFr ? "Tester avec" : isDe ? "Testen mit" : "Testar com"}</span>
|
||||
{exemplosLog.map(([rotulo, texto]) => (
|
||||
<button key={rotulo} type="button" onClick={() => analisarLog(texto)} className="chip-btn">
|
||||
{rotulo}
|
||||
@@ -303,21 +397,21 @@ export default function DiagnosticoPage() {
|
||||
|
||||
<div className="relative mt-5">
|
||||
<label htmlFor="log-serial" className="sr-only">
|
||||
{isEs ? "Registro del monitor serie" : isEn ? "Serial monitor log" : "Log do monitor serial"}
|
||||
{isEs ? "Registro del monitor serie" : isEn ? "Serial monitor log" : isFr ? "Journal du moniteur série" : isDe ? "Serieller Monitor-Log" : "Log do monitor serial"}
|
||||
</label>
|
||||
<textarea
|
||||
id="log-serial"
|
||||
rows={4}
|
||||
value={logInput}
|
||||
onChange={(e) => analisarLog(e.target.value)}
|
||||
placeholder={isEs ? "Pegue el registro del monitor serie aquí" : isEn ? "Paste serial monitor log here" : "Cole o log do monitor serial aqui"}
|
||||
placeholder={isEs ? "Pegue el registro del monitor serie aquí" : isEn ? "Paste serial monitor log here" : isFr ? "Collez le journal du moniteur série ici" : isDe ? "Seriellen Monitor-Log hier einfügen" : "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" />
|
||||
{isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
|
||||
{isEs ? "Limpiar" : isEn ? "Clear" : isFr ? "Effacer" : isDe ? "Löschen" : "Limpar"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -331,8 +425,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">{isEs ? "Causa probable:" : isEn ? "Likely cause:" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
|
||||
<p><strong className="font-semibold text-ink">{isEs ? "Qué hacer:" : isEn ? "What to do:" : "O que fazer:"}</strong> {resultadoLog.acaoRecomendada}</p>
|
||||
<p><strong className="font-semibold text-ink">{isEs ? "Causa probable:" : isEn ? "Likely cause:" : isFr ? "Cause probable :" : isDe ? "Wahrscheinliche Ursache :" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
|
||||
<p><strong className="font-semibold text-ink">{isEs ? "Qué hacer:" : isEn ? "What to do:" : isFr ? "Que faire :" : isDe ? "Was zu tun ist :" : "O que fazer:"}</strong> {resultadoLog.acaoRecomendada}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -342,13 +436,13 @@ export default function DiagnosticoPage() {
|
||||
|
||||
<section className="shell section pt-12 sm:pt-14" aria-labelledby="catalogo-titulo">
|
||||
<h2 id="catalogo-titulo" className="section-title">
|
||||
{isEs ? "Catálogo de fallas" : isEn ? "Fault catalog" : "Catálogo de falhas"}
|
||||
{isEs ? "Catálogo de fallas" : isEn ? "Fault catalog" : isFr ? "Catalogue de pannes" : isDe ? "Fehlerkatalog" : "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">
|
||||
{isEs ? "Buscar por error, síntoma o mensaje serie" : isEn ? "Search by error, symptom, or serial message" : "Buscar por erro, sintoma ou mensagem serial"}
|
||||
{isEs ? "Buscar por error, síntoma o mensaje serie" : isEn ? "Search by error, symptom, or serial message" : isFr ? "Rechercher par erreur, symptôme ou message série" : isDe ? "Nach Fehler, Symptom oder serieller Meldung suchen" : "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
|
||||
@@ -357,14 +451,14 @@ export default function DiagnosticoPage() {
|
||||
type="search"
|
||||
value={termoBusca}
|
||||
onChange={(e) => setTermoBusca(e.target.value)}
|
||||
placeholder={isEs ? "Buscar error, síntoma o mensaje" : isEn ? "Search error, symptom, or message" : "Buscar erro, sintoma ou mensagem"}
|
||||
placeholder={isEs ? "Buscar error, síntoma o mensaje" : isEn ? "Search error, symptom, or message" : isFr ? "Rechercher une erreur, un symptôme ou un message" : isDe ? "Fehler, Symptom oder Meldung suchen" : "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">{isEs ? "Síntoma" : isEn ? "Symptom" : "Sintoma"}</h3>
|
||||
<h3 className="text-sm font-semibold text-ink">{isEs ? "Síntoma" : isEn ? "Symptom" : isFr ? "Symptôme" : isDe ? "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">
|
||||
@@ -375,11 +469,11 @@ export default function DiagnosticoPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-line pt-5">
|
||||
<h3 className="text-sm font-semibold text-ink">{isEs ? "Área" : isEn ? "Area" : "Área"}</h3>
|
||||
<h3 className="text-sm font-semibold text-ink">{isEs ? "Área" : isEn ? "Area" : isFr ? "Domaine" : isDe ? "Bereich" : "Á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" />
|
||||
{isEs ? "Todas" : isEn ? "All" : "Todas"}
|
||||
{isEs ? "Todas" : isEn ? "All" : isFr ? "Tous" : isDe ? "Alle" : "Todas"}
|
||||
<span className="tabular-nums opacity-80">{errosCatalogo.length}</span>
|
||||
</button>
|
||||
{categoriasDiagnostico.map((cat) => {
|
||||
@@ -399,13 +493,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 ? (isEs ? "falla" : isEn ? "fault" : "falha") : (isEs ? "fallas" : isEn ? "faults" : "falhas")}
|
||||
{totalFiltros > 0 && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : " com os filtros atuais"}</span>}
|
||||
{errosFiltrados.length} {errosFiltrados.length === 1 ? (isEs ? "falla" : isEn ? "fault" : isFr ? "panne" : isDe ? "Fehler" : "falha") : (isEs ? "fallas" : isEn ? "faults" : isFr ? "pannes" : isDe ? "Fehler" : "falhas")}
|
||||
{totalFiltros > 0 && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : isFr ? " avec les filtres actuels" : isDe ? " mit aktuellen Filtern" : " 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" />
|
||||
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : "Redefinir filtros"}
|
||||
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : isFr ? "Réinitialiser les filtres" : isDe ? "Filter zurücksetzen" : "Redefinir filtros"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -415,13 +509,13 @@ export default function DiagnosticoPage() {
|
||||
<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">
|
||||
{isEs ? "Ninguna falla coincide con estos filtros" : isEn ? "No faults matching these filters" : "Nenhuma falha com esses filtros"}
|
||||
{isEs ? "Ninguna falla coincide con estos filtros" : isEn ? "No faults matching these filters" : isFr ? "Aucune panne ne correspond à ces filtres" : isDe ? "Keine Fehler entsprechen diesen Filtern" : "Nenhuma falha com esses filtros"}
|
||||
</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
||||
{isEs ? "Busque por un término más general o elimine el síntoma y el área seleccionados." : 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."}
|
||||
{isEs ? "Busque por un término más general o elimine el síntoma y el área seleccionados." : isEn ? "Search for a more general term or remove selected symptom and area." : isFr ? "Recherchez un terme plus général ou retirez le symptôme et le domaine sélectionnés." : isDe ? "Suchen Sie nach einem allgemeineren Begriff oder entfernen Sie gewähltes Symptom und Bereich." : "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">
|
||||
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
|
||||
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : isFr ? "Effacer les filtres" : isDe ? "Filter zurücksetzen" : "Limpar filtros"}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -429,7 +523,7 @@ export default function DiagnosticoPage() {
|
||||
const estaAberto = cardAberto === erro.id;
|
||||
const lang = linguagemCodigo[erro.id] || "arduino";
|
||||
const codigoAtual =
|
||||
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || (isEs ? "# Ejemplo no aplicable" : isEn ? "# Example not applicable" : "# Exemplo não aplicável");
|
||||
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || (isEs ? "# Ejemplo no aplicable" : isEn ? "# Example not applicable" : isFr ? "# Exemple non applicable" : isDe ? "# Beispiel nicht anwendbar" : "# Exemplo não aplicável");
|
||||
const catInfo = categoriasDiagnostico.find((c) => c.id === erro.categoria);
|
||||
const IconeCat = ICONES[catInfo?.icone] ?? AlertTriangle;
|
||||
|
||||
@@ -454,6 +548,10 @@ export default function DiagnosticoPage() {
|
||||
<>Severidad {erro.gravidade.toLowerCase()}</>
|
||||
) : isEn ? (
|
||||
<>Severity: {erro.gravidade.toLowerCase()}</>
|
||||
) : isFr ? (
|
||||
<>Gravité : {erro.gravidade.toLowerCase()}</>
|
||||
) : isDe ? (
|
||||
<>Schweregrad: {erro.gravidade.toLowerCase()}</>
|
||||
) : (
|
||||
<>Gravidade {erro.gravidade.toLowerCase()}</>
|
||||
)}
|
||||
@@ -474,11 +572,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" />
|
||||
{isEs ? "Cómo aparece en el monitor serie" : isEn ? "How it appears in serial monitor" : "Como aparece no monitor serial"}
|
||||
{isEs ? "Cómo aparece en el monitor serie" : isEn ? "How it appears in serial monitor" : isFr ? "Apparence dans le moniteur série" : isDe ? "Erscheinungsbild im seriellen 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}` ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar registro" : isEn ? "Copy log" : "Copiar log")}
|
||||
{copiadoId === `log-${erro.id}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar registro" : isEn ? "Copy log" : isFr ? "Copier le log" : isDe ? "Log kopieren" : "Copiar log")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="code-panel mt-1">
|
||||
@@ -489,7 +587,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" />
|
||||
{isEs ? "Causa raíz" : isEn ? "Root cause" : "Causa raiz"}
|
||||
{isEs ? "Causa raíz" : isEn ? "Root cause" : isFr ? "Cause racine" : isDe ? "Ursache" : "Causa raiz"}
|
||||
</h4>
|
||||
<p className="callout mt-2">{erro.causaRaiz}</p>
|
||||
</div>
|
||||
@@ -498,17 +596,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" />
|
||||
{isEs ? "La solución improvisada común" : isEn ? "The common workaround" : "O remendo comum"}
|
||||
{isEs ? "La solución improvisada común" : isEn ? "The common workaround" : isFr ? "Le bricolage courant" : isDe ? "Der verbreitete Notbehelf" : "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">{isEs ? "Consecuencia:" : isEn ? "Consequence:" : "Consequência:"}</strong> {erro.gambiarra.consequencia}</p>
|
||||
<p className="mt-2"><strong className="font-semibold text-danger">{isEs ? "Consecuencia:" : isEn ? "Consequence:" : isFr ? "Conséquence :" : isDe ? "Folge :" : "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" />
|
||||
{isEs ? "La solución de ingeniería" : isEn ? "The engineering solution" : "A solução de engenharia"}
|
||||
{isEs ? "La solución de ingeniería" : isEn ? "The engineering solution" : isFr ? "La solution d'ingénierie" : isDe ? "Die Ingenieurlösung" : "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">
|
||||
@@ -524,7 +622,7 @@ export default function DiagnosticoPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div role="group" aria-label={isEs ? "Lenguaje del ejemplo" : isEn ? "Example language" : "Linguagem do exemplo"} className="flex flex-wrap gap-2">
|
||||
<div role="group" aria-label={isEs ? "Lenguaje del ejemplo" : isEn ? "Example language" : isFr ? "Langage de l'exemple" : isDe ? "Sprache des Beispiels" : "Linguagem do exemplo"} className="flex flex-wrap gap-2">
|
||||
{[
|
||||
["arduino", "Arduino C++"],
|
||||
["espidf", "ESP-IDF (C)"],
|
||||
@@ -537,7 +635,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}` ? (isEs ? "Copiado" : isEn ? "Copied" : "Copiado") : (isEs ? "Copiar código" : isEn ? "Copy code" : "Copiar código")}
|
||||
{copiadoId === `code-${erro.id}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar código" : isEn ? "Copy code" : isFr ? "Copier le code" : isDe ? "Code kopieren" : "Copiar código")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="code-panel mt-3">
|
||||
@@ -547,7 +645,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">{isEs ? "Consejo de ingeniería:" : isEn ? "Engineering tip:" : "Dica de engenharia:"}</strong> {erro.dicaPro}</p>
|
||||
<p><strong className="font-semibold text-ink">{isEs ? "Consejo de ingeniería:" : isEn ? "Engineering tip:" : isFr ? "Conseil d'ingénierie :" : isDe ? "Praxistipp :" : "Dica de engenharia:"}</strong> {erro.dicaPro}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -560,10 +658,10 @@ export default function DiagnosticoPage() {
|
||||
|
||||
<section className="shell pb-20 md:pb-28" aria-labelledby="checklist-titulo">
|
||||
<h2 id="checklist-titulo" className="section-title">
|
||||
{isEs ? "Lista de verificación previa a campo" : isEn ? "Pre-deployment checklist" : "Checklist antes de ir para campo"}
|
||||
{isEs ? "Lista de verificación previa a campo" : isEn ? "Pre-deployment checklist" : isFr ? "Checklist avant déploiement" : isDe ? "Checkliste vor dem Feldeinsatz" : "Checklist antes de ir para campo"}
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-base text-muted">
|
||||
{isEs ? "Elementos esenciales para validar en hardware y firmware antes de entregar el producto." : 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."}
|
||||
{isEs ? "Elementos esenciales para validar en hardware y firmware antes de entregar el producto." : isEn ? "Essential items to validate on hardware and firmware before shipping a product." : isFr ? "Éléments essentiels à valider sur le matériel et le firmware avant de livrer le produit." : isDe ? "Wesentliche Punkte zur Prüfung von Hardware und Firmware vor der Auslieferung." : "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 }) => (
|
||||
|
||||
Reference in new issue
Block a user