- Localize skip-to-content accessibility link and breadcrumb navigation - Convert section layouts to dynamic generateMetadata with English titles, descriptions, and structured data - Eliminate mixed language expressions and translate remaining SoC connection pin warnings and descriptions - Translate component registry items, categories, tags, and IDF Component Manager guide - Add strict identical string verification to scripts/check-i18n.mjs with technical allowlist Co-Authored-By: Antigravity <noreply@google.com>
634 lines
21 KiB
JavaScript
634 lines
21 KiB
JavaScript
#!/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 estrita de strings idênticas ao pt sem allowlist (em messages, data e lib/data).
|
||
* 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";
|
||
import { ENABLED_LOCALES } from "../i18n/links.js";
|
||
|
||
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;
|
||
|
||
// Allowlist estrita: nomes próprios, marcas, siglas e termos técnicos aceitos como idênticos
|
||
const TECHNICAL_ALLOWLIST = new Set([
|
||
"ESPDocs",
|
||
"Lucas Rayan Guerra",
|
||
"Ciência Embarcada",
|
||
"Hardware",
|
||
"Component Registry",
|
||
"ESP Component Registry",
|
||
"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",
|
||
"ESP-ADF",
|
||
"ESP-SR",
|
||
"ESP-IoT-Solution",
|
||
"ESP-NOW & Mesh",
|
||
"ESP-Zigbee-SDK",
|
||
"ESP-WHO",
|
||
"ESP-NN",
|
||
"ESP-DSP",
|
||
"Rust on ESP32",
|
||
"Arduino Core",
|
||
"Xtensa",
|
||
"RISC-V",
|
||
"Wi-Fi",
|
||
"Bluetooth",
|
||
"BLE",
|
||
"Zigbee",
|
||
"Thread",
|
||
"USB",
|
||
"GPIO",
|
||
"SPI",
|
||
"I2C",
|
||
"I2S",
|
||
"UART",
|
||
"CAN",
|
||
"TWAI",
|
||
"ADC",
|
||
"DAC",
|
||
"PWM",
|
||
"JTAG",
|
||
"Gitea",
|
||
"GitHub",
|
||
"LinkedIn",
|
||
"MIT",
|
||
"DevKit",
|
||
"SoC",
|
||
"PSRAM",
|
||
"SRAM",
|
||
"ROM",
|
||
"Flash",
|
||
"OTA",
|
||
"DMA",
|
||
"WDT",
|
||
"TWDT",
|
||
"IWDT",
|
||
"LDO",
|
||
"DCDC",
|
||
"LVGL",
|
||
"IDF",
|
||
"TinyUSB",
|
||
"LittleFS",
|
||
"CBOR",
|
||
"RFC 8949",
|
||
"Cloud & AIoT Framework",
|
||
"Smart Home Framework",
|
||
"Smart Home & Mesh",
|
||
"{count} frameworks",
|
||
"C / C++",
|
||
"C++",
|
||
"Python",
|
||
"12",
|
||
"78",
|
||
"100%",
|
||
"1",
|
||
"2",
|
||
"3",
|
||
"4",
|
||
"5",
|
||
"6",
|
||
]);
|
||
|
||
function isTechnicalAllowlisted(val, path = "") {
|
||
if (typeof val !== "string") return true;
|
||
const s = val.trim();
|
||
if (TECHNICAL_ALLOWLIST.has(s)) return true;
|
||
// Campos que são metadados técnicos, URLs, identificadores ou configs de código
|
||
if (/(^|\.)(id|namespace|name|nome_completo|versao|categoria|oficial|popular|urlRegistry|urlRepo|exemploYaml|icone|cor|placa|site_oficial|repositorio_github|documentacao_oficial|datasheet|manual_tecnico|guia_de_programacao|gpio|uart|i2c|i2s|spi|adc|dac|touch|pino|funcoes|rotulo|chave|slug)(\[\d+\])*$/i.test(path)) {
|
||
return true;
|
||
}
|
||
if (s.startsWith("http://") || s.startsWith("https://") || s.startsWith("/") || s.startsWith("mailto:")) return true;
|
||
// Versões semver, tags e identificadores
|
||
if (/^\^?[0-9]+(\.[0-9]+)*([a-zA-Z0-9.-]*)$/.test(s)) return true;
|
||
// Emojis / Pictogramas / Símbolos Unicode
|
||
if (/^(\p{Extended_Pictographic}|\p{Emoji_Presentation}|\s)+$/u.test(s)) return true;
|
||
// Medidas, números, percentuais, frequências, consumos, intervalos
|
||
if (/^[-+]?\d+([.,]\d+)?(\s*[%]|(\s*(MHz|GHz|KB|MB|GB|µA|mA|A|V|mV|mW|W|dBm|°C|mm|ohms|kΩ|µF|nF)))?$/i.test(s)) return true;
|
||
if (/^[-+]?\d+([.,]\d+)?\s*(a|to|-)\s*[-+]?\d+([.,]\d+)?(\s*(MHz|GHz|KB|MB|GB|µA|mA|A|V|mV|mW|W|dBm|°C|mm|ohms|kΩ|µF|nF))?$/i.test(s)) return true;
|
||
if (/^\d+\s*mW\s*\([^)]*\)$/i.test(s)) return true;
|
||
// Hardware specs / contagem de periféricos: "1 (CAN 2.0, TWAI®)", "1 (Single-core)", "2 (Dual-core)"
|
||
if (/^\d+(\s*\(.*\))?(\s*(\+|\/|,)\s*\d+(\s*\(.*\))?)*$/i.test(s)) return true;
|
||
// Acrônimos de segurança / criptografia
|
||
if (/^(AES|SHA|RSA|ECDSA|XTS|HMAC|ECC|Digital Signature|V\d+(\s*\(.*\))?)[A-Za-z0-9_/ -]*$/i.test(s)) return true;
|
||
// Identificadores de módulos / produtos / encapsulamentos / pinos
|
||
if (/^[A-Za-z0-9_+-]+(\/[A-Za-z0-9_.-]+)*(\^[0-9.]+)?$/.test(s)) return true;
|
||
if (/^v\d+(\.\d+)*$/i.test(s)) return true;
|
||
if (/^QFN\d+(\s*\(\d+×\d+\s*mm\))?$/i.test(s)) return true;
|
||
// Lista de módulos / chips separados por vírgula ou +
|
||
if (/^([A-Za-z0-9_().-]+(,\s*|\s*\+\s*|\s+))+[A-Za-z0-9_().-]+$/.test(s)) return true;
|
||
// Cores hex
|
||
if (/^#?[0-9a-fA-F]{3,8}$/.test(s)) return true;
|
||
// Blocos de comandos e YAML
|
||
if (s.startsWith("dependencies:") || s.startsWith("idf.py") || s.startsWith("pip install")) return true;
|
||
// Sinais de hardware / categorias de pinos
|
||
if (/^(USB|JTAG|UART|SPI|I2C|I2S|GPIO|MIPI|Low Power|Strapping|Digital|Analog|Power)[A-Za-z0-9_.+/ -]*$/i.test(s)) return true;
|
||
// Nomes próprios de componentes / SDKs
|
||
if (/^(ESP|ESP32|LVGL|TinyUSB|FreeRTOS|Rust|Arduino)[A-Za-z0-9_()./ -]+$/i.test(s)) return true;
|
||
return false;
|
||
}
|
||
|
||
// Chaves específicas do livro (seção 7: apenas permitidas no pt)
|
||
const BOOK_KEYS = new Set([
|
||
"Header.book",
|
||
"Header.bookNew",
|
||
"Header.bookTitle",
|
||
"Header.bookSubtitle",
|
||
"Footer.bookNew",
|
||
"Footer.bookTitle",
|
||
"Footer.bookSubtitle",
|
||
"Footer.amazon",
|
||
"Footer.uiclap",
|
||
"Home.bookChapters",
|
||
"Home.bookTitle",
|
||
"Home.bookSubtitle",
|
||
"Home.bookNewBadge",
|
||
"Home.bookDescription",
|
||
"Home.bookParts",
|
||
"Home.bookChaptersCount",
|
||
"Home.buyDigital",
|
||
"Home.buyPrint",
|
||
"Home.fullRecord",
|
||
"Livro.title",
|
||
"Livro.subtitle",
|
||
"Livro.badge",
|
||
"Livro.description",
|
||
"Livro.buyDigital",
|
||
"Livro.buyPrint",
|
||
"Livro.chaptersTitle",
|
||
]);
|
||
|
||
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++;
|
||
}
|
||
|
||
function info(msg) {
|
||
console.log(` ${CORES.cinza}ℹ${CORES.reset} ${msg}`);
|
||
}
|
||
|
||
/** 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/4] 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 & Chaves por Idioma
|
||
// ---------------------------------------------------------------------------
|
||
function verificarOutrosIdiomas(ptMessages) {
|
||
console.log(`\n${CORES.negrito}${CORES.ciano}[2/4] 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 isEnabled = ENABLED_LOCALES.includes(loc);
|
||
|
||
const pathMessages = `messages/${loc}.json`;
|
||
if (!existsSync(pathMessages)) {
|
||
if (isEnabled) {
|
||
falha(`Catálogo '${loc}' (${pathMessages}) não encontrado para idioma ativo.`);
|
||
} else {
|
||
info(`Catálogo '${loc}' (${pathMessages}) ainda não criado (idioma inativo).`);
|
||
}
|
||
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)) {
|
||
// Chaves exclusivas do livro não são esperadas fora do pt (§7)
|
||
if (BOOK_KEYS.has(k) || k.startsWith("Livro.")) {
|
||
continue;
|
||
}
|
||
if (!(k in chavesLoc)) {
|
||
faltando++;
|
||
} else {
|
||
const vLoc = chavesLoc[k];
|
||
if (typeof vLoc === "string" && vLoc.trim() === "") {
|
||
vazios++;
|
||
} else if (vLoc === vPt && typeof vPt === "string") {
|
||
if (!isTechnicalAllowlisted(vPt, k)) {
|
||
iguaisSemAllowlist++;
|
||
falha(`'${loc}': chave '${k}' tem valor idêntico ao pt fora da allowlist: "${vPt}"`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const k of Object.keys(chavesLoc)) {
|
||
if (BOOK_KEYS.has(k) || k.startsWith("Livro.")) {
|
||
falha(`'${loc}': chave proibida do livro '${k}' presente no catálogo fora de pt.`);
|
||
} else if (!(k in chavesPt)) {
|
||
extras++;
|
||
}
|
||
}
|
||
|
||
if (isEnabled) {
|
||
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 (faltando === 0 && vazios === 0 && iguaisSemAllowlist === 0) {
|
||
sucesso(`Catálogo '${loc}' ativo possui paridade e tradução completas com 'pt'.`);
|
||
}
|
||
} else {
|
||
if (faltando === 0 && vazios === 0) {
|
||
sucesso(`Catálogo '${loc}' (inativo) possui paridade estrutural completa com 'pt'.`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 3. Verificação de Dados por Idioma (data/ e lib/data/)
|
||
// ---------------------------------------------------------------------------
|
||
function encontrarIdenticos(pt, en, path = "") {
|
||
const problemas = [];
|
||
if (typeof pt === "string" && typeof en === "string") {
|
||
if (pt === en && !isTechnicalAllowlisted(pt, path)) {
|
||
problemas.push({ path, value: pt });
|
||
}
|
||
} else if (Array.isArray(pt) && Array.isArray(en)) {
|
||
for (let i = 0; i < Math.min(pt.length, en.length); i++) {
|
||
problemas.push(...encontrarIdenticos(pt[i], en[i], `${path}[${i}]`));
|
||
}
|
||
} else if (typeof pt === "object" && pt !== null && typeof en === "object" && en !== null) {
|
||
for (const k of Object.keys(en)) {
|
||
if (k in pt) {
|
||
problemas.push(...encontrarIdenticos(pt[k], en[k], path ? `${path}.${k}` : k));
|
||
}
|
||
}
|
||
}
|
||
return problemas;
|
||
}
|
||
|
||
async function verificarDadosPorIdioma() {
|
||
console.log(`\n${CORES.negrito}${CORES.ciano}[3/4] Verificação de Dados (data/ e lib/data/) por Idioma${CORES.reset}`);
|
||
|
||
for (const loc of ENABLED_LOCALES) {
|
||
if (loc === "pt") continue;
|
||
|
||
// 1. data/<loc>/series.json vs data/pt/series.json
|
||
const ptSeriesPath = "data/pt/series.json";
|
||
const locSeriesPath = `data/${loc}/series.json`;
|
||
if (existsSync(locSeriesPath)) {
|
||
const ptSeries = JSON.parse(readFileSync(ptSeriesPath, "utf8"));
|
||
const locSeries = JSON.parse(readFileSync(locSeriesPath, "utf8"));
|
||
const problemas = encontrarIdenticos(ptSeries, locSeries, `data/${loc}/series.json`);
|
||
if (problemas.length > 0) {
|
||
falha(`'${loc}': ${problemas.length} valor(es) string em series.json idênticos ao pt fora da allowlist.`);
|
||
for (const p of problemas.slice(0, 5)) {
|
||
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
|
||
}
|
||
} else {
|
||
sucesso(`'${loc}': data/${loc}/series.json validado sem strings vazadas.`);
|
||
}
|
||
}
|
||
|
||
// 2. data/<loc>/frameworks.json vs data/pt/frameworks.json
|
||
const ptFwPath = "data/pt/frameworks.json";
|
||
const locFwPath = `data/${loc}/frameworks.json`;
|
||
if (existsSync(locFwPath)) {
|
||
const ptFw = JSON.parse(readFileSync(ptFwPath, "utf8"));
|
||
const locFw = JSON.parse(readFileSync(locFwPath, "utf8"));
|
||
const problemas = encontrarIdenticos(ptFw, locFw, `data/${loc}/frameworks.json`);
|
||
if (problemas.length > 0) {
|
||
falha(`'${loc}': ${problemas.length} valor(es) string em frameworks.json idênticos ao pt fora da allowlist.`);
|
||
for (const p of problemas.slice(0, 5)) {
|
||
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
|
||
}
|
||
} else {
|
||
sucesso(`'${loc}': data/${loc}/frameworks.json validado sem strings vazadas.`);
|
||
}
|
||
}
|
||
|
||
// 3. data/<loc>/conexoes/*.json vs public/conexoes/*.json
|
||
const locConexoesDir = `data/${loc}/conexoes`;
|
||
if (existsSync(locConexoesDir)) {
|
||
let totalConexoesProblemas = 0;
|
||
const arqs = readdirSync(locConexoesDir).filter((f) => f.endsWith(".json"));
|
||
for (const f of arqs) {
|
||
const ptPath = existsSync(`data/pt/conexoes/${f}`) ? `data/pt/conexoes/${f}` : `public/conexoes/${f}`;
|
||
if (existsSync(ptPath)) {
|
||
const pt = JSON.parse(readFileSync(ptPath, "utf8"));
|
||
const en = JSON.parse(readFileSync(join(locConexoesDir, f), "utf8"));
|
||
const problemas = encontrarIdenticos(pt, en, `conexoes/${f}`);
|
||
totalConexoesProblemas += problemas.length;
|
||
if (problemas.length > 0) {
|
||
for (const p of problemas.slice(0, 3)) {
|
||
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (totalConexoesProblemas > 0) {
|
||
falha(`'${loc}': ${totalConexoesProblemas} valor(es) em conexoes idênticos ao pt fora da allowlist.`);
|
||
} else {
|
||
sucesso(`'${loc}': ${arqs.length} arquivos de conexoes validados sem strings vazadas.`);
|
||
}
|
||
}
|
||
|
||
// 4. lib/data/<loc>/
|
||
try {
|
||
const ptComp = (await import("../lib/data/pt/componentes.js")).COMPONENTES_REGISTRY;
|
||
const locComp = (await import(`../lib/data/${loc}/componentes.js`)).COMPONENTES_REGISTRY;
|
||
const problemasComp = encontrarIdenticos(ptComp, locComp, `lib/data/${loc}/componentes.js`);
|
||
if (problemasComp.length > 0) {
|
||
falha(`'${loc}': ${problemasComp.length} string(s) em componentes.js idênticas ao pt fora da allowlist.`);
|
||
for (const p of problemasComp.slice(0, 3)) {
|
||
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
|
||
}
|
||
} else {
|
||
sucesso(`'${loc}': lib/data/${loc}/componentes.js validado sem strings vazadas.`);
|
||
}
|
||
} catch (e) {
|
||
falha(`Erro ao validar lib/data/${loc}/componentes.js: ${e.message}`);
|
||
}
|
||
|
||
try {
|
||
const ptDiag = (await import("../lib/data/pt/diagnostico.js")).ERROS_COMUNS;
|
||
const locDiag = (await import(`../lib/data/${loc}/diagnostico.js`)).ERROS_COMUNS;
|
||
const problemasDiag = encontrarIdenticos(ptDiag, locDiag, `lib/data/${loc}/diagnostico.js`);
|
||
if (problemasDiag.length > 0) {
|
||
falha(`'${loc}': ${problemasDiag.length} string(s) em diagnostico.js idênticas ao pt fora da allowlist.`);
|
||
} else {
|
||
sucesso(`'${loc}': lib/data/${loc}/diagnostico.js validado sem strings vazadas.`);
|
||
}
|
||
} catch (e) {
|
||
falha(`Erro ao validar lib/data/${loc}/diagnostico.js: ${e.message}`);
|
||
}
|
||
|
||
try {
|
||
const ptGloss = (await import("../lib/data/pt/glossario.js")).categoriasGlossario;
|
||
const locGloss = (await import(`../lib/data/${loc}/glossario.js`)).categoriasGlossario;
|
||
const problemasGloss = encontrarIdenticos(ptGloss, locGloss, `lib/data/${loc}/glossario.js`);
|
||
if (problemasGloss.length > 0) {
|
||
falha(`'${loc}': ${problemasGloss.length} string(s) em glossario.js idênticas ao pt fora da allowlist.`);
|
||
} else {
|
||
sucesso(`'${loc}': lib/data/${loc}/glossario.js validado sem strings vazadas.`);
|
||
}
|
||
} catch (e) {
|
||
falha(`Erro ao validar lib/data/${loc}/glossario.js: ${e.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Usos técnicos legítimos permitidos (seção 10: allowlist explícita)
|
||
const TECHNICAL_BOOK_ALLOWLIST = [
|
||
"https://esp-rs.github.io/book/",
|
||
];
|
||
|
||
function sanitizarTextoParaLivro(conteudo) {
|
||
let limpo = conteudo;
|
||
for (const termo of TECHNICAL_BOOK_ALLOWLIST) {
|
||
limpo = limpo.split(termo).join("");
|
||
}
|
||
return limpo;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 4. Verificação de Regras do Livro (§7 da spec: ausência fora do pt)
|
||
// ---------------------------------------------------------------------------
|
||
function verificarRegrasLivro() {
|
||
console.log(`\n${CORES.negrito}${CORES.ciano}[4/4] 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/<loc>.json
|
||
const pathMsg = `messages/${loc}.json`;
|
||
if (existsSync(pathMsg)) {
|
||
const conteudo = sanitizarTextoParaLivro(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/<loc>/
|
||
const dirData = `data/${loc}`;
|
||
if (existsSync(dirData)) {
|
||
const arquivos = readdirSync(dirData).filter((f) => f.endsWith(".json"));
|
||
for (const arq of arquivos) {
|
||
const conteudo = sanitizarTextoParaLivro(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++;
|
||
}
|
||
}
|
||
const dirConexoes = join(dirData, "conexoes");
|
||
if (existsSync(dirConexoes)) {
|
||
const arqsConexoes = readdirSync(dirConexoes).filter((f) => f.endsWith(".json"));
|
||
for (const arq of arqsConexoes) {
|
||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirConexoes, arq), "utf8"));
|
||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||
if (m) {
|
||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirConexoes}/${arq}'.`);
|
||
violacoes++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Checar lib/data/<loc>/
|
||
const dirLibData = `lib/data/${loc}`;
|
||
if (existsSync(dirLibData)) {
|
||
const arquivos = readdirSync(dirLibData).filter((f) => f.endsWith(".js"));
|
||
for (const arq of arquivos) {
|
||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirLibData, arq), "utf8"));
|
||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||
if (m) {
|
||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirLibData}/${arq}'.`);
|
||
violacoes++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Checar public/exemplos/<loc>/
|
||
const dirExemplos = `public/exemplos/${loc}`;
|
||
if (existsSync(dirExemplos)) {
|
||
const arquivos = readdirSync(dirExemplos);
|
||
for (const arq of arquivos) {
|
||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirExemplos, arq), "utf8"));
|
||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||
if (m) {
|
||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirExemplos}/${arq}'.`);
|
||
violacoes++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (violacoes === 0) {
|
||
sucesso("Nenhuma menção ao livro encontrada em catálogos ou dados fora do português.");
|
||
}
|
||
}
|
||
|
||
async 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);
|
||
await verificarDadosPorIdioma();
|
||
verificarRegrasLivro();
|
||
|
||
console.log(`\n${CORES.negrito}------------------------------------------------------${CORES.reset}`);
|
||
if (totalErros === 0 && totalAvisos === 0) {
|
||
console.log(`${CORES.verde}${CORES.negrito}🎉 TODAS AS VERIFICAÇÕES DE I18N PASSARAM! (0 erros, 0 avisos)${CORES.reset}\n`);
|
||
return 0;
|
||
} else if (totalErros === 0) {
|
||
console.log(`${CORES.amarelo}${CORES.negrito}✔ Verificações passaram com ${totalAvisos} aviso(s).${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 = await main();
|