feat(i18n): English localization (phase 3)
CI / Lint e auditorias (push) Successful in 1m51s
CI / Trivy (dependências e segredos) (push) Successful in 15s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m41s

- 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:
Lucas RayanandAntigravity committed 2026-11-01 14:17:13 -03:00
1 parent 22b1f26aed
commit c1bd2c7e11
61 files changed
+18182 -1225

No files matched your search

+29 -27
View File
@@ -5,7 +5,7 @@
// Ponto único de configuração de idiomas ATIVOS.
// Para ativar um novo idioma no site, basta adicionar seu código aqui (ex.: ["pt", "en"]).
export const ENABLED_LOCALES = ["pt"];
export const ENABLED_LOCALES = ["pt", "en"];
export const ALL_LOCALES = [
{ code: "pt", label: "Português", short: "PT", lang: "pt-BR", og: "pt_BR" },
@@ -23,22 +23,22 @@ export const LOCALES = ALL_LOCALES.filter((l) => ENABLED_LOCALES.includes(l.code
export const CANONICAL_ROUTES = [
{
id: "series_item",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:series|serien)\/([^/]+)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:series|serien)\/([^/?#]+)/,
getSlug: (loc, key) => (loc === "de" ? `/serien/${key}` : `/series/${key}`),
},
{
id: "frameworks_item",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/frameworks\/([^/]+)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/frameworks\/([^/?#]+)/,
getSlug: (loc, key) => `/frameworks/${key}`,
},
{
id: "series",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:series|serien)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:series|serien)(?:\/|$)/,
getSlug: (loc) => (loc === "de" ? "/serien" : "/series"),
},
{
id: "catalogo",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:catalogo|catalog|catalogue|katalog)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:catalogo|catalog|catalogue|katalog)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "de") return "/katalog";
if (loc === "fr") return "/catalogue";
@@ -48,7 +48,7 @@ export const CANONICAL_ROUTES = [
},
{
id: "comparacao",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:comparacao|compare|comparacion|comparaison|vergleich)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:comparacao|compare|comparacion|comparaison|vergleich)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "zh-CN") return "/compare";
if (loc === "es") return "/comparacion";
@@ -59,7 +59,7 @@ export const CANONICAL_ROUTES = [
},
{
id: "componentes",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:componentes|components|composants|komponenten)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:componentes|components|composants|komponenten)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "zh-CN") return "/components";
if (loc === "fr") return "/composants";
@@ -69,7 +69,7 @@ export const CANONICAL_ROUTES = [
},
{
id: "diagnostico",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:diagnostico|diagnostics|diagnostic|diagnose)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:diagnostico|diagnostics|diagnostic|diagnose)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "zh-CN") return "/diagnostics";
if (loc === "fr") return "/diagnostic";
@@ -79,12 +79,12 @@ export const CANONICAL_ROUTES = [
},
{
id: "frameworks",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/frameworks(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/frameworks(?:\/|$)/,
getSlug: () => "/frameworks",
},
{
id: "glossario",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:glossario|glossary|glosario|glossaire|glossar)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:glossario|glossary|glosario|glossaire|glossar)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "zh-CN") return "/glossary";
if (loc === "es") return "/glosario";
@@ -95,7 +95,7 @@ export const CANONICAL_ROUTES = [
},
{
id: "seletor",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:seletor|selector|selecteur|auswahl)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:seletor|selector|selecteur|auswahl)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "es" || loc === "zh-CN") return "/selector";
if (loc === "fr") return "/selecteur";
@@ -105,7 +105,7 @@ export const CANONICAL_ROUTES = [
},
{
id: "sobre",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:sobre|about|acerca|a-propos|ueber-uns)(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/(?:sobre|about|acerca|a-propos|ueber-uns)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "zh-CN") return "/about";
if (loc === "es") return "/acerca";
@@ -116,34 +116,36 @@ export const CANONICAL_ROUTES = [
},
{
id: "livro",
match: /^\/(?:pt|en|es|fr|de|zh-CN)\/livro(?:\/|$)/,
match: /^(?:\/(?:pt|en|es|fr|de|zh-CN))?\/livro(?:\/|$)/,
getSlug: (loc) => (loc === "pt" ? "/livro" : ""),
},
];
export function getLocalizedHref(pathname, targetLocale) {
if (!pathname || pathname === "/" || pathname === `/${targetLocale}` || pathname === `/${targetLocale}/`) {
return `/${targetLocale}/`;
if (!pathname || pathname === "/") {
return `/${targetLocale}`;
}
const [cleanPath, ...queryParts] = pathname.split("?");
const query = queryParts.length > 0 ? `?${queryParts.join("?")}` : "";
for (const loc of ALL_LOCALE_CODES) {
if (cleanPath === `/${loc}` || cleanPath === `/${loc}/`) {
return `/${targetLocale}${query}`;
}
}
for (const route of CANONICAL_ROUTES) {
const m = pathname.match(route.match);
const m = cleanPath.match(route.match);
if (m) {
const slug = route.getSlug(targetLocale, m[1]);
return slug ? `/${targetLocale}${slug}` : `/${targetLocale}/`;
return slug ? `/${targetLocale}${slug}${query}` : `/${targetLocale}${query}`;
}
}
return `/${targetLocale}/`;
return `/${targetLocale}${query}`;
}
export function getPtPath(pathname) {
if (!pathname || pathname === "/" || pathname === "/pt" || pathname === "/pt/") return "/pt";
for (const route of CANONICAL_ROUTES) {
const m = pathname.match(route.match);
if (m) {
const slug = route.getSlug("pt", m[1]);
return slug ? `/pt${slug}` : "/pt";
}
}
return "/pt";
return getLocalizedHref(pathname, "pt");
}