Files
ipal-kit/dist/modules/i18n/localeMiddleware.js
T

104 lines
4.8 KiB
JavaScript

import { CONSENT_COOKIE, parseConsent } from '../consent/storage.js';
import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/index.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, consentCategory = 'functional', consentCookieName = CONSENT_COOKIE, cookieName = LOCALE_COOKIE_NAME }) {
// 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;
// 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({
acceptLanguage: request.headers.get('accept-language'),
config,
cookieLocale: request.cookies.get(cookieName)?.value ?? null
});
// 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