import { NextRequest, NextResponse } from 'next/server'; import { LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE, LOCALE_HEADER, isLocale, Locale } from '@/lib/locales'; /** * Locale-detection middleware (Session 12). * * Detection priority: * 1. URL prefix (/es/scan, /fr/pricing) — reserved for a future * session that does the [locale] segment refactor. For now this * branch is unreachable because no route uses the prefix. * 2. NEXT_LOCALE cookie — set by the locale switcher and persisted * across sessions. * 3. Accept-Language header — best guess from the browser. * 4. Default 'en'. * * The resolved locale lands on the `x-vyndr-locale` REQUEST header * (NOT the response) so downstream server components can read it via * `headers()` without parsing cookies themselves. The cookie is set * on the response when the locale switcher fires (separate code * path); the middleware itself doesn't write cookies. * * Skips: Next.js internals (`_next`), public files (anything with a * dot in the path), and API routes (they don't render UI, no locale * needed). */ function parseAcceptLanguage(header: string | null): Locale | null { if (!header) return null; // "en-US,en;q=0.9,es;q=0.8" → [{lang:'en-us', q:1}, {lang:'en', q:0.9}, ...] const ranked = header .split(',') .map((chunk) => { const [tag, ...params] = chunk.trim().split(';'); const qParam = params.find((p) => p.trim().startsWith('q=')); const q = qParam ? Number(qParam.split('=')[1]) : 1; return { tag: tag.toLowerCase(), q: Number.isFinite(q) ? q : 1 }; }) .filter((entry) => entry.tag) .sort((a, b) => b.q - a.q); for (const entry of ranked) { // Match the primary subtag (en-US → en). const primary = entry.tag.split('-')[0]; if (isLocale(primary)) return primary; } return null; } function resolveLocale(req: NextRequest): Locale { // 1. URL prefix — placeholder for the future [locale] refactor. // Check the first path segment against the locale registry. const firstSegment = req.nextUrl.pathname.split('/')[1] || ''; if (isLocale(firstSegment)) return firstSegment; // 2. Cookie. const cookie = req.cookies.get(LOCALE_COOKIE)?.value; if (isLocale(cookie)) return cookie; // 3. Accept-Language. const fromHeader = parseAcceptLanguage(req.headers.get('accept-language')); if (fromHeader) return fromHeader; // 4. Default. return DEFAULT_LOCALE; } export function middleware(req: NextRequest) { const locale = resolveLocale(req); // Stamp the request header so server components can read locale // via `headers().get('x-vyndr-locale')`. NextResponse.next() with // request headers is the canonical pattern for this. const requestHeaders = new Headers(req.headers); requestHeaders.set(LOCALE_HEADER, locale); return NextResponse.next({ request: { headers: requestHeaders }, }); } // Skip Next.js internals, public files, and API routes (no UI). export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'], }; // Re-export so tests can import without pulling the full middleware. export { resolveLocale, parseAcceptLanguage }; export const __SUPPORTED_LOCALES = LOCALES;