fix(i18n): eliminate portuguese text leaks across english routes
CI / Lint e auditorias (push) Successful in 50s
CI / Trivy (dependências e segredos) (push) Successful in 10s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m31s

- 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>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 17:17:33 -03:00
1 parent c1bd2c7e11
commit 47c55515bb
35 files changed
+2353 -1874

No files matched your search

+218 -24
View File
@@ -5,7 +5,7 @@
* 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.
* 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
@@ -29,7 +29,7 @@ const CORES = {
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
// Allowlist estrita: nomes próprios, marcas, siglas e termos técnicos aceitos como idênticos
const TECHNICAL_ALLOWLIST = new Set([
"ESPDocs",
"Lucas Rayan Guerra",
@@ -57,6 +57,16 @@ const TECHNICAL_ALLOWLIST = new Set([
"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",
@@ -84,6 +94,7 @@ const TECHNICAL_ALLOWLIST = new Set([
"SoC",
"PSRAM",
"SRAM",
"ROM",
"Flash",
"OTA",
"DMA",
@@ -94,6 +105,17 @@ const TECHNICAL_ALLOWLIST = new Set([
"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%",
@@ -105,6 +127,44 @@ const TECHNICAL_ALLOWLIST = new Set([
"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",
@@ -152,6 +212,10 @@ function aviso(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 = {};
@@ -170,7 +234,7 @@ function achatarObjeto(obj, prefixo = "") {
// 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}`);
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";
@@ -232,10 +296,10 @@ function verificarBasePt() {
}
// ---------------------------------------------------------------------------
// 2. Verificação de Catálogos e Dados de Outros Idiomas (quando presentes)
// 2. Verificação de Catálogos & Chaves por Idioma
// ---------------------------------------------------------------------------
function verificarOutrosIdiomas(ptMessages) {
console.log(`\n${CORES.negrito}${CORES.ciano}[2/3] Verificação de Catálogos & Chaves por Idioma${CORES.reset}`);
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);
@@ -251,7 +315,7 @@ function verificarOutrosIdiomas(ptMessages) {
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).`);
info(`Catálogo '${loc}' (${pathMessages}) ainda não criado (idioma inativo).`);
}
continue;
}
@@ -282,9 +346,9 @@ function verificarOutrosIdiomas(ptMessages) {
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)) {
if (!isTechnicalAllowlisted(vPt, k)) {
iguaisSemAllowlist++;
falha(`'${loc}': chave '${k}' tem valor idêntico ao pt fora da allowlist: "${vPt}"`);
}
}
}
@@ -302,19 +366,11 @@ function verificarOutrosIdiomas(ptMessages) {
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'.`);
if (faltando === 0 && vazios === 0 && iguaisSemAllowlist === 0) {
sucesso(`Catálogo '${loc}' ativo possui paridade e tradução completas 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'.`);
}
@@ -322,6 +378,140 @@ function verificarOutrosIdiomas(ptMessages) {
}
}
// ---------------------------------------------------------------------------
// 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/",
@@ -336,10 +526,10 @@ function sanitizarTextoParaLivro(conteudo) {
}
// ---------------------------------------------------------------------------
// 3. Verificação de Regras do Livro (§7 da spec: ausência fora do pt)
// 4. 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}`);
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;
@@ -417,18 +607,22 @@ function verificarRegrasLivro() {
}
}
function main() {
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) {
console.log(`${CORES.verde}${CORES.negrito}🎉 TODAS AS VERIFICAÇÕES DE I18N PASSARAM! (0 erros)${CORES.reset}\n`);
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`);
@@ -436,4 +630,4 @@ function main() {
}
}
process.exitCode = main();
process.exitCode = await main();