feat(i18n): routing and localization infrastructure (phase 1)

Implement Phase 1 of i18n spec:
- Configure next-intl with 6 locales (pt, en, es, fr, de, zh-CN) and pathnames
- Move routes to app/[locale] structure with localized layouts and metadata
- Add middleware for root language negotiation (307), legacy redirects (308), and 404 for non-pt book
- Implement language selector dropdown in header with cookie persistence
- Add system typography rules for zh-CN and word-break/hyphenation for de/fr

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 09:31:22 -03:00
1 parent e92aea0c84
commit 83c0ba3e74
38 files changed
+1574 -199

No files matched your search

+8
View File
@@ -6,6 +6,14 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o
## [Não lançado] ## [Não lançado]
### Adicionado
- Infraestrutura de internacionalização com `next-intl` (Fase 1):
- Roteamento com prefixo de idioma sob `app/[locale]/`, com 6 locales declarados (`pt`, `en`, `es`, `fr`, `de`, `zh-CN`).
- `middleware.js` com negociação de idioma na raiz por cookie `NEXT_LOCALE` e cabeçalho `Accept-Language` (com pesos `q`), redirecionamento 308 de URLs antigas sem prefixo para `/pt/`, e retorno 404 para `/livro` fora do português.
- Seletor de idiomas acessível no cabeçalho com atalho para os 6 idiomas, persistência via cookie `NEXT_LOCALE` e mapeamento de rota correspondente.
- Regras de tipografia de sistema CJK para `zh-CN` e hifenização/quebra de palavras para `de` e `fr`.
## [1.1.0] - 2026-11-01 ## [1.1.0] - 2026-11-01
### Adicionado ### Adicionado
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+162
View File
@@ -0,0 +1,162 @@
import { notFound } from "next/navigation";
import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { routing } from "@/i18n/routing";
import "../globals.css";
import Providers from "@/components/Providers";
import { SITE, jsonLd } from "@/lib/seo";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
display: "swap",
weight: ["500", "600", "700"],
});
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
display: "swap",
weight: ["400", "500", "600", "700"],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains",
display: "swap",
weight: ["400", "500", "600"],
});
const LANG_MAP = {
pt: "pt-BR",
en: "en",
es: "es-419",
fr: "fr",
de: "de",
"zh-CN": "zh-CN",
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }) {
const { locale } = await params;
const ogLocaleMap = {
pt: "pt_BR",
en: "en_US",
es: "es_419",
fr: "fr_FR",
de: "de_DE",
"zh-CN": "zh_CN",
};
return {
metadataBase: new URL(SITE.url),
title: {
default: "ESPDocs · Documentação do ESP32 em português",
template: "%s · ESPDocs",
},
description:
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro. Datasheets, periféricos, segurança em hardware e diagramas de conexão.",
applicationName: "ESPDocs",
authors: [{ name: "Lucas Rayan Guerra", url: "https://lucasrguerra.dev.br" }],
creator: "Lucas Rayan Guerra",
publisher: "Ciência Embarcada",
category: "technology",
keywords: [
"ESP32", "ESP32 português", "documentação ESP32", "pinout ESP32",
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
"comparar ESP32", "qual ESP32 escolher", "datasheet ESP32",
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embarcados",
],
alternates: { canonical: `/${locale}` },
openGraph: {
type: "website",
locale: ogLocaleMap[locale] || "pt_BR",
url: `${SITE.url}/${locale}`,
siteName: SITE.nome,
title: "ESPDocs · Documentação do ESP32 em português",
description:
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro.",
},
twitter: {
card: "summary_large_image",
title: "ESPDocs · Documentação do ESP32 em português",
description:
"Especificações, pinagem, comparação e seleção das 12 séries de ESP32, em português.",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "48x48", type: "image/x-icon" },
{ url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
{ url: "/icon.svg", type: "image/svg+xml" },
],
apple: [
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
],
shortcut: "/favicon.ico",
},
manifest: "/site.webmanifest",
formatDetection: { telephone: false, address: false, email: false },
};
}
export const viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#f5f6f8" },
{ media: "(prefers-color-scheme: dark)", color: "#07090d" },
],
};
export default async function RootLayout({ children, params }) {
const { locale } = await params;
if (!routing.locales.includes(locale)) {
notFound();
}
const lang = LANG_MAP[locale] || locale;
const messages = await getMessages();
return (
<html lang={lang} className={`${spaceGrotesk.variable} ${manrope.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
<head>
{/* Dados estruturados do site e da organização. É o que permite ao
Google montar o box de busca interna e entender a autoria. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.siteESociedade()) }}
/>
</head>
<body className="font-sans antialiased bg-canvas text-ink overflow-x-clip" suppressHydrationWarning>
<a
href="#conteudo"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-full focus:bg-ink focus:px-5 focus:py-3 focus:text-sm focus:font-semibold focus:text-canvas"
>
Pular para o conteúdo
</a>
<NextIntlClientProvider locale={locale} messages={messages}>
<Providers>
{children}
</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}
@@ -1,3 +1,4 @@
import { notFound } from "next/navigation";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import BookCover from "@/components/BookCover"; import BookCover from "@/components/BookCover";
@@ -16,11 +17,20 @@ import {
Wifi, Wifi,
} from "lucide-react"; } from "lucide-react";
export const metadata = paginaMeta({ export function generateStaticParams() {
return [{ locale: "pt" }];
}
export async function generateMetadata({ params }) {
const { locale } = await params;
if (locale !== "pt") {
return {};
}
return paginaMeta({
titulo: "Microcontroladores ESP32, o livro", titulo: "Microcontroladores ESP32, o livro",
descricao: descricao:
"Conheça o livro Microcontroladores ESP32: conectividade, eficiência e desempenho. 12 partes e 78 capítulos em português sobre arquitetura, energia, rádio, periféricos e firmware.", "Conheça o livro Microcontroladores ESP32: conectividade, eficiência e desempenho. 12 partes e 78 capítulos em português sobre arquitetura, energia, rádio, periféricos e firmware.",
caminho: "/livro", caminho: "/pt/livro",
keywords: [ keywords: [
"livro ESP32", "livro ESP32",
"Microcontroladores ESP32", "Microcontroladores ESP32",
@@ -28,7 +38,8 @@ export const metadata = paginaMeta({
"Lucas Rayan Guerra", "Lucas Rayan Guerra",
"Ciência Embarcada", "Ciência Embarcada",
], ],
}); });
}
const CHAPTERS = [ const CHAPTERS = [
{ {
@@ -128,7 +139,11 @@ const FACTS = [
["ISBN", SITE.livroIsbn], ["ISBN", SITE.livroIsbn],
]; ];
export default function LivroPage() { export default async function LivroPage({ params }) {
const { locale } = await params;
if (locale !== "pt") {
notFound();
}
return ( return (
<div className="site-canvas min-h-screen"> <div className="site-canvas min-h-screen">
<script <script
File renamed without changes.
+15 -4
View File
@@ -5,6 +5,7 @@ import Link from "next/link";
import seriesData from "@/public/series.json"; import seriesData from "@/public/series.json";
import frameworksData from "@/public/frameworks.json"; import frameworksData from "@/public/frameworks.json";
import { SITE } from "@/lib/seo"; import { SITE } from "@/lib/seo";
import { getLocalizedHref } from "@/components/LanguageSelector";
import { import {
AlertTriangle, AlertTriangle,
ArrowRight, ArrowRight,
@@ -87,7 +88,11 @@ const ERRORS = [
}, },
]; ];
export default function Home() { export default async function Home({ params }) {
const { locale } = await params;
const isPt = locale === "pt";
const getLinkHref = (href) => getLocalizedHref(`/${locale}${href}`, locale);
const seriesCount = Object.keys(seriesData).length; const seriesCount = Object.keys(seriesData).length;
const frameworkCount = Object.keys(frameworksData).length; const frameworkCount = Object.keys(frameworksData).length;
const featured = FEATURED.map((key) => [key, seriesData[key]]).filter(([, serie]) => Boolean(serie)); const featured = FEATURED.map((key) => [key, seriesData[key]]).filter(([, serie]) => Boolean(serie));
@@ -113,11 +118,11 @@ export default function Home() {
</p> </p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row"> <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/seletor" className="btn btn-primary w-full sm:w-auto"> <Link href={getLinkHref("/seletor")} className="btn btn-primary w-full sm:w-auto">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" /> <SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Escolher meu chip Escolher meu chip
</Link> </Link>
<Link href="/series" className="btn btn-ghost w-full sm:w-auto"> <Link href={getLinkHref("/series")} className="btn btn-ghost w-full sm:w-auto">
Explorar as {seriesCount} séries Explorar as {seriesCount} séries
<ArrowRight className="h-4 w-4" aria-hidden="true" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
@@ -127,10 +132,14 @@ export default function Home() {
<span><strong className="font-semibold text-ink">{seriesCount}</strong> séries</span> <span><strong className="font-semibold text-ink">{seriesCount}</strong> séries</span>
<span aria-hidden="true" className="text-line-strong">·</span> <span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span> <span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span>
{isPt && (
<>
<span aria-hidden="true" className="text-line-strong">·</span> <span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">78</strong> capítulos no livro</span> <span><strong className="font-semibold text-ink">78</strong> capítulos no livro</span>
<span aria-hidden="true" className="text-line-strong">·</span> <span aria-hidden="true" className="text-line-strong">·</span>
<span>100% em português</span> <span>100% em português</span>
</>
)}
</p> </p>
</div> </div>
@@ -176,6 +185,7 @@ export default function Home() {
</div> </div>
</section> </section>
{isPt && (
<section className="shell section pt-8 sm:pt-12" aria-labelledby="livro-titulo"> <section className="shell section pt-8 sm:pt-12" aria-labelledby="livro-titulo">
<div className="relative isolate overflow-hidden rounded-[2rem] bg-[#0b0d10] ring-1 ring-white/10 dark:bg-[#05070a] dark:ring-white/15 sm:rounded-[2.5rem]"> <div className="relative isolate overflow-hidden rounded-[2rem] bg-[#0b0d10] ring-1 ring-white/10 dark:bg-[#05070a] dark:ring-white/15 sm:rounded-[2.5rem]">
<div <div
@@ -226,7 +236,7 @@ export default function Home() {
Comprar impresso Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" /> <ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a> </a>
<Link href="/livro" className="inline-flex min-h-11 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium text-[#a6afbd] transition-colors duration-300 hover:text-[#f4f6f8]"> <Link href="/pt/livro" className="inline-flex min-h-11 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium text-[#a6afbd] transition-colors duration-300 hover:text-[#f4f6f8]">
Ficha completa Ficha completa
<ArrowRight className="h-4 w-4" aria-hidden="true" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
@@ -242,6 +252,7 @@ export default function Home() {
</div> </div>
</div> </div>
</section> </section>
)}
<section className="shell section" aria-labelledby="ferramentas-titulo"> <section className="shell section" aria-labelledby="ferramentas-titulo">
<div className="mx-auto max-w-2xl text-center"> <div className="mx-auto max-w-2xl text-center">
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+24
View File
@@ -759,4 +759,28 @@
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
/* ------------------------------------------------------------------------
Tipografia e layout internacionalizados (seção 9b da spec)
------------------------------------------------------------------------ */
html[lang="zh-CN"] {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", sans-serif;
letter-spacing: 0;
line-height: 1.7;
}
html[lang="zh-CN"] code,
html[lang="zh-CN"] pre,
html[lang="zh-CN"] kbd {
font-family: var(--font-mono);
}
html[lang="de"],
html[lang="fr"] {
hyphens: auto;
}
p, li, dd {
overflow-wrap: anywhere;
}
} }
-134
View File
@@ -1,134 +0,0 @@
import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
import "./globals.css";
import Providers from "@/components/Providers";
import { SITE, jsonLd } from "@/lib/seo";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
display: "swap",
weight: ["500", "600", "700"],
});
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
display: "swap",
weight: ["400", "500", "600", "700"],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains",
display: "swap",
weight: ["400", "500", "600"],
});
export const metadata = {
// metadataBase é o que transforma caminhos relativos em URLs absolutas nas
// tags canonical e og:image. Sem ele, o Google recebe og:image quebrado.
metadataBase: new URL(SITE.url),
title: {
default: "ESPDocs · Documentação do ESP32 em português",
// Cada página fornece só o próprio nome; o sufixo entra automaticamente.
template: "%s · ESPDocs",
},
description:
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro. Datasheets, periféricos, segurança em hardware e diagramas de conexão.",
applicationName: "ESPDocs",
authors: [{ name: "Lucas Rayan Guerra", url: "https://lucasrguerra.dev.br" }],
creator: "Lucas Rayan Guerra",
publisher: "Ciência Embarcada",
category: "technology",
keywords: [
"ESP32", "ESP32 português", "documentação ESP32", "pinout ESP32",
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
"comparar ESP32", "qual ESP32 escolher", "datasheet ESP32",
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embarcados",
],
alternates: { canonical: "/" },
openGraph: {
type: "website",
locale: "pt_BR",
url: SITE.url,
siteName: "ESPDocs",
title: "ESPDocs · Documentação do ESP32 em português",
description:
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro.",
},
twitter: {
card: "summary_large_image",
title: "ESPDocs · Documentação do ESP32 em português",
description:
"Especificações, pinagem, comparação e seleção das 12 séries de ESP32, em português.",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "48x48", type: "image/x-icon" },
{ url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
{ url: "/icon.svg", type: "image/svg+xml" },
],
apple: [
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
],
shortcut: "/favicon.ico",
},
manifest: "/site.webmanifest",
formatDetection: { telephone: false, address: false, email: false },
};
export const viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#f5f6f8" },
{ media: "(prefers-color-scheme: dark)", color: "#07090d" },
],
};
export default function RootLayout({ children }) {
return (
<html lang="pt-BR" className={`${spaceGrotesk.variable} ${manrope.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
<head>
{/* Dados estruturados do site e da organização. É o que permite ao
Google montar o box de busca interna e entender a autoria. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.siteESociedade()) }}
/>
</head>
<body className="font-sans antialiased bg-canvas text-ink overflow-x-clip" suppressHydrationWarning>
<a
href="#conteudo"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-full focus:bg-ink focus:px-5 focus:py-3 focus:text-sm focus:font-semibold focus:text-canvas"
>
Pular para o conteúdo
</a>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
+51 -36
View File
@@ -1,17 +1,21 @@
"use client";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation";
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import GiteaMark from "@/components/GiteaMark"; import GiteaMark from "@/components/GiteaMark";
import { ArrowUpRight } from "lucide-react"; import { ArrowUpRight } from "lucide-react";
import { SITE } from "@/lib/seo"; import { SITE } from "@/lib/seo";
import { LOCALES, getLocalizedHref } from "@/components/LanguageSelector";
const COLUMNS = [ const BASE_COLUMNS = [
{ {
title: "Explorar", title: "Explorar",
links: [ links: [
{ href: "/series", label: "Séries ESP32" }, { href: "/series", label: "Séries ESP32" },
{ href: "/comparacao", label: "Comparador" }, { href: "/comparacao", label: "Comparador" },
{ href: "/catalogo", label: "Catálogo de placas" }, { href: "/catalogo", label: "Catálogo de placas" },
{ href: "/livro", label: "O livro" }, { href: "/livro", label: "O livro", livroOnly: true },
], ],
}, },
{ {
@@ -35,14 +39,26 @@ const COLUMNS = [
]; ];
export default function Footer() { export default function Footer() {
const pathname = usePathname() || "";
const year = new Date().getFullYear(); const year = new Date().getFullYear();
const currentLocale =
LOCALES.find((l) => pathname === `/${l.code}` || pathname.startsWith(`/${l.code}/`))?.code || "pt";
const isPt = currentLocale === "pt";
const getLinkHref = (href) => getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
const columns = BASE_COLUMNS.map((col) => ({
...col,
links: col.links.filter((l) => !l.livroOnly || isPt),
}));
return ( return (
<footer className="mt-20 border-t border-line md:mt-28" aria-label="Rodapé do ESPDocs"> <footer className="mt-20 border-t border-line md:mt-28" aria-label="Rodapé do ESPDocs">
<div className="shell py-14 md:py-20"> <div className="shell py-14 md:py-20">
<div className="grid gap-12 lg:grid-cols-[1.1fr_2fr]"> <div className="grid gap-12 lg:grid-cols-[1.1fr_2fr]">
<div> <div>
<Link href="/" className="inline-flex min-h-11 items-center gap-2.5" aria-label="ESPDocs, página inicial"> <Link href={getLinkHref("/")} className="inline-flex min-h-11 items-center gap-2.5" aria-label="ESPDocs, página inicial">
<Logo className="h-7 w-7" /> <Logo className="h-7 w-7" />
<span className="font-display text-2xl font-semibold tracking-[-0.045em] text-ink">ESPDocs</span> <span className="font-display text-2xl font-semibold tracking-[-0.045em] text-ink">ESPDocs</span>
</Link> </Link>
@@ -51,8 +67,10 @@ export default function Footer() {
Dados públicos, curadoria editorial e links para as fontes oficiais. Dados públicos, curadoria editorial e links para as fontes oficiais.
</p> </p>
{isPt && (
<>
<Link <Link
href="/livro" href="/pt/livro"
className="group mt-7 grid grid-cols-[3rem_1fr_auto] items-center gap-3.5 rounded-2xl border border-line bg-surface p-3.5 transition-colors duration-300 hover:border-line-strong" className="group mt-7 grid grid-cols-[3rem_1fr_auto] items-center gap-3.5 rounded-2xl border border-line bg-surface p-3.5 transition-colors duration-300 hover:border-line-strong"
> >
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -82,10 +100,12 @@ export default function Footer() {
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" /> <ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</a> </a>
</div> </div>
</>
)}
</div> </div>
<div className="grid grid-cols-2 gap-8 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
{COLUMNS.map((column) => ( {columns.map((column) => (
<nav key={column.title} aria-label={column.title}> <nav key={column.title} aria-label={column.title}>
<h2 className="text-sm font-semibold text-ink">{column.title}</h2> <h2 className="text-sm font-semibold text-ink">{column.title}</h2>
<ul className="mt-2 space-y-0"> <ul className="mt-2 space-y-0">
@@ -102,7 +122,7 @@ export default function Footer() {
<ArrowUpRight className="h-3 w-3" aria-hidden="true" /> <ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a> </a>
) : ( ) : (
<Link href={link.href} className="inline-flex min-h-11 min-w-11 items-center text-sm text-muted transition-colors hover:text-accent"> <Link href={getLinkHref(link.href)} className="inline-flex min-h-11 min-w-11 items-center text-sm text-muted transition-colors hover:text-accent">
{link.label} {link.label}
</Link> </Link>
)} )}
@@ -119,45 +139,40 @@ export default function Footer() {
<br /> <br />
<span className="text-xs text-faint">Ciência Embarcada</span> <span className="text-xs text-faint">Ciência Embarcada</span>
</p> </p>
<div className="mt-4 flex items-center gap-2"> <div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-sm">
<a <a href={SITE.autorUrl} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center text-muted transition-colors hover:text-accent">
href={SITE.repositorio} Site pessoal
target="_blank"
rel="noopener noreferrer"
aria-label="Gitea do projeto (abre em nova aba)"
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
>
<GiteaMark className="h-4 w-4" />
</a> </a>
<a <a href={SITE.autorLinkedin} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center text-muted transition-colors hover:text-accent">
href={SITE.autorLinkedin} LinkedIn
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn do autor (abre em nova aba)"
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M4.98 3.5a2.5 2.5 0 1 1 0 5.001 2.5 2.5 0 0 1 0-5zM3 8.98h4v12H3v-12zm6.5 0h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" />
</svg>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="mt-14 flex flex-col gap-4 border-t border-line pt-6 text-xs leading-relaxed text-faint md:flex-row md:items-center md:justify-between"> <div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-line pt-8 text-xs text-muted sm:flex-row">
<p>© {year} ESPDocs. Projeto independente, sem afiliação com a Espressif Systems.</p> <p>© {year} ESPDocs · Conteúdo sob licença MIT. Feito para a comunidade de sistemas embarcados.</p>
<p className="flex flex-wrap items-center gap-x-5 gap-y-2"> <div className="flex items-center gap-6">
<a href={SITE.blog} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent"> <a
href={SITE.repositorio}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-1.5 transition-colors hover:text-accent"
>
<GiteaMark className="h-4 w-4" aria-hidden="true" />
<span>Gitea</span>
</a>
<span>·</span>
<a
href={SITE.blog}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center transition-colors hover:text-accent"
>
Ciência Embarcada Ciência Embarcada
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a> </a>
<a href={SITE.autorUrl} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent"> </div>
lucasrguerra.dev.br
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a>
<span>Fontes oficiais: Espressif Systems.</span>
</p>
</div> </div>
</div> </div>
</footer> </footer>
+34 -15
View File
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
import Link from "next/link"; import Link from "next/link";
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import SearchDialog from "@/components/SearchDialog"; import SearchDialog from "@/components/SearchDialog";
import LanguageSelector, { LOCALES, getLocalizedHref } from "@/components/LanguageSelector";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { import {
@@ -69,12 +70,16 @@ const MOBILE_LINKS = [
{ href: "/sobre", label: "Sobre" }, { href: "/sobre", label: "Sobre" },
]; ];
function isActivePath(pathname, href) { function isActivePath(pathname, href, currentLocale) {
return href === "/" ? pathname === "/" : pathname === href || String(pathname).startsWith(`${href}/`); const localized = getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
if (href === "/" || localized === `/${currentLocale}/` || localized === `/${currentLocale}`) {
return pathname === `/${currentLocale}` || pathname === `/${currentLocale}/`;
}
return pathname === localized || String(pathname).startsWith(`${localized}/`);
} }
export default function Header() { export default function Header() {
const pathname = usePathname(); const pathname = usePathname() || "";
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [openMenu, setOpenMenu] = useState(null); const [openMenu, setOpenMenu] = useState(null);
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
@@ -82,6 +87,12 @@ export default function Header() {
const closeRef = useRef(null); const closeRef = useRef(null);
const menuButtonRef = useRef(null); const menuButtonRef = useRef(null);
const currentLocale =
LOCALES.find((l) => pathname === `/${l.code}` || pathname.startsWith(`/${l.code}/`))?.code || "pt";
const isPt = currentLocale === "pt";
const getLinkHref = (href) => getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
useEffect(() => setMounted(true), []); useEffect(() => setMounted(true), []);
useEffect(() => { useEffect(() => {
@@ -118,18 +129,20 @@ export default function Header() {
}; };
}, [drawerOpen]); }, [drawerOpen]);
const visibleMobileLinks = isPt ? MOBILE_LINKS : MOBILE_LINKS.filter((l) => l.href !== "/livro");
return ( return (
<header className="sticky top-0 z-50 pt-3 sm:pt-4"> <header className="sticky top-0 z-50 pt-3 sm:pt-4">
<div className="shell"> <div className="shell">
<div className="nav-pill flex h-16 items-center justify-between gap-3 rounded-full px-3 sm:px-4"> <div className="nav-pill flex h-16 items-center justify-between gap-3 rounded-full px-3 sm:px-4">
<Link href="/" className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label="ESPDocs, página inicial"> <Link href={getLinkHref("/")} className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label="ESPDocs, página inicial">
<Logo className="h-[30px] w-[30px]" /> <Logo className="h-[30px] w-[30px]" />
<span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span> <span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span>
</Link> </Link>
<nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label="Navegação principal"> <nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label="Navegação principal">
{MENUS.map((menu) => { {MENUS.map((menu) => {
const active = menu.items.some((item) => isActivePath(pathname, item.href)); const active = menu.items.some((item) => isActivePath(pathname, item.href, currentLocale));
const expanded = openMenu === menu.id; const expanded = openMenu === menu.id;
return ( return (
<div key={menu.id} className="relative"> <div key={menu.id} className="relative">
@@ -150,12 +163,13 @@ export default function Header() {
> >
{menu.items.map((item) => { {menu.items.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const localizedHref = getLinkHref(item.href);
return ( return (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={localizedHref}
className="group flex items-start gap-3 rounded-2xl px-3 py-3 transition-colors hover:bg-surface-2" className="group flex items-start gap-3 rounded-2xl px-3 py-3 transition-colors hover:bg-surface-2"
aria-current={isActivePath(pathname, item.href) ? "page" : undefined} aria-current={isActivePath(pathname, item.href, currentLocale) ? "page" : undefined}
> >
<span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent-soft text-accent transition-colors group-hover:bg-surface"> <span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent-soft text-accent transition-colors group-hover:bg-surface">
<Icon className="h-4 w-4" aria-hidden="true" /> <Icon className="h-4 w-4" aria-hidden="true" />
@@ -173,20 +187,23 @@ export default function Header() {
); );
})} })}
<Link href="/livro" className={`nav-link ${isActivePath(pathname, "/livro") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/livro") ? "page" : undefined}> {isPt && (
<Link href={getLinkHref("/livro")} className={`nav-link ${isActivePath(pathname, "/livro", currentLocale) ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/livro", currentLocale) ? "page" : undefined}>
Livro Livro
</Link> </Link>
<Link href="/sobre" className={`nav-link ${isActivePath(pathname, "/sobre") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre") ? "page" : undefined}> )}
<Link href={getLinkHref("/sobre")} className={`nav-link ${isActivePath(pathname, "/sobre", currentLocale) ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre", currentLocale) ? "page" : undefined}>
Sobre Sobre
</Link> </Link>
</nav> </nav>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Link href="/seletor" className="btn btn-primary btn-sm hidden lg:inline-flex"> <Link href={getLinkHref("/seletor")} className="btn btn-primary btn-sm hidden lg:inline-flex">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" /> <SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Selecionar chip Selecionar chip
</Link> </Link>
<SearchDialog /> <SearchDialog />
<LanguageSelector />
<ThemeToggle /> <ThemeToggle />
<button <button
ref={menuButtonRef} ref={menuButtonRef}
@@ -238,9 +255,10 @@ export default function Header() {
</button> </button>
</div> </div>
{isPt && (
<div className="px-4 pb-2"> <div className="px-4 pb-2">
<Link <Link
href="/livro" href="/pt/livro"
onClick={() => setDrawerOpen(false)} onClick={() => setDrawerOpen(false)}
className="group grid grid-cols-[3.25rem_1fr_auto] items-center gap-3 rounded-2xl bg-[#0b0d10] p-3 text-[#f4f6f8] ring-1 ring-white/10" className="group grid grid-cols-[3.25rem_1fr_auto] items-center gap-3 rounded-2xl bg-[#0b0d10] p-3 text-[#f4f6f8] ring-1 ring-white/10"
> >
@@ -261,15 +279,16 @@ export default function Header() {
<ArrowRight className="h-4 w-4 shrink-0 text-[#75cdc1] transition-transform duration-300 group-hover:translate-x-1" aria-hidden="true" /> <ArrowRight className="h-4 w-4 shrink-0 text-[#75cdc1] transition-transform duration-300 group-hover:translate-x-1" aria-hidden="true" />
</Link> </Link>
</div> </div>
)}
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label="Navegação móvel"> <nav className="flex-1 overflow-y-auto px-3 py-2" aria-label="Navegação móvel">
{MOBILE_LINKS.map((link) => ( {visibleMobileLinks.map((link) => (
<Link <Link
key={link.href} key={link.href}
href={link.href} href={getLinkHref(link.href)}
onClick={() => setDrawerOpen(false)} onClick={() => setDrawerOpen(false)}
className="group flex min-h-11 items-center justify-between gap-3 rounded-xl px-3 text-sm font-medium text-ink-soft transition-colors hover:bg-surface-2 hover:text-ink" className="group flex min-h-11 items-center justify-between gap-3 rounded-xl px-3 text-sm font-medium text-ink-soft transition-colors hover:bg-surface-2 hover:text-ink"
aria-current={isActivePath(pathname, link.href) ? "page" : undefined} aria-current={isActivePath(pathname, link.href, currentLocale) ? "page" : undefined}
> >
{link.label} {link.label}
<ArrowRight className="h-4 w-4 text-faint transition-transform duration-300 group-hover:translate-x-0.5 group-hover:text-accent" aria-hidden="true" /> <ArrowRight className="h-4 w-4 text-faint transition-transform duration-300 group-hover:translate-x-0.5 group-hover:text-accent" aria-hidden="true" />
@@ -278,7 +297,7 @@ export default function Header() {
</nav> </nav>
<div className="p-4 pt-2"> <div className="p-4 pt-2">
<Link href="/seletor" onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full"> <Link href={getLinkHref("/seletor")} onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" /> <SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Selecionar meu chip Selecionar meu chip
</Link> </Link>
+232
View File
@@ -0,0 +1,232 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import { ChevronDown, Globe } from "lucide-react";
export const LOCALES = [
{ code: "pt", label: "Português", short: "PT", lang: "pt-BR" },
{ code: "en", label: "English", short: "EN", lang: "en" },
{ code: "es", label: "Español", short: "ES", lang: "es-419" },
{ code: "fr", label: "Français", short: "FR", lang: "fr" },
{ code: "de", label: "Deutsch", short: "DE", lang: "de" },
{ code: "zh-CN", label: "简体中文", short: "中", lang: "zh-CN" },
];
const CANONICAL_ROUTES = [
{
id: "series_item",
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\/([^/]+)/,
getSlug: (loc, key) => `/frameworks/${key}`,
},
{
id: "series",
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)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "de") return "/katalog";
if (loc === "fr") return "/catalogue";
if (loc === "en" || loc === "zh-CN") return "/catalog";
return "/catalogo";
},
},
{
id: "comparacao",
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";
if (loc === "fr") return "/comparaison";
if (loc === "de") return "/vergleich";
return "/comparacao";
},
},
{
id: "componentes",
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";
if (loc === "de") return "/komponenten";
return "/componentes";
},
},
{
id: "diagnostico",
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";
if (loc === "de") return "/diagnose";
return "/diagnostico";
},
},
{
id: "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)(?:\/|$)/,
getSlug: (loc) => {
if (loc === "en" || loc === "zh-CN") return "/glossary";
if (loc === "es") return "/glosario";
if (loc === "fr") return "/glossaire";
if (loc === "de") return "/glossar";
return "/glossario";
},
},
{
id: "seletor",
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";
if (loc === "de") return "/auswahl";
return "/seletor";
},
},
{
id: "sobre",
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";
if (loc === "fr") return "/a-propos";
if (loc === "de") return "/ueber-uns";
return "/sobre";
},
},
{
id: "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}/`;
}
for (const route of CANONICAL_ROUTES) {
const m = pathname.match(route.match);
if (m) {
const slug = route.getSlug(targetLocale, m[1]);
return slug ? `/${targetLocale}${slug}` : `/${targetLocale}/`;
}
}
return `/${targetLocale}/`;
}
export default function LanguageSelector() {
const pathname = usePathname() || "";
const [open, setOpen] = useState(false);
const [hash, setHash] = useState("");
const containerRef = useRef(null);
// Detecta locale da URL atual
const currentLocaleCode =
LOCALES.find((l) => pathname === `/${l.code}` || pathname.startsWith(`/${l.code}/`))?.code || "pt";
const currentLocale = LOCALES.find((l) => l.code === currentLocaleCode) || LOCALES[0];
useEffect(() => {
if (typeof window !== "undefined") {
setHash(window.location.hash || "");
}
}, [pathname]);
useEffect(() => {
function onPointerDown(e) {
if (containerRef.current && !containerRef.current.contains(e.target)) {
setOpen(false);
}
}
function onKeyDown(e) {
if (e.key === "Escape") {
setOpen(false);
}
}
document.addEventListener("mousedown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("mousedown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, []);
const handleSelect = (code) => {
if (typeof document !== "undefined") {
document.cookie = `NEXT_LOCALE=${code}; path=/; max-age=31536000; SameSite=Lax`;
}
setOpen(false);
};
return (
<div ref={containerRef} className="relative">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex h-11 min-w-[44px] items-center gap-1.5 rounded-full border border-line bg-surface px-3 text-sm font-semibold text-ink transition-colors hover:bg-surface-2 focus-visible:outline-2 focus-visible:outline-accent"
aria-haspopup="listbox"
aria-expanded={open}
aria-label={`Idioma atual: ${currentLocale.label}. Clique para trocar de idioma.`}
title={`Idioma: ${currentLocale.label}`}
>
<Globe className="h-4 w-4 text-muted" aria-hidden="true" />
<span className="font-semibold">{currentLocale.short}</span>
<ChevronDown
className={`h-3 w-3 text-muted transition-transform duration-200 ${open ? "rotate-180" : ""}`}
aria-hidden="true"
/>
</button>
{open && (
<div
role="listbox"
aria-label="Idiomas disponíveis"
className="absolute right-0 top-full mt-2 w-48 rounded-2xl border border-line bg-surface p-1.5 shadow-[0_24px_60px_-20px_rgba(15,23,42,0.35)] dark:shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] z-50"
>
{LOCALES.map((loc) => {
const isActive = loc.code === currentLocaleCode;
const href = `${getLocalizedHref(pathname, loc.code)}${hash}`;
return (
<a
key={loc.code}
href={href}
lang={loc.lang}
hrefLang={loc.lang}
role="option"
aria-selected={isActive}
aria-current={isActive ? "true" : undefined}
onClick={() => handleSelect(loc.code)}
className={`flex min-h-[44px] items-center justify-between rounded-xl px-3 py-2 text-sm transition-colors ${
isActive
? "bg-accent-soft text-accent font-semibold"
: "text-ink-soft hover:bg-surface-2 hover:text-ink"
}`}
>
<span>{loc.label}</span>
<span className={`text-xs ${isActive ? "text-accent" : "text-muted"}`}>
{loc.short}
</span>
</a>
);
})}
</div>
)}
</div>
);
}
+4
View File
@@ -0,0 +1,4 @@
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
+26
View File
@@ -0,0 +1,26 @@
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !hasLocale(routing.locales, locale)) {
locale = routing.defaultLocale;
}
let messages = {};
try {
messages = (await import(`../messages/${locale}.json`)).default;
} catch {
try {
messages = (await import("../messages/pt.json")).default;
} catch {
messages = {};
}
}
return {
locale,
messages,
};
});
+101
View File
@@ -0,0 +1,101 @@
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["pt", "en", "es", "fr", "de", "zh-CN"],
defaultLocale: "en",
localePrefix: "always",
pathnames: {
"/": "/",
"/series": {
pt: "/series",
en: "/series",
es: "/series",
fr: "/series",
de: "/serien",
"zh-CN": "/series",
},
"/series/[key]": {
pt: "/series/[key]",
en: "/series/[key]",
es: "/series/[key]",
fr: "/series/[key]",
de: "/serien/[key]",
"zh-CN": "/series/[key]",
},
"/catalogo": {
pt: "/catalogo",
en: "/catalog",
es: "/catalogo",
fr: "/catalogue",
de: "/katalog",
"zh-CN": "/catalog",
},
"/comparacao": {
pt: "/comparacao",
en: "/compare",
es: "/comparacion",
fr: "/comparaison",
de: "/vergleich",
"zh-CN": "/compare",
},
"/componentes": {
pt: "/componentes",
en: "/components",
es: "/componentes",
fr: "/composants",
de: "/komponenten",
"zh-CN": "/components",
},
"/diagnostico": {
pt: "/diagnostico",
en: "/diagnostics",
es: "/diagnostico",
fr: "/diagnostic",
de: "/diagnose",
"zh-CN": "/diagnostics",
},
"/frameworks": {
pt: "/frameworks",
en: "/frameworks",
es: "/frameworks",
fr: "/frameworks",
de: "/frameworks",
"zh-CN": "/frameworks",
},
"/frameworks/[key]": {
pt: "/frameworks/[key]",
en: "/frameworks/[key]",
es: "/frameworks/[key]",
fr: "/frameworks/[key]",
de: "/frameworks/[key]",
"zh-CN": "/frameworks/[key]",
},
"/glossario": {
pt: "/glossario",
en: "/glossary",
es: "/glosario",
fr: "/glossaire",
de: "/glossar",
"zh-CN": "/glossary",
},
"/seletor": {
pt: "/seletor",
en: "/selector",
es: "/selector",
fr: "/selecteur",
de: "/auswahl",
"zh-CN": "/selector",
},
"/sobre": {
pt: "/sobre",
en: "/about",
es: "/acerca",
fr: "/a-propos",
de: "/ueber-uns",
"zh-CN": "/about",
},
"/livro": {
pt: "/livro",
},
},
});
+1
View File
@@ -0,0 +1 @@
{}
+102
View File
@@ -0,0 +1,102 @@
import createMiddleware from "next-intl/middleware";
import { NextResponse } from "next/server";
import { routing } from "./i18n/routing";
const intlMiddleware = createMiddleware(routing);
const SUPPORTED_LOCALES = routing.locales;
function parseAcceptLanguage(header) {
if (!header) return [];
return header
.split(",")
.map((part) => {
const [langRange, qPart] = part.trim().split(";");
let q = 1.0;
if (qPart) {
const match = qPart.match(/q=([\d.]+)/);
if (match) {
const parsed = parseFloat(match[1]);
if (!isNaN(parsed)) q = parsed;
}
}
return { lang: langRange.trim().toLowerCase(), q };
})
.sort((a, b) => b.q - a.q);
}
function matchLocale(lang) {
if (lang.startsWith("pt")) return "pt";
if (lang.startsWith("es")) return "es";
if (lang.startsWith("fr")) return "fr";
if (lang.startsWith("de")) return "de";
if (lang.startsWith("zh")) return "zh-CN";
if (lang.startsWith("en")) return "en";
return null;
}
function resolveRootLocale(request) {
const cookieValue = request.cookies.get("NEXT_LOCALE")?.value;
if (cookieValue && SUPPORTED_LOCALES.includes(cookieValue)) {
return cookieValue;
}
const acceptLang = request.headers.get("accept-language");
const parsed = parseAcceptLanguage(acceptLang);
for (const { lang } of parsed) {
const matched = matchLocale(lang);
if (matched) return matched;
}
return "en";
}
export default function middleware(request) {
const { pathname, search } = request.nextUrl;
// Exclusões de caminhos e arquivos estáticos
if (
pathname.startsWith("/api") ||
pathname.startsWith("/_next") ||
pathname === "/robots.txt" ||
pathname === "/sitemap.xml" ||
pathname === "/google21a790a2c23c0ec5.html" ||
/\.[a-zA-Z0-9]+$/.test(pathname)
) {
return NextResponse.next();
}
// 1. Raiz: negociação de idioma (307)
if (pathname === "/") {
const targetLocale = resolveRootLocale(request);
const url = new URL(`/${targetLocale}/`, request.url);
url.search = search;
const response = NextResponse.redirect(url, 307);
response.headers.set("Vary", "Accept-Language, Cookie");
response.headers.set("Cache-Control", "private, no-store");
return response;
}
// 2. Livro fora do português: 404
if (/^\/(en|es|fr|de|zh-CN)\/livro(\/.*)?$/.test(pathname)) {
return new NextResponse(null, { status: 404 });
}
// 3. URLs antigas sem prefixo reconhecido: redirecionamento 308 para /pt/<caminho>
const hasLocalePrefix = SUPPORTED_LOCALES.some(
(loc) => pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)
);
if (!hasLocalePrefix) {
const url = new URL(`/pt${pathname}`, request.url);
url.search = search;
return NextResponse.redirect(url, 308);
}
// 4. Delegação para o roteamento do next-intl
return intlMiddleware(request);
}
export const config = {
matcher: [
"/((?!api|_next|_vercel|.*\\..*|robots\\.txt|sitemap\\.xml|google21a790a2c23c0ec5\\.html).*)",
],
};
+6 -1
View File
@@ -1,3 +1,7 @@
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./i18n/request.js");
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// A imagem Docker liga isto (NEXT_OUTPUT=standalone) para empacotar só o que // A imagem Docker liga isto (NEXT_OUTPUT=standalone) para empacotar só o que
@@ -13,4 +17,5 @@ const nextConfig = {
}, },
}; };
export default nextConfig; export default withNextIntl(nextConfig);
+779 -3
View File
@@ -11,6 +11,7 @@
"googleapis": "^165.0.0", "googleapis": "^165.0.0",
"lucide-react": "^1.16.0", "lucide-react": "^1.16.0",
"next": "^15.5.28", "next": "^15.5.28",
"next-intl": "^4.14.9",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
@@ -37,6 +38,31 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@eloqnt/config": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@eloqnt/config/-/config-0.1.1.tgz",
"integrity": "sha512-YRDigSaiJEzKDfzTth5kG+L/PKfB3mo8h7yNzpXExPfvQmcD05WoZT57PdhJE0jI+Vva6eb0OCU8hnHHgdME3g==",
"license": "MIT"
},
"node_modules/@eloqnt/format-json": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eloqnt/format-json/-/format-json-0.1.0.tgz",
"integrity": "sha512-4Pkb+HyzgWNJXoU48aJhUyNZSjZvMGooPCNG1ZFmBm55xTiNNPQCNXH66Ob6sXS6RvbWZrRpCf+Uo/Jz9K9g+A==",
"license": "MIT",
"dependencies": {
"@eloqnt/config": "^0.1.0"
}
},
"node_modules/@eloqnt/format-po": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@eloqnt/format-po/-/format-po-0.1.1.tgz",
"integrity": "sha512-aLtmIoqXGEaeTT0RcsLN8sZ3aIrsT/c/dVs28zrLhDjO3+vA3KNP+r98N0xq8w/k36+/GIUt4uZNbqcA71GVWw==",
"license": "MIT",
"dependencies": {
"@eloqnt/config": "^0.1.1",
"po-parser": "^2.2.0"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -214,6 +240,36 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz",
"integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==",
"license": "MIT"
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.22.tgz",
"integrity": "sha512-YuwN+dDKgssfuYwtOyGQD0CSn7K46g9cv1Fp0VgKpjZDDFvv8b8crzP990IcJhRPWHhKzCGsbUSJVIguyyFA5A==",
"license": "MIT",
"dependencies": {
"@formatjs/icu-skeleton-parser": "2.1.12"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.12.tgz",
"integrity": "sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==",
"license": "MIT"
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.14.tgz",
"integrity": "sha512-8O7V38QmbB11+ryJ3KM/znrzXtiqgpXzqNoqDsndHZZ5/zDKTTL+cf5F0mzXkQEwhx8IlWnqYvEovf2S5EwLTw==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.7"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1108,6 +1164,310 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@parcel/watcher": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
"integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.6.0",
"@parcel/watcher-darwin-arm64": "2.6.0",
"@parcel/watcher-darwin-x64": "2.6.0",
"@parcel/watcher-freebsd-x64": "2.6.0",
"@parcel/watcher-linux-arm-glibc": "2.6.0",
"@parcel/watcher-linux-arm-musl": "2.6.0",
"@parcel/watcher-linux-arm64-glibc": "2.6.0",
"@parcel/watcher-linux-arm64-musl": "2.6.0",
"@parcel/watcher-linux-x64-glibc": "2.6.0",
"@parcel/watcher-linux-x64-musl": "2.6.0",
"@parcel/watcher-win32-arm64": "2.6.0",
"@parcel/watcher-win32-x64": "2.6.0"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
"integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
"integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
"integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
"integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
"integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
"integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
"integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
"integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
"integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
"integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
"integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
"integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1161,6 +1521,228 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.21.tgz",
"integrity": "sha512-VRreJpbRygpSD1hj3HBEOIA1xjXYnhB+IfcKsxZRhCIBGRP4UkqxZH4/ipFh/heIRp7EKQ56E8ygwEftLNCWXw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.21.tgz",
"integrity": "sha512-KqCJ04pMYDBUYpAOFGUFR8TEjqo4h0Vtwe/za9xNRDKsxSDnjgM7HVhfO0uNNpMMlM2ao2E6d0iLpApIRvNGnA==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.21.tgz",
"integrity": "sha512-tLkw3uWxQj69qBRX5Xm8vbq+5iX5e/ktoPXhrDeFFa2jNMc+AWcxa0bqwatvVGK40v5mcmz6Ulu5Nk9jF8ciMA==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.21.tgz",
"integrity": "sha512-lFHVlHB0B5C7FwyprWJQp7b2iF/ohj2RqnMiv8uCnFJc+rYP0Py2I7ftZ+rOX4Zu7Ra9eMk7GnmuxAywCZYUJQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.21.tgz",
"integrity": "sha512-gXDgrMMDsWT7r5gZsml10snBS9yaoVYwU9vGffa+DAts4ctEMrMHv+G8AH+ZxCV9gr5MfdgtGGQKjXjLay4mXQ==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-ppc64-gnu": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.21.tgz",
"integrity": "sha512-hZF7J2YzXiADZ4eQxTFOWJcSSy3fbUR8/DcCXqiBHh0bVyLl2Y+O5pVWxk/r/XV5tA9n7DnnLlvkPPsHoajE9w==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-s390x-gnu": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.21.tgz",
"integrity": "sha512-rmtGwkAlbl+fQMGrGUMklUmByueOmt1ju9sKsE5YWVA0KfFYM0W87WVx4lFYQXt+E4P3zkLjeqo/AUa5mFbF1A==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.21.tgz",
"integrity": "sha512-EUJsyrFw3aNE64iQzk3bDqjADgl4l0vpxfwVMjMuZIooNg35yKqvkw3bYNEhQyrBDY0+5kKoiirGLpth3Wf3lA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.21.tgz",
"integrity": "sha512-ZXUnLjxTHxvNhVmQmYlb8XNKArfi/MiGoW5DmfuME8ttCLH9OJTqJVFy7g1yYxUCgw3tU6dF1uU59N8P/Yr5Pw==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.21.tgz",
"integrity": "sha512-jnnQpAHiWZgEobVfOPvJJzm1Jh4ZEIKh2IqcDqHv6zJcjvaEkHdkt+qdRsJ9AqvK7g18WBRHTnvKm5IxdbkagA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.21.tgz",
"integrity": "sha512-TaVTnNm9c1I+VG3hJUt11uspwbTrTQ9+fUPzODlvx4tj/ChYqUm4cKTIHw3O97hILxpmhBR0hbvLEs9seOV59A==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.21.tgz",
"integrity": "sha512-aPv58Clf6SdAQtouzDYKbEtcCETY2hOXlrHgSFnFIJQOlMWINHSGazaM1Iv63/9wOUDPq6+0ZFytMqnPtER9OQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1170,6 +1752,15 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@swc/types": {
"version": "0.1.28",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz",
"integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
@@ -2699,6 +3290,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/content-type": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2847,7 +3451,6 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4233,6 +4836,21 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/icu-minify": {
"version": "4.14.9",
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.14.9.tgz",
"integrity": "sha512-PmO+fNnC3K3mfPKTnm3RrPERrhQZnjm1AlpyZQ0ShbXi8Y/kYZ3YOde8TEjCLz9k3P2DmzkBn8lykGrCAi1Ajw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^3.4.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4285,6 +4903,31 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "11.2.15",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.15.tgz",
"integrity": "sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/fast-memoize": "3.1.7",
"@formatjs/icu-messageformat-parser": "3.5.18"
}
},
"node_modules/intl-messageformat/node_modules/@formatjs/icu-messageformat-parser": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.18.tgz",
"integrity": "sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==",
"license": "MIT",
"dependencies": {
"@formatjs/icu-skeleton-parser": "2.1.11"
}
},
"node_modules/intl-messageformat/node_modules/@formatjs/icu-skeleton-parser": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz",
"integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==",
"license": "MIT"
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4450,7 +5093,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -4505,7 +5147,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
@@ -5386,6 +6027,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
"integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
"license": "MIT",
"dependencies": {
"content-type": "^2.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/next": { "node_modules/next": {
"version": "15.5.28", "version": "15.5.28",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.28.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.5.28.tgz",
@@ -5438,6 +6095,86 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.14.9",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.14.9.tgz",
"integrity": "sha512-uauRtUC+TrYNA1OL/QcwL9irndLrI9jK3Wdaa9W0B5AeKh9cqVdNeT0IWgLnhBqBRrVwppVdI0M+C1+JdGPQ3w==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@eloqnt/config": "^0.1.1",
"@eloqnt/format-json": "^0.1.0",
"@eloqnt/format-po": "^0.1.1",
"@formatjs/intl-localematcher": "^0.8.1",
"@parcel/watcher": "^2.4.1",
"@swc/core": "~1.16.0",
"icu-minify": "^4.14.9",
"negotiator": "^1.0.0",
"next-intl-swc-plugin-extractor": "4.14.9",
"use-intl": "^4.14.9"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/next-intl-swc-plugin-extractor": {
"version": "4.14.9",
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.14.9.tgz",
"integrity": "sha512-8jAkmDwQJo6RDavBHm9QRl1YTn2WXf+rI4SVhMaZxZcxopnwZic6fKTXOoNIo3kzC+a6vJySkOkXmHTVFLAYtg==",
"license": "MIT"
},
"node_modules/next-intl/node_modules/@swc/core": {
"version": "1.16.21",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.21.tgz",
"integrity": "sha512-sE2asan0US6gcp11oBDFJb5+s+ltaAPBcagVQGub1+qo+8TsQET2TmXfJCw/Oy0w1eEPp1rV1wsR/Umx2KXGcw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.28"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.16.21",
"@swc/core-darwin-x64": "1.16.21",
"@swc/core-linux-arm-gnueabihf": "1.16.21",
"@swc/core-linux-arm64-gnu": "1.16.21",
"@swc/core-linux-arm64-musl": "1.16.21",
"@swc/core-linux-ppc64-gnu": "1.16.21",
"@swc/core-linux-s390x-gnu": "1.16.21",
"@swc/core-linux-x64-gnu": "1.16.21",
"@swc/core-linux-x64-musl": "1.16.21",
"@swc/core-win32-arm64-msvc": "1.16.21",
"@swc/core-win32-ia32-msvc": "1.16.21",
"@swc/core-win32-x64-msvc": "1.16.21"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/next-themes": { "node_modules/next-themes": {
"version": "0.4.6", "version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
@@ -5448,6 +6185,12 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
} }
}, },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-domexception": { "node_modules/node-domexception": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -5786,6 +6529,12 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/po-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.2.0.tgz",
"integrity": "sha512-NdTrKgh0oO7+y+RFX8KKhOB438x/j94UGXEFzl/7pHH0JjWSn42iJ9tgmPksIO6n1JKDHmNDcejPJfKspljB9g==",
"license": "MIT"
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -7009,6 +7758,33 @@
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
"license": "BSD" "license": "BSD"
}, },
"node_modules/use-intl": {
"version": "4.14.9",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.14.9.tgz",
"integrity": "sha512-37ktvdCCz+6JY2VFPl/GV69Ds4ahiUT2c2xEZZRZYxdgGLxFsUbdxzjQVRm3YMIOUU0VPs6zvlifb4Yjk4IxIg==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^3.1.0",
"@schummar/icu-type-parser": "1.21.5",
"icu-minify": "^4.14.9",
"intl-messageformat": "^11.1.0"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/web-streams-polyfill": { "node_modules/web-streams-polyfill": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+1
View File
@@ -17,6 +17,7 @@
"googleapis": "^165.0.0", "googleapis": "^165.0.0",
"lucide-react": "^1.16.0", "lucide-react": "^1.16.0",
"next": "^15.5.28", "next": "^15.5.28",
"next-intl": "^4.14.9",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
+9 -2
View File
@@ -92,6 +92,7 @@ function obterRotasExistentes() {
if (arq.endsWith("page.jsx") || arq.endsWith("page.js")) { if (arq.endsWith("page.jsx") || arq.endsWith("page.js")) {
let rota = arq let rota = arq
.replace(/^app/, "") .replace(/^app/, "")
.replace(/\/\[locale\]/, "")
.replace(/\/page\.(jsx|js)$/, ""); .replace(/\/page\.(jsx|js)$/, "");
if (rota === "") rota = "/"; if (rota === "") rota = "/";
rotas.add(rota); rotas.add(rota);
@@ -132,7 +133,12 @@ function auditarPerformanceConfigs() {
// Checar globals.css e layout.jsx para transições no body // Checar globals.css e layout.jsx para transições no body
const globalsCss = existsSync("app/globals.css") ? readFileSync("app/globals.css", "utf8") : ""; const globalsCss = existsSync("app/globals.css") ? readFileSync("app/globals.css", "utf8") : "";
const layoutJsx = existsSync("app/layout.jsx") ? readFileSync("app/layout.jsx", "utf8") : ""; const layoutPath = existsSync("app/[locale]/layout.jsx")
? "app/[locale]/layout.jsx"
: existsSync("app/layout.jsx")
? "app/layout.jsx"
: null;
const layoutJsx = layoutPath ? readFileSync(layoutPath, "utf8") : "";
if (globalsCss.includes("body {\n\t\t@apply bg-slate-50 text-slate-900 transition-colors")) { if (globalsCss.includes("body {\n\t\t@apply bg-slate-50 text-slate-900 transition-colors")) {
falha("globals.css tem transition-colors global no body (provoca animações não compostas no load)."); falha("globals.css tem transition-colors global no body (provoca animações não compostas no load).");
@@ -299,7 +305,8 @@ function auditarRotasInternas() {
if (caminho.startsWith("/api") || caminho.startsWith("/favicon") || caminho.startsWith("/icon") || caminho.startsWith("/logo") || caminho.startsWith("/site.webmanifest")) { if (caminho.startsWith("/api") || caminho.startsWith("/favicon") || caminho.startsWith("/icon") || caminho.startsWith("/logo") || caminho.startsWith("/site.webmanifest")) {
continue; continue;
} }
const existe = rotasValidas.has(caminho) || rotasDinamicas.some((rx) => rx.test(caminho)); const caminhoSemLocale = caminho.replace(/^\/(?:pt|en|es|fr|de|zh-CN)(?=\/|$)/, "") || "/";
const existe = rotasValidas.has(caminho) || rotasValidas.has(caminhoSemLocale) || rotasDinamicas.some((rx) => rx.test(caminho) || rx.test(caminhoSemLocale));
if (!existe) { if (!existe) {
falha(`${arq} -> Link interno aponta para rota inexistente: '${caminho}'`); falha(`${arq} -> Link interno aponta para rota inexistente: '${caminho}'`);
rotasQuebradas++; rotasQuebradas++;