import { negotiateLocale, isValidLocale, LOCALE_COOKIE_NAME } from '../i18n/index.js'; import { CONSENT_COOKIE, parseConsent } from '../consent/storage.js'; /** * First path segment of a URL pathname, or '' for root. * '/pl/o-nas' → 'pl', '/o-nas' → 'o-nas', '/' → ''. */ function firstSegment(pathname) { return pathname.split('/').filter(Boolean)[0] ?? ''; } /** * Builds locale-routing logic for Next.js middleware. * * Behavior: * - path already starts with a valid locale (/pl/...) → pass through * - any other path (/, /o-nas) → redirect to /{locale}{path}, where locale * comes from negotiateLocale (cookie → Accept-Language → default) * - the chosen locale is written to a cookie so the next visit is stable * * The plugin returns a decision; the thin middleware.ts in the client project * turns it into a NextResponse. This keeps all logic in the plugin while * respecting that middleware.ts must physically live in the client app. * * @example * // middleware.ts (client project) — one wiring file, no logic: * import { NextResponse } from 'next/server' * import { localeMiddleware } from './ipal.middleware' // created from this factory * export function proxy(req) { * const r = localeMiddleware(req) * // Both results may carry an optional cookie — 'next' when the visitor * // switched language (URL locale differs from the stored one) and consented, * // 'redirect' on the initial locale negotiation. Set it whenever present. * const res = r.type === 'next' ? NextResponse.next() : NextResponse.redirect(r.location) * if (r.cookie) res.cookies.set(r.cookie.name, r.cookie.value) * return res * } */ export function createLocaleMiddleware({ config, cookieName = LOCALE_COOKIE_NAME, consentCategory = 'functional', consentCookieName = CONSENT_COOKIE }) { // Whether the locale cookie may be written: 'necessary' is always granted; // 'functional' (default) requires the visitor to have consented. function mayPersistLocale(request) { if (consentCategory === 'necessary') return true; const consent = parseConsent(request.cookies.get(consentCookieName)?.value); return consent?.[consentCategory] === true; } return function localeMiddleware(request) { const { pathname } = request.nextUrl; // Single-locale sites have no /pl, /en prefix and no negotiation — one // language, no redirect. The middleware becomes a pass-through: paths stay // as-is (/o-nas), nothing to detect or persist. (Projects that are truly // single-locale usually don't even mount this middleware, but guarding here // makes it safe if they do.) if (config.locales.length === 1) { return { type: 'next' }; } // Already locale-prefixed (e.g. the visitor switched language by // navigating to /en). Routing is fine — but if the URL's locale differs // from the stored cookie, the visitor is *choosing* a language, and we // should remember it — provided they consented to the gating category. // Without consent we leave the cookie untouched: the switch works for this // visit but isn't persisted, which is exactly the functional-cookie rule. const urlLocale = firstSegment(pathname); if (isValidLocale(urlLocale, config)) { const currentCookie = request.cookies.get(cookieName)?.value ?? null; if (currentCookie !== urlLocale && mayPersistLocale(request)) { return { type: 'next', cookie: { name: cookieName, value: urlLocale } }; } return { type: 'next' }; } // Resolve the locale to use const locale = negotiateLocale({ cookieLocale: request.cookies.get(cookieName)?.value ?? null, acceptLanguage: request.headers.get('accept-language'), config }); // Redirect to the locale-prefixed path, preserving the rest const url = request.nextUrl.clone(); url.pathname = `/${locale}${pathname === '/' ? '' : pathname}`; // Persist the locale choice ONLY if the visitor consented to the gating // category. 'necessary' is always granted, so passing consentCategory: // 'necessary' always persists. For 'functional' (default), we read the // consent cookie and only write the locale cookie when functional is true. // Without consent the locale is still detected each request (routing works), // it just isn't remembered across visits — which is the whole point of // gating a functional cookie behind consent. return { type: 'redirect', location: url.toString(), ...mayPersistLocale(request) ? { cookie: { name: cookieName, value: locale } } : {} }; }; } /** * Default Next.js middleware matcher: run on everything except API routes, the * admin panel, Next internals, and files with an extension (static assets). */ export const DEFAULT_MIDDLEWARE_MATCHER = [ '/((?!api|admin|_next|.*\\..*).*)' ]; //# sourceMappingURL=localeMiddleware.js.map