Files
ESPDocs/scripts/check-i18n.mjs
T
Lucas RayanandAntigravity c1bd2c7e11
CI / Lint e auditorias (push) Successful in 1m51s
CI / Trivy (dependências e segredos) (push) Successful in 15s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m41s
feat(i18n): English localization (phase 3)
- Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en
- Implement localized routing, slugs, and language negotiation with fallback to en
- Build dedicated search index for English with strict book exclusion
- Update components and pages to dynamically render localized strings and datasets
- Verify full structural parity and exact 1:1 match on Portuguese pages via comparator

Co-Authored-By: Antigravity <noreply@google.com>
2026-11-01 14:17:13 -03:00

440 lines
13 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 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";
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;
// Termos técnicos e nomes próprios permitidos permanecerem iguais ao pt
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",
"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",
"Flash",
"OTA",
"DMA",
"WDT",
"TWDT",
"IWDT",
"LDO",
"DCDC",
"LVGL",
"IDF",
"12",
"78",
"100%",
"1",
"2",
"3",
"4",
"5",
"6",
]);
// 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++;
}
/** 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 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 {
aviso(`Catálogo '${loc}' (${pathMessages}) ainda não criado (esperado para 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") {
const limpo = vPt.trim();
if (!TECHNICAL_ALLOWLIST.has(limpo) && !/^\d+$/.test(limpo)) {
iguaisSemAllowlist++;
}
}
}
}
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 (iguaisSemAllowlist > 0) {
aviso(`'${loc}': ${iguaisSemAllowlist} valor(es) idêntico(s) ao pt sem allowlist.`);
}
if (faltando === 0 && vazios === 0) {
sucesso(`Catálogo '${loc}' ativo possui paridade estrutural completa com 'pt'.`);
}
} else {
// Idioma inativo: valida integridade dos arquivos existentes sem quebrar se estiver incompleto
if (faltando > 0) aviso(`'${loc}' (inativo/em progresso): ${faltando} chave(s) ausente(s) em relação ao pt.`);
if (extras > 0) aviso(`'${loc}' (inativo): ${extras} chave(s) excedente(s) em relação ao pt.`);
if (vazios > 0) aviso(`'${loc}' (inativo): ${vazios} valor(es) vazio(s).`);
if (faltando === 0 && vazios === 0) {
sucesso(`Catálogo '${loc}' (inativo) possui paridade estrutural completa com 'pt'.`);
}
}
}
}
// 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;
}
// ---------------------------------------------------------------------------
// 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/<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.");
}
}
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();