feat(i18n): Spanish localization (phase 4)
CI / Lint e auditorias (push) Successful in 1m31s
CI / Trivy (dependências e segredos) (push) Successful in 10s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m23s

Complete implementation of Spanish (es-419) language support:
- Canonical catalogs and data: messages/es.json, data/es/**, lib/data/es/**, public/exemplos/es/**
- Canonical slugs: /es/series, /es/catalogo, /es/comparacion, /es/componentes, /es/diagnostico, /es/frameworks, /es/glosario, /es/selector, /es/acerca
- Dedicated search index at /api/busca/es with Spanish accent normalization
- Strict exclusion of book references in Spanish routes and catalogs
- Language selector and root negotiation with es active (ENABLED_LOCALES = ['pt', 'en', 'es'])
- Extended verification suite: check-i18n, smoke-imagem and verifica-vazamentos

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 19:50:20 -03:00
1 parent 0940814522
commit 660b28c09d
72 files changed
+17848 -947

No files matched your search

+16 -4
View File
@@ -125,6 +125,15 @@ const TECHNICAL_ALLOWLIST = new Set([
"4",
"5",
"6",
"Buscar...",
"Catálogo de placas",
"Diagnóstico",
"Índice completo",
"Periféricos",
"Modern C++",
"Núcleos de CPU (Dual-Core vs Single-Core)",
"Bluetooth Classic (BR/EDR)",
"Ethernet MAC 10/100 Mbps",
]);
function isTechnicalAllowlisted(val, path = "") {
@@ -142,7 +151,7 @@ function isTechnicalAllowlisted(val, path = "") {
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+([.,]\d+)?(\s*°C)?\s*(a|to|-)\s*[-+]?\d+(\/\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;
@@ -416,7 +425,7 @@ async function verificarDadosPorIdioma() {
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)) {
for (const p of problemas.slice(0, 50)) {
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
}
} else {
@@ -433,7 +442,7 @@ async function verificarDadosPorIdioma() {
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)) {
for (const p of problemas.slice(0, 50)) {
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
}
} else {
@@ -454,7 +463,7 @@ async function verificarDadosPorIdioma() {
const problemas = encontrarIdenticos(pt, en, `conexoes/${f}`);
totalConexoesProblemas += problemas.length;
if (problemas.length > 0) {
for (const p of problemas.slice(0, 3)) {
for (const p of problemas.slice(0, 10)) {
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
}
}
@@ -503,6 +512,9 @@ async function verificarDadosPorIdioma() {
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.`);
for (const p of problemasGloss) {
console.log(` ${CORES.cinza}${p.path}: "${p.value}"${CORES.reset}`);
}
} else {
sucesso(`'${loc}': lib/data/${loc}/glossario.js validado sem strings vazadas.`);
}
+18 -1
View File
@@ -49,6 +49,12 @@ if [[ " $ENABLED_LOCALES " =~ " en " ]]; then
done
fi
if [[ " $ENABLED_LOCALES " =~ " es " ]]; then
for p in /series /catalogo /comparacion /componentes /diagnostico /frameworks /glosario /selector /acerca; do
status 200 "/es$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
@@ -84,6 +90,9 @@ else
if [[ ! " $ENABLED_LOCALES " =~ " es " ]]; then
destino 307 / /en 'Accept-Language: es-MX,es;q=0.9'
destino 307 / /en 'Cookie: NEXT_LOCALE=es'
else
destino 307 / /es 'Accept-Language: es-MX,es;q=0.9'
destino 307 / /es 'Cookie: NEXT_LOCALE=es'
fi
if [[ ! " $ENABLED_LOCALES " =~ " fr " ]]; then
destino 307 / /en 'Accept-Language: fr-CA,fr;q=0.9'
@@ -121,8 +130,16 @@ if [[ " $ENABLED_LOCALES " =~ " en " ]]; then
case "$head_en" in *"$marca"*) ;; *) echo "FALHOU /en/about: $marca fora do <head>"; falhas=$((falhas + 1));; esac
done
# 7. Varredura estrita contra vazamento de português (texto visível e blocos ld+json) nas 36 rotas
# 7. Varredura estrita contra vazamento de português (texto visível e blocos ld+json) nas rotas inglesas e espanholas
python3 scripts/verifica-vazamentos.py "$base" || falhas=$((falhas + 1))
fi
if [[ " $ENABLED_LOCALES " =~ " es " ]]; then
html_es=$(curl -s -A 'Mozilla/5.0 Chrome/130' "$base/es/acerca")
head_es=${html_es%%</head>*}
for marca in '<title>' 'rel="canonical"'; do
case "$head_es" in *"$marca"*) ;; *) echo "FALHOU /es/acerca: $marca fora do <head>"; falhas=$((falhas + 1));; esac
done
fi
exit $((falhas > 0))
+54 -6
View File
@@ -41,10 +41,19 @@ pt_regex = re.compile(
re.I
)
# Regex estrito para vazamentos de português em páginas em espanhol
# Palavras/sufixos que existem em português mas são incorretos em espanhol
pt_in_es_regex = re.compile(
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|"
r"série|séries|livro|ção|ções|muito|já|estão|é um|é uma|"
r"pelo|pela|especificações|especificação|início|artigo)\b",
re.I
)
# Termos técnicos legítimos aceitos em links/IDs/nomes
TECHNICAL_EXEMPT_PATHS = {"@id", "@type", "@context", "url", "image", "logo", "sameAs"}
def check_jsonld(obj, path=""):
def check_jsonld(obj, path="", regex=pt_regex, is_es=False):
problems = []
if isinstance(obj, str):
key = path.split(".")[-1].split("[")[0]
@@ -53,14 +62,14 @@ def check_jsonld(obj, path=""):
if (path.endswith("@id") or path.endswith("url")) and ("#artigo" in obj or "#sobre" in obj):
problems.append((path, f"Anchor leak in {obj}"))
return problems
if pt_regex.search(obj):
if regex.search(obj):
problems.append((path, obj))
elif isinstance(obj, list):
for i, val in enumerate(obj):
problems.extend(check_jsonld(val, f"{path}[{i}]"))
problems.extend(check_jsonld(val, f"{path}[{i}]", regex, is_es))
elif isinstance(obj, dict):
for k, val in obj.items():
problems.extend(check_jsonld(val, f"{path}.{k}" if path else k))
problems.extend(check_jsonld(val, f"{path}.{k}" if path else k, regex, is_es))
return problems
falhas = 0
@@ -81,7 +90,7 @@ for u in urls:
for i, s in enumerate(scripts):
try:
data = json.loads(s)
issues = check_jsonld(data, f"script[{i}]")
issues = check_jsonld(data, f"script[{i}]", pt_regex, False)
for p, v in issues:
print(f" ✖ JSON-LD vazamento em {u} -> {p}: \"{v}\"")
falhas += 1
@@ -97,8 +106,47 @@ for u in urls:
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
falhas += 1
# Varredura das rotas em espanhol
fixas_es = [
"/es", "/es/series", "/es/comparacion", "/es/diagnostico", "/es/selector",
"/es/frameworks", "/es/componentes", "/es/glosario", "/es/catalogo", "/es/acerca"
]
urls_es = fixas_es + [f"/es/series/{s}" for s in series] + [f"/es/frameworks/{f}" for f in frameworks]
print(f"\nESPDocs · Varredura de i18n nas 36 rotas em espanhol ({base})")
for u in urls_es:
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
# 1. Verifica blocos ld+json
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_es_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. Verifica texto visível
clean = re.sub(r"<(script|style)[\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 pt_in_es_regex.search(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 em inglês verificadas com ZERO vazamentos (texto e JSON-LD).")
print(f" ✔ Todas as {len(urls)} rotas em inglês e {len(urls_es)} rotas em espanhol verificadas com ZERO vazamentos (texto e JSON-LD).")
sys.exit(0)
else:
print(f" ✖ Total de falhas detectadas: {falhas}")