gate locale behind functional consent
This commit is contained in:
-3
@@ -1,3 +0,0 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=getLocalizedSlugs.d.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["getLocalizedSlugs.d.ts"],"sourcesContent":["import type { LocalizedSlugs } from './localizedPath.js';\nimport type { I18nConfig } from './types.js';\n/**\n * A localized field as Payload returns it when queried with `locale: 'all'`:\n * a map of locale code → value.\n */\ntype LocalizedField = Record<string, unknown>;\ntype GetLocalizedSlugsArgs = {\n config: I18nConfig;\n /**\n * The document's localized slug field, as returned by Payload with\n * `locale: 'all'` — e.g. { pl: 'strona-glowna', en: 'home' }.\n */\n slugField: LocalizedField | null | undefined;\n};\n/**\n * Normalizes a document's localized slug field into the LocalizedSlugs\n * contract consumed by buildLocalizedPath / switchLocalePath.\n *\n * Pure function — the template fetches the document (payload.findByID with\n * `locale: 'all'`, where the slug field must be `localized: true`) and passes\n * the raw field in. The plugin never touches the database.\n *\n * Keeps only entries whose locale is configured and whose slug is a\n * non-empty string, so callers get a clean, trustworthy map.\n *\n * @example\n * const doc = await payload.findByID({ collection: 'pages', id, locale: 'all' })\n * const slugs = getLocalizedSlugs({ slugField: doc.slug, config })\n * // → { pl: 'strona-glowna', en: 'home' }\n * switchLocalePath({ slugs, targetLocale: 'en', config }) // → '/en'\n */\nexport declare function getLocalizedSlugs({ config, slugField }: GetLocalizedSlugsArgs): LocalizedSlugs;\nexport {};\n"],"names":[],"mappings":"AAiCA,WAAU"}
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* Returns the full locale definition for a given code, or undefined if not found.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=helpers.d.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["helpers.d.ts"],"sourcesContent":["import type { I18nConfig, LocaleDefinition } from './types.js';\n/**\n * Returns all configured locale codes.\n */\nexport declare function getLocaleCodes(config: I18nConfig): string[];\n/**\n * Returns the default locale code.\n */\nexport declare function getDefaultLocale(config: I18nConfig): string;\n/**\n * Checks if a string is a valid configured locale code.\n */\nexport declare function isValidLocale(code: string, config: I18nConfig): boolean;\n/**\n * Returns the full locale definition for a given code, or undefined if not found.\n */\nexport declare function getLocaleDefinition(code: string, config: I18nConfig): LocaleDefinition | undefined;\n"],"names":[],"mappings":"AAaA;;CAEC,GACD,WAA4G"}
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
export { getLocalizedSlugs } from './getLocalizedSlugs.js';
|
||||
export { getDefaultLocale, getLocaleCodes, getLocaleDefinition, isValidLocale } from './helpers.js';
|
||||
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './localeMiddleware.js';
|
||||
export { buildLocalizationConfig } from './localizationConfig.js';
|
||||
export { buildLocalizedPath, switchLocalePath } from './localizedPath.js';
|
||||
export { LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale } from './negotiateLocale.js';
|
||||
export { validateI18nConfig } from './validation.js';
|
||||
|
||||
//# sourceMappingURL=index.d.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["index.d.ts"],"sourcesContent":["export { getLocalizedSlugs } from './getLocalizedSlugs.js';\nexport { getDefaultLocale, getLocaleCodes, getLocaleDefinition, isValidLocale } from './helpers.js';\nexport { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './localeMiddleware.js';\nexport type { LocaleMiddlewareResult } from './localeMiddleware.js';\nexport { buildLocalizationConfig } from './localizationConfig.js';\nexport { buildLocalizedPath, switchLocalePath } from './localizedPath.js';\nexport type { LocalizedSlugs } from './localizedPath.js';\nexport { LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale } from './negotiateLocale.js';\nexport type { I18nConfig, LocaleDefinition } from './types.js';\nexport { validateI18nConfig } from './validation.js';\n"],"names":["getLocalizedSlugs","getDefaultLocale","getLocaleCodes","getLocaleDefinition","isValidLocale","createLocaleMiddleware","DEFAULT_MIDDLEWARE_MATCHER","buildLocalizationConfig","buildLocalizedPath","switchLocalePath","LOCALE_COOKIE_NAME","matchAcceptLanguage","negotiateLocale","validateI18nConfig"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,yBAAyB;AAC3D,SAASC,gBAAgB,EAAEC,cAAc,EAAEC,mBAAmB,EAAEC,aAAa,QAAQ,eAAe;AACpG,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,wBAAwB;AAE3F,SAASC,uBAAuB,QAAQ,0BAA0B;AAClE,SAASC,kBAAkB,EAAEC,gBAAgB,QAAQ,qBAAqB;AAE1E,SAASC,kBAAkB,EAAEC,mBAAmB,EAAEC,eAAe,QAAQ,uBAAuB;AAEhG,SAASC,kBAAkB,QAAQ,kBAAkB"}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=localeMiddleware.d.js.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["localeMiddleware.d.ts"],"sourcesContent":["import type { I18nConfig } from './types.js';\n/**\n * Minimal request shape the middleware reads. Kept structural so the plugin\n * doesn't hard-depend on next/server types; a Next.js `NextRequest` satisfies it.\n */\ntype MiddlewareRequest = {\n cookies: {\n get: (name: string) => {\n value: string;\n } | undefined;\n };\n headers: {\n get: (name: string) => null | string;\n };\n nextUrl: {\n clone: () => URL;\n pathname: string;\n search: string;\n };\n url: string;\n};\n/**\n * What the factory returns — the caller (in Next next-middleware.ts) decides how to\n * act: `redirect` means send a 307 to `location` and set the locale cookie;\n * `next` means let the request pass through untouched.\n */\nexport type LocaleMiddlewareResult = {\n cookie: {\n name: string;\n value: string;\n };\n location: string;\n type: 'redirect';\n} | {\n type: 'next';\n};\ntype CreateLocaleMiddlewareArgs = {\n config: I18nConfig;\n /** Cookie name for the locale choice. Defaults to LOCALE_COOKIE_NAME. */\n cookieName?: string;\n};\n/**\n * Builds locale-routing logic for Next.js middleware.\n *\n * Behavior:\n * - path already starts with a valid locale (/pl/...) → pass through\n * - any other path (/, /o-nas) → redirect to /{locale}{path}, where locale\n * comes from negotiateLocale (cookie → Accept-Language → default)\n * - the chosen locale is written to a cookie so the next visit is stable\n *\n * The plugin returns a decision; the thin next-middleware.ts in the client project\n * turns it into a NextResponse. This keeps all logic in the plugin while\n * respecting that next-middleware.ts must physically live in the client app.\n *\n * @example\n * // next-middleware.ts (client project) — one wiring file, no logic:\n * import { NextResponse } from 'next/server'\n * import { localeMiddleware } from './ipal.middleware' // created from this factory\n * export function middleware(req) {\n * const r = localeMiddleware(req)\n * if (r.type === 'next') return NextResponse.next()\n * const res = NextResponse.redirect(r.location)\n * res.cookies.set(r.cookie.name, r.cookie.value)\n * return res\n * }\n */\nexport declare function createLocaleMiddleware({ config, cookieName, }: CreateLocaleMiddlewareArgs): (request: MiddlewareRequest) => LocaleMiddlewareResult;\n/**\n * Default Next.js middleware matcher: run on everything except API routes, the\n * admin panel, Next internals, and files with an extension (static assets).\n */\nexport declare const DEFAULT_MIDDLEWARE_MATCHER: string[];\nexport {};\n"],"names":[],"mappings":"AAwEA,WAAU"}
|
||||
+20
-8
@@ -20,12 +20,12 @@ type MiddlewareRequest = {
|
||||
url: string;
|
||||
};
|
||||
/**
|
||||
* What the factory returns — the caller (in Next next-middleware.ts) decides how to
|
||||
* What the factory returns — the caller (in Next middleware.ts) decides how to
|
||||
* act: `redirect` means send a 307 to `location` and set the locale cookie;
|
||||
* `next` means let the request pass through untouched.
|
||||
*/
|
||||
export type LocaleMiddlewareResult = {
|
||||
cookie: {
|
||||
cookie?: {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
@@ -36,6 +36,16 @@ export type LocaleMiddlewareResult = {
|
||||
};
|
||||
type CreateLocaleMiddlewareArgs = {
|
||||
config: I18nConfig;
|
||||
/**
|
||||
* Consent category that gates *persisting* the locale cookie. The locale is
|
||||
* always detected (routing works regardless), but the choice is only written
|
||||
* to a cookie once the visitor has consented to this category. Defaults to
|
||||
* 'functional'. Pass 'necessary' to always persist (treat locale as strictly
|
||||
* necessary), which restores the pre-consent behaviour.
|
||||
*/
|
||||
consentCategory?: 'functional' | 'necessary';
|
||||
/** Name of the consent cookie to read. Defaults to CONSENT_COOKIE. */
|
||||
consentCookieName?: string;
|
||||
/** Cookie name for the locale choice. Defaults to LOCALE_COOKIE_NAME. */
|
||||
cookieName?: string;
|
||||
};
|
||||
@@ -48,23 +58,25 @@ type CreateLocaleMiddlewareArgs = {
|
||||
* 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 next-middleware.ts in the client project
|
||||
* 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 next-middleware.ts must physically live in the client app.
|
||||
* respecting that middleware.ts must physically live in the client app.
|
||||
*
|
||||
* @example
|
||||
* // next-middleware.ts (client project) — one wiring file, no logic:
|
||||
* // 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 middleware(req) {
|
||||
* export function proxy(req) {
|
||||
* const r = localeMiddleware(req)
|
||||
* if (r.type === 'next') return NextResponse.next()
|
||||
* const res = NextResponse.redirect(r.location)
|
||||
* res.cookies.set(r.cookie.name, r.cookie.value)
|
||||
* // cookie is optional: only present when the visitor consented to the
|
||||
* // gating category (functional by default). Guard before setting.
|
||||
* if (r.cookie) res.cookies.set(r.cookie.name, r.cookie.value)
|
||||
* return res
|
||||
* }
|
||||
*/
|
||||
export declare function createLocaleMiddleware({ config, cookieName, }: CreateLocaleMiddlewareArgs): (request: MiddlewareRequest) => LocaleMiddlewareResult;
|
||||
export declare function createLocaleMiddleware({ config, consentCategory, consentCookieName, cookieName, }: CreateLocaleMiddlewareArgs): (request: MiddlewareRequest) => LocaleMiddlewareResult;
|
||||
/**
|
||||
* Default Next.js middleware matcher: run on everything except API routes, the
|
||||
* admin panel, Next internals, and files with an extension (static assets).
|
||||
|
||||
+28
-11
@@ -1,3 +1,4 @@
|
||||
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.
|
||||
@@ -14,22 +15,24 @@ import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/inde
|
||||
* 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 next-middleware.ts in the client project
|
||||
* 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 next-middleware.ts must physically live in the client app.
|
||||
* respecting that middleware.ts must physically live in the client app.
|
||||
*
|
||||
* @example
|
||||
* // next-middleware.ts (client project) — one wiring file, no logic:
|
||||
* // 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 middleware(req) {
|
||||
* export function proxy(req) {
|
||||
* const r = localeMiddleware(req)
|
||||
* if (r.type === 'next') return NextResponse.next()
|
||||
* const res = NextResponse.redirect(r.location)
|
||||
* res.cookies.set(r.cookie.name, r.cookie.value)
|
||||
* // cookie is optional: only present when the visitor consented to the
|
||||
* // gating category (functional by default). Guard before setting.
|
||||
* if (r.cookie) res.cookies.set(r.cookie.name, r.cookie.value)
|
||||
* return res
|
||||
* }
|
||||
*/ export function createLocaleMiddleware({ config, cookieName = LOCALE_COOKIE_NAME }) {
|
||||
*/ export function createLocaleMiddleware({ config, consentCategory = 'functional', consentCookieName = CONSENT_COOKIE, cookieName = LOCALE_COOKIE_NAME }) {
|
||||
return function localeMiddleware(request) {
|
||||
const { pathname } = request.nextUrl;
|
||||
// Already locale-prefixed → nothing to do
|
||||
@@ -47,13 +50,27 @@ import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/inde
|
||||
// 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.
|
||||
let mayPersist = consentCategory === 'necessary';
|
||||
if (!mayPersist) {
|
||||
const consent = parseConsent(request.cookies.get(consentCookieName)?.value);
|
||||
mayPersist = consent?.[consentCategory] === true;
|
||||
}
|
||||
return {
|
||||
type: 'redirect',
|
||||
cookie: {
|
||||
name: cookieName,
|
||||
value: locale
|
||||
},
|
||||
location: url.toString()
|
||||
location: url.toString(),
|
||||
...mayPersist ? {
|
||||
cookie: {
|
||||
name: cookieName,
|
||||
value: locale
|
||||
}
|
||||
} : {}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
-3
@@ -1,3 +0,0 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=localizationConfig.d.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["localizationConfig.d.ts"],"sourcesContent":["import type { Config } from 'payload';\nimport type { I18nConfig } from './types.js';\ntype PayloadLocalizationConfig = NonNullable<Config['localization']>;\n/**\n * Transforms validated i18n config into Payload's localization object.\n * Pure function — no side effects, no validation (caller validates first).\n */\nexport declare function buildLocalizationConfig(config: I18nConfig): PayloadLocalizationConfig;\nexport {};\n"],"names":[],"mappings":"AAQA,WAAU"}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=localizedPath.d.js.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["localizedPath.d.ts"],"sourcesContent":["import type { I18nConfig } from './types.js';\n/**\n * Minimal shape the path builder needs from a document.\n *\n * The plugin doesn't know the client's Pages type, so it depends only on\n * this contract: a map of locale code → slug for that locale. The template\n * supplies it (e.g. by reading the localized slug field across locales).\n */\nexport type LocalizedSlugs = Record<string, string>;\ntype BuildPathArgs = {\n config: I18nConfig;\n /**\n * Slug that represents the site root (served at /{locale} with no trailing\n * segment). Defaults to 'home'. Matched against the slug in the target locale.\n */\n homeSlug?: string;\n /** Target locale to build the path for */\n locale: string;\n /**\n * Localized segment the document lives under, e.g.\n * `{ pl: 'artykuly', en: 'articles' }` → /pl/artykuly/moj-post.\n *\n * These are the slugs of the collection's archive page, so the prefix is\n * whatever an editor named that page — and it differs per locale for free.\n * A document under a prefix is never the home page, so homeSlug is ignored.\n */\n prefix?: LocalizedSlugs;\n /** slug per locale, e.g. { pl: 'strona-glowna', en: 'home' } */\n slugs: LocalizedSlugs;\n};\n/**\n * Builds a locale-prefixed path for a document in a target locale.\n *\n * Always prefixes the locale: /{locale} or /{locale}/{slug}. The home slug\n * collapses to the locale root. Returns undefined if the document has no slug\n * in the target locale (caller decides fallback behavior).\n *\n * @example\n * buildLocalizedPath({ slugs: { pl: 'strona-glowna', en: 'home' }, locale: 'en', config })\n * // → '/en' (home slug collapses to root)\n *\n * buildLocalizedPath({ slugs: { pl: 'o-nas', en: 'about' }, locale: 'en', config })\n * // → '/en/about'\n *\n * buildLocalizedPath({\n * slugs: { pl: 'moj-post', en: 'my-post' },\n * prefix: { pl: 'artykuly', en: 'articles' },\n * locale: 'en',\n * config,\n * })\n * // → '/en/articles/my-post'\n */\nexport declare function buildLocalizedPath({ config, homeSlug, locale, prefix, slugs, }: BuildPathArgs): string | undefined;\ntype SwitchLocaleArgs = {\n config: I18nConfig;\n homeSlug?: string;\n prefix?: LocalizedSlugs;\n slugs: LocalizedSlugs;\n targetLocale: string;\n};\n/**\n * Resolves the equivalent path for the same document in a different locale —\n * the language-switcher use case (/pl/strona-glowna → /en/home).\n *\n * Never dead-ends on a 404. When the document has no slug in the target locale,\n * falls back to the archive it belongs to (/en/articles) if there is one, and\n * to the locale root otherwise — the closest place the visitor would want.\n */\nexport declare function switchLocalePath({ config, homeSlug, prefix, slugs, targetLocale, }: SwitchLocaleArgs): string;\nexport {};\n"],"names":[],"mappings":"AAqEA,WAAU"}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=negotiateLocale.d.js.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["negotiateLocale.d.ts"],"sourcesContent":["import type { I18nConfig } from './types.js';\n/** Cookie name the template uses to persist a visitor's locale choice. */\nexport declare const LOCALE_COOKIE_NAME = \"ipal-locale\";\ntype NegotiateLocaleArgs = {\n /** Raw Accept-Language header value */\n acceptLanguage?: null | string;\n config: I18nConfig;\n /** Value of the locale cookie, if present (from LOCALE_COOKIE_NAME) */\n cookieLocale?: null | string;\n};\n/**\n * Resolves which locale to serve, in priority order:\n * 1. Cookie (explicit prior choice)\n * 2. Accept-Language header (best match against configured locales)\n * 3. Configured default locale\n *\n * Pure function — the template feeds it request data and acts on the result\n * (redirect, cookie set). No Next.js or request objects here.\n */\nexport declare function negotiateLocale({ acceptLanguage, config, cookieLocale, }: NegotiateLocaleArgs): string;\n/**\n * Parses an Accept-Language header and returns the best-matching configured\n * locale, or undefined if none match.\n *\n * Matches on the primary subtag (e.g. \"en-US\" matches configured \"en\"),\n * respecting the header's quality-value ordering.\n */\nexport declare function matchAcceptLanguage(acceptLanguage: null | string | undefined, config: I18nConfig): string | undefined;\nexport {};\n"],"names":[],"mappings":"AA4BA,WAAU"}
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Single locale definition provided by the client project.
|
||||
*/ /**
|
||||
* Full i18n configuration passed through plugin options.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=types.d.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["types.d.ts"],"sourcesContent":["/**\n * Single locale definition provided by the client project.\n */\nexport type LocaleDefinition = {\n /** BCP-47 language code, e.g. 'pl', 'en', 'de' */\n code: string;\n /** Human-readable label, e.g. 'Polski', 'English' */\n label: string;\n /** Right-to-left script (defaults to false) */\n rtl?: boolean;\n};\n/**\n * Full i18n configuration passed through plugin options.\n */\nexport type I18nConfig = {\n defaultLocale: string;\n /** Enable locale fallback when content is missing (defaults to true) */\n fallback?: boolean;\n locales: [LocaleDefinition, ...LocaleDefinition[]];\n};\n"],"names":[],"mappings":"AAAA;;CAEC,GASD;;CAEC,GACD,WAKE"}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Validates i18n config at plugin initialization.
|
||||
* Throws descriptive errors — fail fast, no silent defaults.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=validation.d.js.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["validation.d.ts"],"sourcesContent":["import type { I18nConfig } from './types.js';\n/**\n * Validates i18n config at plugin initialization.\n * Throws descriptive errors — fail fast, no silent defaults.\n */\nexport declare function validateI18nConfig(config: I18nConfig): void;\n"],"names":[],"mappings":"AACA;;;CAGC,GACD,WAAqE"}
|
||||
Reference in New Issue
Block a user