Files
ESPDocs/scripts/check-i18n.mjs
T
Lucas RayanandClaude Sonnet 5.5 fe1802f818
CI / Lint e auditorias (push) Successful in 48s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 3m41s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 16s
Release / Cria a release no Gitea (push) Successful in 6s
fix: book canonical, fr/de pin warnings, pin grouping per locale, SEO checks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 23:58:11 -03:00

688 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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",
"Buscar...",
"Catálogo de placas",
"Diagnóstico",
"Índice completo",
"Periféricos",
"Modern C++",
"Núcleos de CPU (Dual-Core vs Single-Core)",
"Bluetooth Classic (BR/EDR)",
"Ethernet MAC 10/100 Mbps",
"Séries ESP32",
]);
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*°C)?\s*(a|to|-)\s*[-+]?\d+(\/\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, 50)) {
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, 50)) {
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, 10)) {
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.`);
for (const p of problemasGloss) {
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
}
} 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.");
}
}
// ---------------------------------------------------------------------------
// 5. Português ou inglês dentro dos dados de outros idiomas
// ---------------------------------------------------------------------------
// Valores que começam com número ("4 (SPI0/1 para Flash...)") passam pela allowlist
// técnica; esta varredura pega o texto em português que sobra dentro deles.
const PALAVRAS_SO_PORTUGUESAS = /\b(até|canais|uso geral|conforme a carga|podem ser|saída|resolução|integrada no|encapsulamento|memória|não|única|isócronos|proximidade e|dividida em|a partir da|e mais \d+|ativa|ativo|baixo consumo|correspondentes|velocidade|com (GCM|LP|cache))\b/i;
// Frases de aviso em inglês que não podem aparecer nos dados dos outros idiomas (en é a origem delas).
const FRASES_EM_INGLES = /\b(Do not leave|Must not be|Not recommended|Recommended Voltage|For chips (with|without)|Dedicated (to|Chip|Hold|analog)|Analog input pin|Multifunction pin|Floating by default|Used to control|Voltage level must|Low-power|Primary Strapping|This pin does not)\b/;
function percorrer(valor, caminho, aoAchar, ingles = true) {
if (typeof valor === "string") {
if (PALAVRAS_SO_PORTUGUESAS.test(valor)) aoAchar(caminho, valor, "português");
else if (ingles && FRASES_EM_INGLES.test(valor)) aoAchar(caminho, valor, "inglês");
} else if (Array.isArray(valor)) {
valor.forEach((v, i) => percorrer(v, `${caminho}[${i}]`, aoAchar, ingles));
} else if (valor && typeof valor === "object") {
for (const [k, v] of Object.entries(valor)) percorrer(v, `${caminho}.${k}`, aoAchar, ingles);
}
}
function verificarPortuguesNosDados() {
console.log(`\n${CORES.negrito}${CORES.ciano}[5/5] Português dentro dos dados de outros idiomas${CORES.reset}`);
let achados = 0;
for (const loc of LOCALES) {
if (loc === "pt") continue;
const arquivos = [`data/${loc}/series.json`, `data/${loc}/frameworks.json`];
const dirCon = `data/${loc}/conexoes`;
if (existsSync(dirCon)) for (const a of readdirSync(dirCon)) arquivos.push(join(dirCon, a));
for (const arq of arquivos) {
if (!existsSync(arq)) continue;
percorrer(JSON.parse(readFileSync(arq, "utf8")), arq, (caminho, valor, lingua) => {
if (achados < 10) falha(`'${loc}': texto em ${lingua} em ${caminho}: "${valor.slice(0, 80)}"`);
achados++;
}, loc !== "en");
}
}
if (achados === 0) sucesso("Nenhum texto em português ou inglês solto nos dados dos outros idiomas.");
else if (achados > 10) falha(`... e mais ${achados - 10} ocorrência(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();
verificarPortuguesNosDados();
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();