feat(i18n): restrict public routes and metadata to enabled locales
CI / Lint e auditorias (push) Successful in 41s
CI / Trivy (dependências e segredos) (push) Successful in 10s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m5s

- Add ENABLED_LOCALES configuration in i18n/links.js (currently ["pt"])
- Redirect inactive locale prefixes to /pt via 307 in middleware.js
- Restrict root locale negotiation to active locales with fallback to pt
- Hide LanguageSelector when only one locale is enabled
- Generate sitemap, hreflang alternates, and og:locale only for active locales
- Generate static SSG pages only for active locales in build
- Adapt check:i18n and smoke-imagem.sh to validate active vs inactive locales

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 12:10:25 -03:00
1 parent 3a1a095e97
commit d479a8d361
9 files changed
+219 -79

No files matched your search

+26 -9
View File
@@ -13,6 +13,7 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { ENABLED_LOCALES } from "../i18n/links.js";
const CORES = {
reset: "\x1b[0m",
@@ -209,10 +210,15 @@ function verificarOutrosIdiomas(ptMessages) {
for (const loc of LOCALES) {
if (loc === "pt") continue;
const isEnabled = ENABLED_LOCALES.includes(loc);
const pathMessages = `messages/${loc}.json`;
if (!existsSync(pathMessages)) {
aviso(`Catálogo '${loc}' (${pathMessages}) ainda não criado (esperado para sua fase de tradução).`);
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).`);
}
continue;
}
@@ -250,15 +256,26 @@ function verificarOutrosIdiomas(ptMessages) {
if (!(k in chavesPt)) extras++;
}
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 (isEnabled) {
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}' possui paridade estrutural completa com 'pt'.`);
if (faltando === 0 && vazios === 0) {
sucesso(`Catálogo '${loc}' ativo possui paridade estrutural completa 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'.`);
}
}
}
}
+50 -17
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Teste de fumaça do servidor: rotas por idioma, redirects e regras do livro.
#
# scripts/smoke-imagem.sh http://127.0.0.1:3999
# scripts/smoke-imagem.sh http://127.0.0.1:3998
#
# Cada falha é impressa e o script sai com 1 no fim, para mostrar tudo de uma vez.
set -uo pipefail
@@ -26,28 +26,61 @@ destino() {
# O servidor leva alguns segundos para subir.
for _ in $(seq 1 30); do curl -fsS -o /dev/null "$base/pt" && break; sleep 2; done
# Páginas que existem em todos os idiomas ativos (a home é a que mais depende do render no servidor).
for l in pt en es fr de zh-CN; do status 200 "/$l"; done
for p in /series /sobre /livro; do status 200 "/pt$p"; done
# Lista de idiomas ativos lida dinamicamente do módulo i18n/links.js
ENABLED_LOCALES=${ENABLED_LOCALES:-$(node -e 'import("./i18n/links.js").then(m => console.log(m.ENABLED_LOCALES.join(" ")))' 2>/dev/null || echo "pt")}
ALL_LOCALES="pt en es fr de zh-CN"
# Negociação de idioma na raiz.
destino 307 / /pt 'Accept-Language: pt-BR,pt;q=0.9'
destino 307 / /es 'Accept-Language: es-MX,es;q=0.9,en;q=0.5'
destino 307 / /fr 'Accept-Language: fr-CA,fr;q=0.9'
destino 307 / /de 'Accept-Language: de-AT,de;q=0.8'
destino 307 / /zh-CN 'Accept-Language: zh-CN,zh;q=0.9'
destino 307 / /en 'Accept-Language: it-IT'
destino 307 / /pt 'Accept-Language: en;q=0.5,pt;q=0.9'
destino 307 / /es 'Cookie: NEXT_LOCALE=es'
# 1. Páginas que existem em todos os idiomas ATIVOS
for l in $ENABLED_LOCALES; do
status 200 "/$l"
done
for p in /series /sobre /livro; do
status 200 "/pt$p"
done
# URLs antigas (sem prefixo) eram em português.
# 2. Idiomas INATIVOS: redirecionam com 307 para /pt
for l in $ALL_LOCALES; do
if [[ ! " $ENABLED_LOCALES " =~ " $l " ]]; then
destino 307 "/$l" /pt
destino 307 "/$l/series" /pt/series
destino 307 "/$l/livro" /pt/livro
fi
done
# 3. Negociação de idioma na raiz (deve considerar apenas ativos)
if [[ " $ENABLED_LOCALES " == " pt " ]]; then
# Com só pt ativo, todo Accept-Language e cookie de idioma inativo cai em /pt
destino 307 / /pt 'Accept-Language: pt-BR,pt;q=0.9'
destino 307 / /pt 'Accept-Language: es-MX,es;q=0.9,en;q=0.5'
destino 307 / /pt 'Accept-Language: fr-CA,fr;q=0.9'
destino 307 / /pt 'Accept-Language: de-AT,de;q=0.8'
destino 307 / /pt 'Accept-Language: zh-CN,zh;q=0.9'
destino 307 / /pt 'Accept-Language: it-IT'
destino 307 / /pt 'Accept-Language: en;q=0.5,pt;q=0.9'
destino 307 / /pt 'Cookie: NEXT_LOCALE=es'
else
destino 307 / /pt 'Accept-Language: pt-BR,pt;q=0.9'
[[ " $ENABLED_LOCALES " =~ " es " ]] && destino 307 / /es 'Accept-Language: es-MX,es;q=0.9,en;q=0.5'
[[ " $ENABLED_LOCALES " =~ " fr " ]] && destino 307 / /fr 'Accept-Language: fr-CA,fr;q=0.9'
[[ " $ENABLED_LOCALES " =~ " de " ]] && destino 307 / /de 'Accept-Language: de-AT,de;q=0.8'
[[ " $ENABLED_LOCALES " =~ " zh-CN " ]] && destino 307 / /zh-CN 'Accept-Language: zh-CN,zh;q=0.9'
[[ " $ENABLED_LOCALES " =~ " en " ]] && destino 307 / /en 'Accept-Language: it-IT'
destino 307 / /pt 'Accept-Language: en;q=0.5,pt;q=0.9'
[[ " $ENABLED_LOCALES " =~ " es " ]] && destino 307 / /es 'Cookie: NEXT_LOCALE=es'
fi
# 4. URLs antigas (sem prefixo) continuam 308 permanente para /pt/...
destino 308 /series/ESP32-S3 /pt/series/ESP32-S3 'Accept-Language: en-US'
destino 308 /livro /pt/livro 'Accept-Language: en-US'
# O livro existe só em português.
for l in en es fr de zh-CN; do status 404 "/$l/livro"; done
# 5. O livro dá 404 apenas em idiomas ATIVOS que não sejam pt
for l in $ENABLED_LOCALES; do
if [ "$l" != "pt" ]; then
status 404 "/$l/livro"
fi
done
# Metadata no <head> inicial (title, canonical, hreflang), para qualquer cliente.
# 6. Metadata no <head> inicial (title, canonical), para qualquer cliente
html=$(curl -s -A 'Mozilla/5.0 Chrome/130' "$base/pt/sobre")
head=${html%%</head>*}
for marca in '<title>' 'rel="canonical"'; do