feat(i18n): French and German localization (phase 5)
- Add full message catalogs and content for French (fr) and German (de) - Implement ASCII canonical slugs for French and German routes - Add language-specific search endpoints (/api/busca/fr, /api/busca/de) - Configure strict 404 responses for /fr/livro and /de/livro - Localize SEO, OpenGraph (fr_FR, de_DE), Twitter and JSON-LD schemas - Add localized code example comments across 14 examples for fr and de - Enable 'fr' and 'de' in ENABLED_LOCALES with root negotiation and redirect - Extend test suite in check-i18n, smoke-imagem and verifica-vazamentos Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
1 parent
7b673aeb4b
commit
7fce46b9e1
104 files changed
+33369
-686
No files matched your search
@@ -134,6 +134,7 @@ const TECHNICAL_ALLOWLIST = new Set([
|
||||
"Núcleos de CPU (Dual-Core vs Single-Core)",
|
||||
"Bluetooth Classic (BR/EDR)",
|
||||
"Ethernet MAC 10/100 Mbps",
|
||||
"Séries ESP32",
|
||||
]);
|
||||
|
||||
function isTechnicalAllowlisted(val, path = "") {
|
||||
|
||||
@@ -55,6 +55,18 @@ if [[ " $ENABLED_LOCALES " =~ " es " ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ " $ENABLED_LOCALES " =~ " fr " ]]; then
|
||||
for p in /series /catalogue /comparaison /composants /diagnostic /frameworks /glossaire /selecteur /a-propos; do
|
||||
status 200 "/fr$p"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ " $ENABLED_LOCALES " =~ " de " ]]; then
|
||||
for p in /serien /katalog /vergleich /komponenten /diagnose /frameworks /glossar /auswahl /ueber-uns; do
|
||||
status 200 "/de$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
|
||||
@@ -96,9 +108,15 @@ else
|
||||
fi
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " fr " ]]; then
|
||||
destino 307 / /en 'Accept-Language: fr-CA,fr;q=0.9'
|
||||
else
|
||||
destino 307 / /fr 'Accept-Language: fr-FR,fr;q=0.9'
|
||||
destino 307 / /fr 'Cookie: NEXT_LOCALE=fr'
|
||||
fi
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " de " ]]; then
|
||||
destino 307 / /en 'Accept-Language: de-AT,de;q=0.8'
|
||||
else
|
||||
destino 307 / /de 'Accept-Language: de-DE,de;q=0.9'
|
||||
destino 307 / /de 'Cookie: NEXT_LOCALE=de'
|
||||
fi
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " zh-CN " ]]; then
|
||||
destino 307 / /en 'Accept-Language: zh-CN,zh;q=0.9'
|
||||
@@ -142,4 +160,20 @@ if [[ " $ENABLED_LOCALES " =~ " es " ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ " $ENABLED_LOCALES " =~ " fr " ]]; then
|
||||
html_fr=$(curl -s -A 'Mozilla/5.0 Chrome/130' "$base/fr/a-propos")
|
||||
head_fr=${html_fr%%</head>*}
|
||||
for marca in '<title>' 'rel="canonical"'; do
|
||||
case "$head_fr" in *"$marca"*) ;; *) echo "FALHOU /fr/a-propos: $marca fora do <head>"; falhas=$((falhas + 1));; esac
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ " $ENABLED_LOCALES " =~ " de " ]]; then
|
||||
html_de=$(curl -s -A 'Mozilla/5.0 Chrome/130' "$base/de/ueber-uns")
|
||||
head_de=${html_de%%</head>*}
|
||||
for marca in '<title>' 'rel="canonical"'; do
|
||||
case "$head_de" in *"$marca"*) ;; *) echo "FALHOU /de/ueber-uns: $marca fora do <head>"; falhas=$((falhas + 1));; esac
|
||||
done
|
||||
fi
|
||||
|
||||
exit $((falhas > 0))
|
||||
@@ -145,8 +145,98 @@ for u in urls_es:
|
||||
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
||||
falhas += 1
|
||||
|
||||
# Varredura das rotas em francês
|
||||
pt_in_fr_regex = re.compile(
|
||||
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|livro|"
|
||||
r"ção|ções|muito|já|estão|é um|é uma|pelo|pela|especificações|especificação|início|artigo|seletor)\b",
|
||||
re.I
|
||||
)
|
||||
|
||||
fixas_fr = [
|
||||
"/fr", "/fr/series", "/fr/comparaison", "/fr/diagnostic", "/fr/selecteur",
|
||||
"/fr/frameworks", "/fr/composants", "/fr/glossaire", "/fr/catalogue", "/fr/a-propos"
|
||||
]
|
||||
urls_fr = fixas_fr + [f"/fr/series/{s}" for s in series] + [f"/fr/frameworks/{f}" for f in frameworks]
|
||||
|
||||
print(f"\nESPDocs · Varredura de i18n nas {len(urls_fr)} rotas em francês ({base})")
|
||||
|
||||
for u in urls_fr:
|
||||
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_fr_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_fr_regex.search(l)]
|
||||
for h in hits:
|
||||
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
||||
falhas += 1
|
||||
|
||||
# Varredura das rotas em alemão
|
||||
pt_in_de_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
|
||||
)
|
||||
|
||||
fixas_de = [
|
||||
"/de", "/de/serien", "/de/vergleich", "/de/diagnose", "/de/auswahl",
|
||||
"/de/frameworks", "/de/komponenten", "/de/glossar", "/de/katalog", "/de/ueber-uns"
|
||||
]
|
||||
urls_de = fixas_de + [f"/de/serien/{s}" for s in series] + [f"/de/frameworks/{f}" for f in frameworks]
|
||||
|
||||
print(f"\nESPDocs · Varredura de i18n nas {len(urls_de)} rotas em alemão ({base})")
|
||||
|
||||
for u in urls_de:
|
||||
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_de_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_de_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 e {len(urls_es)} rotas em espanhol 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 e {len(urls_de)} rotas de 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