- Add ENABLED_LOCALES configuration in i18n/links.js (currently ["pt"]) - Redirect inactive locale prefixes to /pt via 307 in middleware.js - Restrict root locale negotiation to active locales with fallback to pt - Hide LanguageSelector when only one locale is enabled - Generate sitemap, hreflang alternates, and og:locale only for active locales - Generate static SSG pages only for active locales in build - Adapt check:i18n and smoke-imagem.sh to validate active vs inactive locales Co-Authored-By: Antigravity <noreply@google.com>
345 lines
9.7 KiB
JavaScript
345 lines
9.7 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",
|
|
"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 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)) {
|
|
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 (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'.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 = 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 = 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();
|