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
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:
1 parent
ae61416468
commit
5a9fe676fc
61 files changed
+2517
-920
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
Executable
+63
@@ -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);
|
||||
@@ -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}")
|
||||
|
||||
Reference in new issue
Block a user