feat(i18n): Simplified Chinese (zh-CN), search answers per locale, fr/de/es data fixes
CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-01 23:28:39 -03:00
1 parent ae61416468
commit 5a9fe676fc
61 files changed
+2517 -920

No files matched your search

+38
View File
@@ -620,6 +620,43 @@ function verificarRegrasLivro() {
}
}
// ---------------------------------------------------------------------------
// 5. Portuguê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)\b/i;
function percorrer(valor, caminho, aoAchar) {
if (typeof valor === "string") {
if (PALAVRAS_SO_PORTUGUESAS.test(valor)) aoAchar(caminho, valor);
} else if (Array.isArray(valor)) {
valor.forEach((v, i) => percorrer(v, `${caminho}[${i}]`, aoAchar));
} else if (valor && typeof valor === "object") {
for (const [k, v] of Object.entries(valor)) percorrer(v, `${caminho}.${k}`, aoAchar);
}
}
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) => {
if (achados < 10) falha(`'${loc}': texto em português em ${caminho}: "${valor.slice(0, 80)}"`);
achados++;
});
}
}
if (achados === 0) sucesso("Nenhum texto em português 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}`);
@@ -629,6 +666,7 @@ async function main() {
verificarOutrosIdiomas(ptMessages);
await verificarDadosPorIdioma();
verificarRegrasLivro();
verificarPortuguesNosDados();
console.log(`\n${CORES.negrito}------------------------------------------------------${CORES.reset}`);
if (totalErros === 0 && totalAvisos === 0) {
+22
View File
@@ -67,6 +67,12 @@ if [[ " $ENABLED_LOCALES " =~ " de " ]]; then
done
fi
if [[ " $ENABLED_LOCALES " =~ " zh-CN " ]]; then
for p in /series /catalog /compare /components /diagnostics /frameworks /glossary /selector /about; do
status 200 "/zh-CN$p"
done
fi
# 2. Idiomas INATIVOS: redirecionam com 307 para o idioma fallback (en se ativo, senão pt)
for l in $ALL_LOCALES; do
if [[ ! " $ENABLED_LOCALES " =~ " $l " ]]; then
@@ -120,6 +126,11 @@ else
fi
if [[ ! " $ENABLED_LOCALES " =~ " zh-CN " ]]; then
destino 307 / /en 'Accept-Language: zh-CN,zh;q=0.9'
else
destino 307 / /zh-CN 'Accept-Language: zh-CN,zh;q=0.9'
destino 307 / /zh-CN 'Accept-Language: zh-Hans-CN'
destino 307 / /zh-CN 'Accept-Language: zh-TW,zh;q=0.9'
destino 307 / /zh-CN 'Cookie: NEXT_LOCALE=zh-CN'
fi
fi
@@ -176,4 +187,15 @@ if [[ " $ENABLED_LOCALES " =~ " de " ]]; then
done
fi
if [[ " $ENABLED_LOCALES " =~ " zh-CN " ]]; then
html_zh=$(curl -s -A 'Mozilla/5.0 Chrome/130' "$base/zh-CN/about")
head_zh=${html_zh%%</head>*}
for marca in '<title>' 'rel="canonical"'; do
case "$head_zh" in *"$marca"*) ;; *) echo "FALHOU /zh-CN/about: $marca fora do <head>"; falhas=$((falhas + 1));; esac
done
fi
# 8. Respostas da busca no idioma da página (o motor roda no navegador)
LOCALES="$ENABLED_LOCALES" node scripts/verifica-busca.mjs "$base" || falhas=$((falhas + 1))
exit $((falhas > 0))
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env node
// Respostas da busca por idioma: texto no idioma da página e links dentro dele.
//
// node scripts/verifica-busca.mjs http://127.0.0.1:3998
//
// O texto das "respostas diretas" é montado no navegador pelo motor; as varreduras
// de página não o enxergam. Aqui rodamos o mesmo motor sobre o índice servido.
import { preparar, buscar } from "../lib/searchEngine.js";
const base = process.argv[2];
if (!base) {
console.error("uso: verifica-busca.mjs <url-base>");
process.exit(2);
}
const idiomas = (process.env.LOCALES || "pt en es fr de zh-CN").split(/\s+/);
const consultas = {
pt: ["flash s3", "gpio26 s3", "pinos usb c6", "deep sleep p4", "psram"],
en: ["flash s3", "gpio26 s3", "usb pins c6", "deep sleep p4", "psram"],
es: ["flash s3", "gpio26 s3", "usb c6", "deep sleep p4", "psram"],
fr: ["flash s3", "gpio26 s3", "usb c6", "deep sleep p4", "psram"],
de: ["flash s3", "gpio26 s3", "usb c6", "deep sleep p4", "psram"],
"zh-CN": ["s3 闪存", "gpio26 s3", "c6 usb 引脚", "低功耗 p4", "哪款 esp32 有 psram", "安全启动"],
};
// Palavras que só existem em português (as respostas dos outros idiomas não podem ter).
const SO_PT = /(ligados à|dados da série|diagrama de pinos|que dependem do modo|já filtrado|em cada série|especificações do|\bcondicionais\b|\bpinos\b)/i;
const strings = (r) => [r.titulo, r.descricao, ...(r.linhas ?? []).flatMap((l) => [l.k, l.v]), ...(r.chips ?? [])].filter(Boolean);
let falhas = 0;
for (const loc of idiomas) {
const res = await fetch(`${base}/api/busca/${loc}`);
if (!res.ok) {
console.log(`FALHOU /api/busca/${loc}: ${res.status}`);
falhas++;
continue;
}
const indice = preparar(await res.json());
for (const q of consultas[loc] ?? consultas.en) {
const { resultados } = buscar(indice, q);
if (!resultados.length) {
console.log(`FALHOU [${loc}] "${q}": sem resultados`);
falhas++;
continue;
}
for (const r of resultados.filter((x) => x.tipo === "resposta" || x.tipo === "pino")) {
if (loc !== "pt" && !r.href.startsWith(`/${loc}/`)) {
console.log(`FALHOU [${loc}] "${q}": link fora do idioma: ${r.href}`);
falhas++;
}
if (loc !== "pt") {
for (const s of strings(r)) {
if (SO_PT.test(s)) {
console.log(`FALHOU [${loc}] "${q}": português em "${s.slice(0, 80)}"`);
falhas++;
}
}
}
}
}
}
if (falhas === 0) console.log(` ✔ Busca: respostas e links corretos em ${idiomas.join(", ")}.`);
process.exit(falhas > 0 ? 1 : 0);
+65 -1
View File
@@ -235,8 +235,72 @@ for u in urls_de:
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
falhas += 1
# Varredura das rotas em chinês simplificado: português OU inglês solto (fora de blocos de código)
pt_in_zh_regex = re.compile(
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|série|séries|livro|"
r"ção|ções|muito|já|estão|é um|é uma|pelo|pela|especificações|especificação|início|artigo|sobre|seletor)\b",
re.I
)
# Frase em inglês solta: palavra funcional entre palavras minúsculas, ou expressões de interface.
# Nomes de produto em Title Case ("Arduino Core for ESP32", "Google Home") não casam.
en_in_zh_regex = re.compile(
r"\b[a-z]{2,}\s+(?:the|and|with|for|your|this|that|from|are|you|will|not|how|what)\s+[a-z]{2,}\b"
r"|(?i:\b(?:skip to content|click here|learn more|read more|back to|please)\b)"
)
fixas_zh = [
"/zh-CN", "/zh-CN/series", "/zh-CN/compare", "/zh-CN/diagnostics", "/zh-CN/selector",
"/zh-CN/frameworks", "/zh-CN/components", "/zh-CN/glossary", "/zh-CN/catalog", "/zh-CN/about"
]
urls_zh = fixas_zh + [f"/zh-CN/series/{s}" for s in series] + [f"/zh-CN/frameworks/{f}" for f in frameworks]
print(f"\nESPDocs · Varredura de i18n nas {len(urls_zh)} rotas em chinês ({base})")
# Mensagens reais de log/erro da ferramenta: aparecem iguais em qualquer idioma.
ASSINATURAS_DE_LOG = {"Failed to connect to ESP32: Timed out waiting for packet header"}
def vazou_zh(texto):
if texto.strip() in ASSINATURAS_DE_LOG:
return None
return pt_in_zh_regex.search(texto) or en_in_zh_regex.search(texto)
for u in urls_zh:
try:
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
except Exception as e:
print(f" ✖ ERRO ao acessar {u}: {e}")
falhas += 1
continue
# <html lang> correto
if not re.search(r"<html[^>]*\blang=\"zh-CN\"", raw):
print(f" ✖ <html lang> diferente de zh-CN em {u}")
falhas += 1
# 1. Verifica blocos ld+json (strings de texto, não URLs/ids)
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
for i, s in enumerate(scripts):
try:
data = json.loads(s)
issues = check_jsonld(data, f"script[{i}]", pt_in_zh_regex, True)
for p_, v in issues:
print(f" ✖ JSON-LD vazamento em {u} -> {p_}: \"{v}\"")
falhas += 1
except Exception as e:
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
falhas += 1
# 2. Texto visível, sem blocos de código (exemplos têm logs e comentários em inglês por desenho)
clean = re.sub(r"<(script|style|pre|code)[\s\S]*?</\1>", "", raw)
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
hits = [l.strip() for l in clean.split("\n") if l.strip() and vazou_zh(l)]
for h in hits:
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
falhas += 1
if falhas == 0:
print(f" ✔ Todas as {len(urls)} rotas en, {len(urls_es)} rotas es, {len(urls_fr)} rotas fr e {len(urls_de)} rotas de verificadas com ZERO vazamentos (texto e JSON-LD).")
print(f" ✔ Todas as {len(urls)} rotas en, {len(urls_es)} rotas es, {len(urls_fr)} rotas fr, {len(urls_de)} rotas de e {len(urls_zh)} rotas zh verificadas com ZERO vazamentos (texto e JSON-LD).")
sys.exit(0)
else:
print(f" ✖ Total de falhas detectadas: {falhas}")