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:
1 parent
e92aea0c84
commit
83c0ba3e74
38 files changed
+1699
-324
No files matched your search
+102
@@ -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).*)",
|
||||
],
|
||||
};
|
||||
Reference in new issue
Block a user