feat(i18n): English localization (phase 3)
- Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en - Implement localized routing, slugs, and language negotiation with fallback to en - Build dedicated search index for English with strict book exclusion - Update components and pages to dynamically render localized strings and datasets - Verify full structural parity and exact 1:1 match on Portuguese pages via comparator Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
1 parent
22b1f26aed
commit
c1bd2c7e11
61 files changed
+18182
-1225
No files matched your search
+98
-3
@@ -34,6 +34,9 @@ const TECHNICAL_ALLOWLIST = new Set([
|
||||
"ESPDocs",
|
||||
"Lucas Rayan Guerra",
|
||||
"Ciência Embarcada",
|
||||
"Hardware",
|
||||
"Component Registry",
|
||||
"ESP Component Registry",
|
||||
"ESP32",
|
||||
"ESP32-S2",
|
||||
"ESP32-S3",
|
||||
@@ -75,6 +78,7 @@ const TECHNICAL_ALLOWLIST = new Set([
|
||||
"JTAG",
|
||||
"Gitea",
|
||||
"GitHub",
|
||||
"LinkedIn",
|
||||
"MIT",
|
||||
"DevKit",
|
||||
"SoC",
|
||||
@@ -101,6 +105,36 @@ const TECHNICAL_ALLOWLIST = new Set([
|
||||
"6",
|
||||
]);
|
||||
|
||||
// Chaves específicas do livro (seção 7: apenas permitidas no pt)
|
||||
const BOOK_KEYS = new Set([
|
||||
"Header.book",
|
||||
"Header.bookNew",
|
||||
"Header.bookTitle",
|
||||
"Header.bookSubtitle",
|
||||
"Footer.bookNew",
|
||||
"Footer.bookTitle",
|
||||
"Footer.bookSubtitle",
|
||||
"Footer.amazon",
|
||||
"Footer.uiclap",
|
||||
"Home.bookChapters",
|
||||
"Home.bookTitle",
|
||||
"Home.bookSubtitle",
|
||||
"Home.bookNewBadge",
|
||||
"Home.bookDescription",
|
||||
"Home.bookParts",
|
||||
"Home.bookChaptersCount",
|
||||
"Home.buyDigital",
|
||||
"Home.buyPrint",
|
||||
"Home.fullRecord",
|
||||
"Livro.title",
|
||||
"Livro.subtitle",
|
||||
"Livro.badge",
|
||||
"Livro.description",
|
||||
"Livro.buyDigital",
|
||||
"Livro.buyPrint",
|
||||
"Livro.chaptersTitle",
|
||||
]);
|
||||
|
||||
let totalErros = 0;
|
||||
let totalAvisos = 0;
|
||||
|
||||
@@ -237,6 +271,10 @@ function verificarOutrosIdiomas(ptMessages) {
|
||||
let iguaisSemAllowlist = 0;
|
||||
|
||||
for (const [k, vPt] of Object.entries(chavesPt)) {
|
||||
// Chaves exclusivas do livro não são esperadas fora do pt (§7)
|
||||
if (BOOK_KEYS.has(k) || k.startsWith("Livro.")) {
|
||||
continue;
|
||||
}
|
||||
if (!(k in chavesLoc)) {
|
||||
faltando++;
|
||||
} else {
|
||||
@@ -253,7 +291,11 @@ function verificarOutrosIdiomas(ptMessages) {
|
||||
}
|
||||
|
||||
for (const k of Object.keys(chavesLoc)) {
|
||||
if (!(k in chavesPt)) extras++;
|
||||
if (BOOK_KEYS.has(k) || k.startsWith("Livro.")) {
|
||||
falha(`'${loc}': chave proibida do livro '${k}' presente no catálogo fora de pt.`);
|
||||
} else if (!(k in chavesPt)) {
|
||||
extras++;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnabled) {
|
||||
@@ -280,6 +322,19 @@ function verificarOutrosIdiomas(ptMessages) {
|
||||
}
|
||||
}
|
||||
|
||||
// Usos técnicos legítimos permitidos (seção 10: allowlist explícita)
|
||||
const TECHNICAL_BOOK_ALLOWLIST = [
|
||||
"https://esp-rs.github.io/book/",
|
||||
];
|
||||
|
||||
function sanitizarTextoParaLivro(conteudo) {
|
||||
let limpo = conteudo;
|
||||
for (const termo of TECHNICAL_BOOK_ALLOWLIST) {
|
||||
limpo = limpo.split(termo).join("");
|
||||
}
|
||||
return limpo;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Verificação de Regras do Livro (§7 da spec: ausência fora do pt)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -294,7 +349,7 @@ function verificarRegrasLivro() {
|
||||
// Checar messages/<loc>.json
|
||||
const pathMsg = `messages/${loc}.json`;
|
||||
if (existsSync(pathMsg)) {
|
||||
const conteudo = readFileSync(pathMsg, "utf8");
|
||||
const conteudo = sanitizarTextoParaLivro(readFileSync(pathMsg, "utf8"));
|
||||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||||
if (m) {
|
||||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${pathMsg}'.`);
|
||||
@@ -307,13 +362,53 @@ function verificarRegrasLivro() {
|
||||
if (existsSync(dirData)) {
|
||||
const arquivos = readdirSync(dirData).filter((f) => f.endsWith(".json"));
|
||||
for (const arq of arquivos) {
|
||||
const conteudo = readFileSync(join(dirData, arq), "utf8");
|
||||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirData, arq), "utf8"));
|
||||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||||
if (m) {
|
||||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirData}/${arq}'.`);
|
||||
violacoes++;
|
||||
}
|
||||
}
|
||||
const dirConexoes = join(dirData, "conexoes");
|
||||
if (existsSync(dirConexoes)) {
|
||||
const arqsConexoes = readdirSync(dirConexoes).filter((f) => f.endsWith(".json"));
|
||||
for (const arq of arqsConexoes) {
|
||||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirConexoes, arq), "utf8"));
|
||||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||||
if (m) {
|
||||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirConexoes}/${arq}'.`);
|
||||
violacoes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checar lib/data/<loc>/
|
||||
const dirLibData = `lib/data/${loc}`;
|
||||
if (existsSync(dirLibData)) {
|
||||
const arquivos = readdirSync(dirLibData).filter((f) => f.endsWith(".js"));
|
||||
for (const arq of arquivos) {
|
||||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirLibData, arq), "utf8"));
|
||||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||||
if (m) {
|
||||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirLibData}/${arq}'.`);
|
||||
violacoes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checar public/exemplos/<loc>/
|
||||
const dirExemplos = `public/exemplos/${loc}`;
|
||||
if (existsSync(dirExemplos)) {
|
||||
const arquivos = readdirSync(dirExemplos);
|
||||
for (const arq of arquivos) {
|
||||
const conteudo = sanitizarTextoParaLivro(readFileSync(join(dirExemplos, arq), "utf8"));
|
||||
const m = conteudo.match(FORBIDDEN_BOOK_TERMS);
|
||||
if (m) {
|
||||
falha(`Termo proibido do livro '${m[0]}' encontrado em '${dirExemplos}/${arq}'.`);
|
||||
violacoes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-10
@@ -30,6 +30,11 @@ for _ in $(seq 1 30); do curl -fsS -o /dev/null "$base/pt" && break; sleep 2; do
|
||||
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"
|
||||
|
||||
FALLBACK_LOCALE="pt"
|
||||
if [[ " $ENABLED_LOCALES " =~ " en " ]]; then
|
||||
FALLBACK_LOCALE="en"
|
||||
fi
|
||||
|
||||
# 1. Páginas que existem em todos os idiomas ATIVOS
|
||||
for l in $ENABLED_LOCALES; do
|
||||
status 200 "/$l"
|
||||
@@ -38,12 +43,22 @@ for p in /series /sobre /livro; do
|
||||
status 200 "/pt$p"
|
||||
done
|
||||
|
||||
# 2. Idiomas INATIVOS: redirecionam com 307 para /pt
|
||||
if [[ " $ENABLED_LOCALES " =~ " en " ]]; then
|
||||
for p in /series /catalog /compare /components /diagnostics /frameworks /glossary /selector /about; do
|
||||
status 200 "/en$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
|
||||
destino 307 "/$l" /pt
|
||||
destino 307 "/$l/series" /pt/series
|
||||
destino 307 "/$l/livro" /pt/livro
|
||||
destino 307 "/$l" "/$FALLBACK_LOCALE"
|
||||
destino 307 "/$l/series" "/$FALLBACK_LOCALE/series"
|
||||
if [ "$FALLBACK_LOCALE" = "en" ]; then
|
||||
destino 307 "/$l/livro" "/en"
|
||||
else
|
||||
destino 307 "/$l/livro" "/pt/livro"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -60,13 +75,25 @@ if [[ " $ENABLED_LOCALES " == " pt " ]]; then
|
||||
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 / /en 'Accept-Language: en-US,en;q=0.9'
|
||||
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'
|
||||
destino 307 / /pt 'Cookie: NEXT_LOCALE=pt'
|
||||
destino 307 / /en 'Cookie: NEXT_LOCALE=en'
|
||||
# Idiomas inativos negociam para fallback (en)
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " es " ]]; then
|
||||
destino 307 / /en 'Accept-Language: es-MX,es;q=0.9'
|
||||
destino 307 / /en 'Cookie: NEXT_LOCALE=es'
|
||||
fi
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " fr " ]]; then
|
||||
destino 307 / /en 'Accept-Language: fr-CA,fr;q=0.9'
|
||||
fi
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " de " ]]; then
|
||||
destino 307 / /en 'Accept-Language: de-AT,de;q=0.8'
|
||||
fi
|
||||
if [[ ! " $ENABLED_LOCALES " =~ " zh-CN " ]]; then
|
||||
destino 307 / /en 'Accept-Language: zh-CN,zh;q=0.9'
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. URLs antigas (sem prefixo) continuam 308 permanente para /pt/...
|
||||
@@ -87,4 +114,12 @@ for marca in '<title>' 'rel="canonical"'; do
|
||||
case "$head" in *"$marca"*) ;; *) echo "FALHOU /pt/sobre: $marca fora do <head>"; falhas=$((falhas + 1));; esac
|
||||
done
|
||||
|
||||
if [[ " $ENABLED_LOCALES " =~ " en " ]]; then
|
||||
html_en=$(curl -s -A 'Mozilla/5.0 Chrome/130' "$base/en/about")
|
||||
head_en=${html_en%%</head>*}
|
||||
for marca in '<title>' 'rel="canonical"'; do
|
||||
case "$head_en" in *"$marca"*) ;; *) echo "FALHOU /en/about: $marca fora do <head>"; falhas=$((falhas + 1));; esac
|
||||
done
|
||||
fi
|
||||
|
||||
exit $((falhas > 0))
|
||||
Reference in new issue
Block a user