#!/usr/bin/env node /** * ESPDocs - Script de Verificação Automatizada de Internacionalização (check:i18n) * * Implementa os critérios de aceite da seção 10 (itens 1 e 2) da spec docs/specs/i18n.md: * 1. Consistência estrutural de catálogos e dados (chaves idênticas ao pt). * 2. Ausência de valores vazios. * 3. Detecção de strings idênticas ao pt sem estarem na allowlist técnica. * 4. Verificação estrita de ausência de referências ao livro fora do pt (seção 7). * * Uso: npm run check:i18n */ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; const CORES = { reset: "\x1b[0m", verde: "\x1b[32m", vermelho: "\x1b[31m", amarelo: "\x1b[33m", azul: "\x1b[34m", ciano: "\x1b[36m", negrito: "\x1b[1m", cinza: "\x1b[90m", }; const LOCALES = ["pt", "en", "es", "fr", "de", "zh-CN"]; const FORBIDDEN_BOOK_TERMS = /\b(livro|libro|livre|buch|书|book|isbn|amazon|uiclap|capítulo|capitulo|chapter|chapitre|kapitel|章节)\b/i; // Termos técnicos e nomes próprios permitidos permanecerem iguais ao pt const TECHNICAL_ALLOWLIST = new Set([ "ESPDocs", "Lucas Rayan Guerra", "Ciência Embarcada", "ESP32", "ESP32-S2", "ESP32-S3", "ESP32-S31", "ESP32-C2", "ESP32-C3", "ESP32-C5", "ESP32-C6", "ESP32-C61", "ESP32-P4", "ESP32-H2", "ESP32-H4", "ESP-IDF", "Arduino", "MicroPython", "FreeRTOS", "Rust", "Matter", "ESP-RainMaker", "ESP-Matter", "Xtensa", "RISC-V", "Wi-Fi", "Bluetooth", "BLE", "Zigbee", "Thread", "USB", "GPIO", "SPI", "I2C", "I2S", "UART", "CAN", "TWAI", "ADC", "DAC", "PWM", "JTAG", "Gitea", "GitHub", "MIT", "DevKit", "SoC", "PSRAM", "SRAM", "Flash", "OTA", "DMA", "WDT", "TWDT", "IWDT", "LDO", "DCDC", "LVGL", "IDF", "12", "78", "100%", "1", "2", "3", "4", "5", "6", ]); let totalErros = 0; let totalAvisos = 0; function sucesso(msg) { console.log(` ${CORES.verde}✔${CORES.reset} ${msg}`); } function falha(msg) { console.log(` ${CORES.vermelho}✖${CORES.reset} ${msg}`); totalErros++; } function aviso(msg) { console.log(` ${CORES.amarelo}⚠${CORES.reset} ${msg}`); totalAvisos++; } /** Achata um objeto aninhado em chaves dot-notation */ function achatarObjeto(obj, prefixo = "") { const resultado = {}; for (const [k, v] of Object.entries(obj)) { const chave = prefixo ? `${prefixo}.${k}` : k; if (v !== null && typeof v === "object" && !Array.isArray(v)) { Object.assign(resultado, achatarObjeto(v, chave)); } else { resultado[chave] = v; } } return resultado; } // --------------------------------------------------------------------------- // 1. Verificação da Base Canônica em Português (pt) // --------------------------------------------------------------------------- function verificarBasePt() { console.log(`\n${CORES.negrito}${CORES.ciano}[1/3] Verificação da Fonte Canônica (pt)${CORES.reset}`); // messages/pt.json const messagesPtPath = "messages/pt.json"; if (!existsSync(messagesPtPath)) { falha(`Arquivo de mensagens '${messagesPtPath}' não encontrado.`); return null; } let ptMessages; try { ptMessages = JSON.parse(readFileSync(messagesPtPath, "utf8")); sucesso(`Catálogo '${messagesPtPath}' carregado com sucesso (${Object.keys(ptMessages).length} namespaces).`); } catch (e) { falha(`Erro ao processar JSON em '${messagesPtPath}': ${e.message}`); return null; } // data/pt/ const dataFiles = ["series.json", "frameworks.json"]; for (const f of dataFiles) { const p = join("data/pt", f); if (existsSync(p)) { try { JSON.parse(readFileSync(p, "utf8")); sucesso(`Arquivo de dados '${p}' validado com sucesso.`); } catch (e) { falha(`Erro em '${p}': ${e.message}`); } } else { falha(`Arquivo esperado '${p}' não encontrado.`); } } // data/pt/conexoes const conexoesDir = "data/pt/conexoes"; if (existsSync(conexoesDir)) { const arquivos = readdirSync(conexoesDir).filter((f) => f.endsWith(".json")); if (arquivos.length >= 11) { sucesso(`Diretório '${conexoesDir}' contém ${arquivos.length} arquivos de conexões validados.`); } else { aviso(`Diretório '${conexoesDir}' contém apenas ${arquivos.length} arquivos.`); } } else { falha(`Diretório '${conexoesDir}' não encontrado.`); } // lib/data/pt/ const libDataFiles = ["glossario.js", "diagnostico.js", "componentes.js", "topicos.js"]; for (const f of libDataFiles) { const p = join("lib/data/pt", f); if (existsSync(p)) { sucesso(`Módulo de dados '${p}' verificado.`); } else { falha(`Módulo esperado '${p}' não encontrado.`); } } return ptMessages; } // --------------------------------------------------------------------------- // 2. Verificação de Catálogos e Dados de Outros Idiomas (quando presentes) // --------------------------------------------------------------------------- function verificarOutrosIdiomas(ptMessages) { console.log(`\n${CORES.negrito}${CORES.ciano}[2/3] Verificação de Catálogos & Chaves por Idioma${CORES.reset}`); if (!ptMessages) return; const chavesPt = achatarObjeto(ptMessages); const totalChavesPt = Object.keys(chavesPt).length; sucesso(`Catálogo 'pt' contém ${totalChavesPt} chaves de tradução ativas.`); for (const loc of LOCALES) { if (loc === "pt") continue; const pathMessages = `messages/${loc}.json`; if (!existsSync(pathMessages)) { aviso(`Catálogo '${loc}' (${pathMessages}) ainda não criado (esperado para sua fase de tradução).`); continue; } let locMessages; try { locMessages = JSON.parse(readFileSync(pathMessages, "utf8")); } catch (e) { falha(`Erro de parsing em '${pathMessages}': ${e.message}`); continue; } const chavesLoc = achatarObjeto(locMessages); let faltando = 0; let extras = 0; let vazios = 0; let iguaisSemAllowlist = 0; for (const [k, vPt] of Object.entries(chavesPt)) { if (!(k in chavesLoc)) { faltando++; } else { const vLoc = chavesLoc[k]; if (typeof vLoc === "string" && vLoc.trim() === "") { vazios++; } else if (vLoc === vPt && typeof vPt === "string") { const limpo = vPt.trim(); if (!TECHNICAL_ALLOWLIST.has(limpo) && !/^\d+$/.test(limpo)) { iguaisSemAllowlist++; } } } } for (const k of Object.keys(chavesLoc)) { if (!(k in chavesPt)) extras++; } if (faltando > 0) falha(`'${loc}': ${faltando} chave(s) ausente(s) em relação ao pt.`); if (extras > 0) aviso(`'${loc}': ${extras} chave(s) excedente(s) em relação ao pt.`); if (vazios > 0) falha(`'${loc}': ${vazios} valor(es) vazio(s).`); if (iguaisSemAllowlist > 0) { aviso(`'${loc}': ${iguaisSemAllowlist} valor(es) idêntico(s) ao pt sem allowlist.`); } if (faltando === 0 && vazios === 0) { sucesso(`Catálogo '${loc}' possui paridade estrutural completa com 'pt'.`); } } } // --------------------------------------------------------------------------- // 3. Verificação de Regras do Livro (§7 da spec: ausência fora do pt) // --------------------------------------------------------------------------- function verificarRegrasLivro() { console.log(`\n${CORES.negrito}${CORES.ciano}[3/3] Verificação Estrita de Referências ao Livro (Seção 7)${CORES.reset}`); let violacoes = 0; for (const loc of LOCALES) { if (loc === "pt") continue; // Checar messages/.json const pathMsg = `messages/${loc}.json`; if (existsSync(pathMsg)) { const conteudo = readFileSync(pathMsg, "utf8"); const m = conteudo.match(FORBIDDEN_BOOK_TERMS); if (m) { falha(`Termo proibido do livro '${m[0]}' encontrado em '${pathMsg}'.`); violacoes++; } } // Checar data// const dirData = `data/${loc}`; if (existsSync(dirData)) { const arquivos = readdirSync(dirData).filter((f) => f.endsWith(".json")); for (const arq of arquivos) { const conteudo = readFileSync(join(dirData, arq), "utf8"); const m = conteudo.match(FORBIDDEN_BOOK_TERMS); if (m) { falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirData}/${arq}'.`); violacoes++; } } } } if (violacoes === 0) { sucesso("Nenhuma menção ao livro encontrada em catálogos ou dados fora do português."); } } function main() { console.log(`${CORES.negrito}${CORES.azul}======================================================${CORES.reset}`); console.log(`${CORES.negrito}${CORES.azul} ESPDocs · Verificação de Internacionalização (i18n) ${CORES.reset}`); console.log(`${CORES.negrito}${CORES.azul}======================================================${CORES.reset}`); const ptMessages = verificarBasePt(); verificarOutrosIdiomas(ptMessages); verificarRegrasLivro(); console.log(`\n${CORES.negrito}------------------------------------------------------${CORES.reset}`); if (totalErros === 0) { console.log(`${CORES.verde}${CORES.negrito}🎉 TODAS AS VERIFICAÇÕES DE I18N PASSARAM! (0 erros)${CORES.reset}\n`); return 0; } else { console.log(`${CORES.vermelho}${CORES.negrito}✖ Foram encontrados ${totalErros} erro(s) de i18n que bloqueiam o aceite.${CORES.reset}\n`); return 1; } } process.exitCode = main();