@fluenti/next 0.4.0-rc.0 → 0.4.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-navigation.d.ts +34 -0
- package/dist/create-navigation.d.ts.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +31 -27
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +1 -1
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.d.ts +109 -75
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +141 -23
- package/dist/middleware.js.map +1 -1
- package/dist/navigation.cjs +1 -1
- package/dist/navigation.cjs.map +1 -1
- package/dist/navigation.d.ts +5 -2
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +79 -8
- package/dist/navigation.js.map +1 -1
- package/dist/read-config.d.ts.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/with-fluenti.d.ts +4 -6
- package/dist/with-fluenti.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/middleware.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@fluenti/next/i18n-config`);var t=`x-fluenti-locale`;function n(n){let{NextResponse:
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@fluenti/next/i18n-config`);var t=`x-fluenti-locale`;function n(n){let{NextResponse:c}=n,l=n.locales??e.locales,u=n.sourceLocale??e.sourceLocale,h=n.cookieName??e.cookieName,g=n.localePrefix??`as-needed`,_=n.rewriteDefaultLocale??!1,v=n.setCookie??!1,y=n.cookieOptions??{},b=n.localeDetection??!0,x=n.alternateLinks??!1,S=n.pathnames,C=n.domains,w=n.beforeResponse;function T(e,r,i,o,s){if(e.headers.set(t,i),v&&a(e,r,i,h,s,y),x||n.getAlternateLinks){let t=n.getAlternateLinks?m(n.getAlternateLinks,r,i,l):p(r,l,u,g,r.nextUrl.basePath??``,S);e.headers.set(`Link`,t)}if(w){let t=w({response:e,request:r,locale:i,type:o});if(t)return t}return e}return function(e){let a=l,p=u,{pathname:m,search:v}=e.nextUrl,y=e.nextUrl.basePath??``,x=m.split(`/`),w=x[1]??``,E=g===`never`?null:r(w,a),D;if(E)D=E;else if(!b)D=p;else{let t=n.detectLocale?.(e);D=t!==void 0&&r(t,a)!==null?r(t,a):C?o(e,C,a)??s(e,a,p,h):s(e,a,p,h)}let O=new Headers(e.headers);if(O.set(t,D),g===`never`){if(_){let t=new URL(`${y}/${D}${m}${v}`,e.url);return T(c.rewrite(t,{request:{headers:O}}),e,D,`rewrite`,null)}return T(c.next({request:{headers:O}}),e,D,`next`,null)}if(S&&E){let t=i(`/`+x.slice(2).join(`/`)),n=d(t,D,S);if(n){let t=new URL(`${y}/${D}${n}${v}`,e.url);return T(c.rewrite(t,{request:{headers:O}}),e,D,`rewrite`,E)}let r=f(t,D,S);if(r&&r!==t){let t=new URL(`${y}/${D}${r}${v}`,e.url);return T(c.redirect(t),e,D,`redirect`,E)}}if(!E&&(g===`always`||D!==p)){let t=m;if(S){let e=f(m,D,S);e&&(t=e)}let n=new URL(`${y}/${D}${t}${v}`,e.url);return T(c.redirect(n),e,D,`redirect`,E)}if(!E&&D===p&&_){let t=new URL(`${y}/${p}${m}${v}`,e.url);return T(c.rewrite(t,{request:{headers:O}}),e,D,`rewrite`,E)}if(g===`as-needed`&&E===p){let t=i(`/`+x.slice(2).join(`/`));if(_){let n=new URL(`${y}${t}${v}`,e.url);return T(c.redirect(n),e,D,`redirect`,E)}let n=new URL(`${y}${t}${v}`,e.url);return T(c.rewrite(n,{request:{headers:O}}),e,D,`rewrite`,E)}return T(c.next({request:{headers:O}}),e,D,`next`,E)}}function r(e,t){if(!e)return null;let n=e.toLowerCase();return t.find(e=>e.toLowerCase()===n)??null}function i(e){return e.replace(/\/+/g,`/`)||`/`}function a(e,t,n,r,i,a){if(i||t.cookies.get(r)?.value===n)return;let o=[`${r}=${encodeURIComponent(n)}`];o.push(`path=${a.path??`/`}`),o.push(`max-age=${a.maxAge??31536e3}`),o.push(`samesite=${a.sameSite??`lax`}`),a.domain&&o.push(`domain=${a.domain}`),(a.secure??t.url.startsWith(`https`))&&o.push(`secure`),e.headers.set(`set-cookie`,o.join(`;`))}function o(e,t,n){let i=e.headers.get(`host`)?.split(`:`)[0]??``;for(let e of t)if(i===e.domain||i.endsWith(`.`+e.domain)){let t=r(e.defaultLocale,e.locales??n);if(t)return t}return null}function s(e,t,n,i){let a=e.cookies.get(i)?.value;if(a){let e=r(a,t);if(e)return e}let o=e.headers.get(`accept-language`);if(o)for(let e of o.split(`,`)){let n=e.split(`;`)[0].trim(),i=r(n,t);if(i)return i;let a=n.split(`-`)[0].toLowerCase(),o=t.find(e=>{let t=e.toLowerCase();return t===a||t.startsWith(a+`-`)});if(o)return o}return n}function c(e,t){let n=e.split(`/`);return r(n[1]??``,t)?i(`/`+n.slice(2).join(`/`)):e}function l(e,t){let n=e.split(`/`).filter(Boolean),r=t.split(`/`).filter(Boolean),i={};for(let e=0;e<n.length;e++){let t=n[e];if(t.startsWith(`[...`)&&t.endsWith(`]`)){let n=t.slice(4,-1);return i[n]=r.slice(e).join(`/`),i}if(t.startsWith(`[`)&&t.endsWith(`]`)){if(e>=r.length)return null;i[t.slice(1,-1)]=r[e];continue}if(e>=r.length||t!==r[e])return null}return n.length===r.length?i:null}function u(e,t){return e.replace(/\[\.\.\.(\w+)\]|\[(\w+)\]/g,(e,n,r)=>t[n??r]??``)}function d(e,t,n){for(let[r,i]of Object.entries(n)){let n=i[t];if(!n)continue;if(n===e)return r;let a=l(n,e);if(a)return u(r,a)}return null}function f(e,t,n){for(let[r,i]of Object.entries(n)){let n=i[t];if(!n)continue;if(r===e)return n;let a=l(r,e);if(a)return u(n,a)}return null}function p(e,t,n,r,i,a){let o=new URL(e.url).origin,s=c(e.nextUrl.pathname,t),l=t.map(e=>{let t;if(t=r===`never`||r===`as-needed`&&e===n?s:`/${e}${s}`,a){let i=f(s,e,a);i&&(t=r===`never`||r===`as-needed`&&e===n?i:`/${e}${i}`)}return`<${o}${i}${t}>; rel="alternate"; hreflang="${e}"`}),u=r===`always`?`/${n}${s}`:s;return l.push(`<${o}${i}${u}>; rel="alternate"; hreflang="x-default"`),l.join(`, `)}function m(e,t,n,r){let i=new URL(t.url).origin;return e({pathname:c(t.nextUrl.pathname,r),locale:n,locales:r,origin:i,basePath:t.nextUrl.basePath??``}).map(e=>`<${e.href}>; rel="alternate"; hreflang="${e.hreflang}"`).join(`, `)}exports.LOCALE_HEADER=t,exports.createI18nMiddleware=n;
|
|
2
2
|
//# sourceMappingURL=middleware.cjs.map
|
package/dist/middleware.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.cjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * @module @fluenti/next/middleware\n *\n * Built-in i18n middleware for Next.js App Router.\n *\n * Uses `x-fluenti-locale` request header to pass locale from middleware to\n * server components — avoids `Set-Cookie` on every request (CDN-friendly).\n *\n * Cookie is only used to remember user preference (set by LocaleSwitcher).\n *\n * @example Minimal — locales/sourceLocale/cookieName auto-read from fluenti.config.ts\n * ```ts\n * // src/middleware.ts\n * import { NextResponse } from 'next/server'\n * import { createI18nMiddleware } from '@fluenti/next/middleware'\n *\n * export default createI18nMiddleware({ NextResponse })\n *\n * export const config = {\n * matcher: ['/((?!_next|api|favicon).*)'],\n * }\n * ```\n *\n * @example With app/[locale]/ directory structure (rewriteDefaultLocale)\n * ```ts\n * import { NextResponse } from 'next/server'\n * import { createI18nMiddleware } from '@fluenti/next/middleware'\n *\n * export default createI18nMiddleware({ NextResponse, rewriteDefaultLocale: true })\n * ```\n *\n * @example Composing with Clerk\n * ```ts\n * import { NextResponse } from 'next/server'\n * import { clerkMiddleware } from '@clerk/nextjs/server'\n * import { createI18nMiddleware } from '@fluenti/next/middleware'\n *\n * const i18nMiddleware = createI18nMiddleware({ NextResponse })\n *\n * export default clerkMiddleware(async (auth, req) => {\n * await auth.protect()\n * return i18nMiddleware(req)\n * })\n * ```\n */\n\n// Auto-generated config values are resolved at Next.js build time via withFluenti's\n// webpack/Turbopack alias: @fluenti/next/i18n-config → .fluenti/i18n-config.js\n// This import is kept external (not bundled by tsup) so the alias can take effect.\nimport {\n locales as _configLocales,\n sourceLocale as _configSourceLocale,\n cookieName as _configCookieName,\n} from '@fluenti/next/i18n-config'\n\n/** Header name used to pass resolved locale from middleware to RSC */\nexport const LOCALE_HEADER = 'x-fluenti-locale'\n\nexport interface I18nMiddlewareConfig {\n /** Available locales. If omitted, reads from `fluenti.config.ts`. */\n locales?: string[]\n /** Source/default locale. If omitted, reads from `fluenti.config.ts`. */\n sourceLocale?: string\n /** Cookie name for reading user preference (default: 'locale') */\n cookieName?: string\n /**\n * Locale prefix strategy:\n * - `'always'`: all locales get a URL prefix (e.g. `/en/about`, `/fr/about`)\n * - `'as-needed'`: source locale has no prefix, others do (e.g. `/about`, `/fr/about`)\n *\n * Default: `'as-needed'`\n */\n localePrefix?: 'always' | 'as-needed'\n /**\n * When true, bare paths for the source locale are internally rewritten to include\n * the source locale prefix. Required when using `app/[locale]/` directory structure\n * with `localePrefix: 'as-needed'`.\n *\n * Also changes the handling of explicit source-locale URLs (e.g. `/en/about`):\n * instead of rewriting to `/about`, they are **redirected** to `/about` so the\n * browser follows the canonical URL, which is then rewritten internally.\n *\n * Example: `GET /about` → internally rewritten to `/en/about` (URL stays `/about`)\n * Example: `GET /en/about` → 302 redirect → `/about` → rewritten to `/en/about`\n *\n * Default: `false`\n */\n rewriteDefaultLocale?: boolean\n /**\n * When true, the detected locale is written to a `Set-Cookie` response header so the\n * preference is persisted across requests (useful when locale is detected from\n * `Accept-Language` rather than an existing cookie).\n *\n * Disabled by default to keep responses CDN-cacheable.\n *\n * The cookie is only written when the locale was **detected** (not read from the URL\n * path), and only when it differs from the existing cookie value.\n */\n setCookie?: boolean\n /**\n * Custom locale detection function. Called when no locale is present in the URL path.\n * Return a locale string to override the default cookie → Accept-Language → default chain.\n * Return `undefined` to fall through to built-in detection.\n *\n * Useful for JWT-based preferences, subdomain detection, or any custom logic.\n *\n * @example Subdomain detection\n * ```ts\n * detectLocale: (req) => {\n * const host = req.headers.get('host') ?? ''\n * if (host.startsWith('fr.')) return 'fr'\n * }\n * ```\n *\n * @example JWT claim\n * ```ts\n * detectLocale: (req) => {\n * const token = req.cookies.get('auth')?.value\n * return token ? parseLocaleFromJwt(token) : undefined\n * }\n * ```\n */\n detectLocale?: (req: NextRequest) => string | undefined\n}\n\ntype NextRequest = {\n nextUrl: { pathname: string; search: string; basePath?: string }\n url: string\n cookies: { get(name: string): { value: string } | undefined }\n headers: Headers\n}\n\ntype NextResponseStatic = {\n redirect(url: URL): NextResponseInstance\n rewrite(url: URL, init?: Record<string, unknown>): NextResponseInstance\n next(init?: Record<string, unknown>): NextResponseInstance\n}\n\ntype NextResponseInstance = {\n headers: { set(name: string, value: string): void }\n}\n\n/**\n * Create an i18n middleware function for Next.js.\n *\n * Requires `NextResponse` to be passed in because the middleware module runs\n * in Next.js Edge Runtime where `require('next/server')` is not available.\n *\n * @example\n * ```ts\n * import { NextResponse } from 'next/server'\n * import { createI18nMiddleware } from '@fluenti/next/middleware'\n *\n * export default createI18nMiddleware({ NextResponse })\n * ```\n */\nexport function createI18nMiddleware(config: I18nMiddlewareConfig & { NextResponse: NextResponseStatic }) {\n const { NextResponse } = config\n const resolvedLocales: string[] = config.locales ?? _configLocales\n const resolvedSourceLocale: string = config.sourceLocale ?? _configSourceLocale\n const cookieName = config.cookieName ?? _configCookieName\n const localePrefix = config.localePrefix ?? 'as-needed'\n const rewriteDefaultLocale = config.rewriteDefaultLocale ?? false\n const setCookieEnabled = config.setCookie ?? false\n\n return function i18nMiddleware(request: NextRequest) {\n const locales = resolvedLocales\n const sourceLocale = resolvedSourceLocale\n const { pathname, search } = request.nextUrl\n const basePath = request.nextUrl.basePath ?? ''\n\n // Extract locale from URL path (case-insensitive — /ZH-CN → zh-CN)\n // Note: request.nextUrl.pathname already strips basePath (Next.js behavior)\n const segments = pathname.split('/')\n const firstSegment = segments[1] ?? ''\n const pathLocale = findLocale(firstSegment, locales)\n\n // Determine the active locale\n let locale: string\n\n if (pathLocale) {\n locale = pathLocale\n } else {\n // No locale in path — try custom detection first, then cookie → Accept-Language → default\n const custom = config.detectLocale?.(request)\n locale =\n custom !== undefined && findLocale(custom, locales) !== null\n ? findLocale(custom, locales)!\n : detectLocale(request, locales, sourceLocale, cookieName)\n }\n\n // Build new request headers preserving originals (auth headers, etc.)\n const requestHeaders = new Headers(request.headers)\n requestHeaders.set(LOCALE_HEADER, locale)\n\n // Case 1: No locale in path → redirect to /{locale}{path}\n // In 'always' mode: redirect all bare paths (including source locale)\n // In 'as-needed' mode: only redirect non-source locales\n if (!pathLocale && (localePrefix === 'always' || locale !== sourceLocale)) {\n const redirectUrl = new URL(\n `${basePath}/${locale}${pathname}${search}`,\n request.url,\n )\n const response = NextResponse.redirect(redirectUrl)\n response.headers.set(LOCALE_HEADER, locale)\n maybeSetCookie(response, request, locale, cookieName, setCookieEnabled, pathLocale)\n return response\n }\n\n // Case 2: as-needed + no prefix + source locale + rewriteDefaultLocale\n // → internally rewrite to /{sourceLocale}{path} so Next.js matches [locale] segment\n // Use `pathname` directly (preserves trailing slash for trailingSlash:true compat)\n if (!pathLocale && locale === sourceLocale && rewriteDefaultLocale) {\n const rewriteUrl = new URL(\n `${basePath}/${sourceLocale}${pathname}${search}`,\n request.url,\n )\n const response = NextResponse.rewrite(rewriteUrl, {\n request: { headers: requestHeaders },\n })\n response.headers.set(LOCALE_HEADER, locale)\n maybeSetCookie(response, request, locale, cookieName, setCookieEnabled, pathLocale)\n return response\n }\n\n // Case 3: as-needed mode, source locale has explicit prefix → strip it\n // rewriteDefaultLocale=false: rewrite to /about (flat app/about/ structure)\n // rewriteDefaultLocale=true: redirect to /about (browser re-requests, Case 2 rewrites)\n if (localePrefix === 'as-needed' && pathLocale === sourceLocale) {\n const pathWithoutLocale = ('/' + segments.slice(2).join('/')).replace(/\\/+/g, '/')\n if (rewriteDefaultLocale) {\n const redirectUrl = new URL(\n `${basePath}${pathWithoutLocale}${search}`,\n request.url,\n )\n const response = NextResponse.redirect(redirectUrl)\n response.headers.set(LOCALE_HEADER, locale)\n return response\n }\n const rewriteUrl = new URL(\n `${basePath}${pathWithoutLocale}${search}`,\n request.url,\n )\n const response = NextResponse.rewrite(rewriteUrl, {\n request: { headers: requestHeaders },\n })\n response.headers.set(LOCALE_HEADER, locale)\n return response\n }\n\n // Case 4: No prefix + source locale (rewriteDefaultLocale=false) → pass through\n // Case 5: Non-source locale with correct prefix → pass through\n const response = NextResponse.next({\n request: { headers: requestHeaders },\n })\n response.headers.set(LOCALE_HEADER, locale)\n maybeSetCookie(response, request, locale, cookieName, setCookieEnabled, pathLocale)\n\n return response\n }\n}\n\n/**\n * Case-insensitive locale lookup. Returns the canonical form from the locales array,\n * or null if not found. Handles BCP 47 case variance (e.g. zh-cn → zh-CN).\n */\nfunction findLocale(candidate: string, locales: string[]): string | null {\n if (!candidate) return null\n const lower = candidate.toLowerCase()\n return locales.find(l => l.toLowerCase() === lower) ?? null\n}\n\n/**\n * Conditionally add a Set-Cookie header to persist the detected locale.\n * Only writes when setCookie is enabled, locale was not from URL path,\n * and cookie value differs from detected locale.\n */\nfunction maybeSetCookie(\n response: NextResponseInstance,\n request: NextRequest,\n locale: string,\n cookieName: string,\n setCookie: boolean,\n pathLocale: string | null,\n): void {\n if (!setCookie || pathLocale) return\n if (request.cookies.get(cookieName)?.value === locale) return\n response.headers.set(\n 'set-cookie',\n `${cookieName}=${encodeURIComponent(locale)};path=/;max-age=31536000;samesite=lax`,\n )\n}\n\n/**\n * Detect locale from request: cookie → Accept-Language → default.\n * All comparisons are case-insensitive (BCP 47).\n */\nfunction detectLocale(\n request: NextRequest,\n locales: string[],\n defaultLocale: string,\n cookieName: string,\n): string {\n // 1. Cookie (user preference) — case-insensitive\n const cookieLocale = request.cookies.get(cookieName)?.value\n if (cookieLocale) {\n const found = findLocale(cookieLocale, locales)\n if (found) return found\n }\n\n // 2. Accept-Language header — case-insensitive\n const acceptLang = request.headers.get('accept-language')\n if (acceptLang) {\n for (const part of acceptLang.split(',')) {\n const lang = part.split(';')[0]!.trim()\n const exact = findLocale(lang, locales)\n if (exact) return exact\n const prefix = lang.split('-')[0]!.toLowerCase()\n const match = locales.find(l => {\n const ll = l.toLowerCase()\n return ll === prefix || ll.startsWith(prefix + '-')\n })\n if (match) return match\n }\n }\n\n // 3. Default\n return defaultLocale\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"middleware.cjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * @module @fluenti/next/middleware\n *\n * Built-in i18n middleware for Next.js App Router.\n *\n * Uses `x-fluenti-locale` request header to pass locale from middleware to\n * server components — avoids `Set-Cookie` on every request (CDN-friendly).\n *\n * @example Minimal\n * ```ts\n * import { NextResponse } from 'next/server'\n * import { createI18nMiddleware } from '@fluenti/next/middleware'\n *\n * export default createI18nMiddleware({ NextResponse })\n * ```\n *\n * @example With pathnames + alternateLinks + domains\n * ```ts\n * import { NextResponse } from 'next/server'\n * import { createI18nMiddleware } from '@fluenti/next/middleware'\n *\n * export default createI18nMiddleware({\n * NextResponse,\n * rewriteDefaultLocale: true,\n * alternateLinks: true,\n * pathnames: {\n * '/about': { fr: '/a-propos' },\n * '/blog/[slug]': { fr: '/articles/[slug]' },\n * },\n * domains: [\n * { domain: 'fr.example.com', defaultLocale: 'fr' },\n * ],\n * })\n * ```\n */\n\nimport {\n locales as _configLocales,\n sourceLocale as _configSourceLocale,\n cookieName as _configCookieName,\n} from '@fluenti/next/i18n-config'\n\n/** Header name used to pass resolved locale from middleware to RSC */\nexport const LOCALE_HEADER = 'x-fluenti-locale'\n\nexport interface CookieOptions {\n /** Cookie domain (e.g. '.example.com' for cross-subdomain) */\n domain?: string\n /** Secure flag (default: auto-detect from request URL) */\n secure?: boolean\n /** SameSite attribute (default: 'lax') */\n sameSite?: 'lax' | 'strict' | 'none'\n /** Max age in seconds (default: 31536000 = 1 year) */\n maxAge?: number\n /** Cookie path (default: '/') */\n path?: string\n}\n\nexport interface DomainConfig {\n /** Domain hostname (e.g. 'fr.example.com') */\n domain: string\n /** Default locale for this domain */\n defaultLocale: string\n /** Optional subset of locales available on this domain */\n locales?: string[]\n}\n\nexport interface AlternateLinkEntry {\n href: string\n hreflang: string\n}\n\nexport interface I18nMiddlewareConfig {\n /** Available locales. If omitted, reads from `fluenti.config.ts`. */\n locales?: string[]\n /** Source/default locale. If omitted, reads from `fluenti.config.ts`. */\n sourceLocale?: string\n /** Cookie name for reading user preference (default: 'locale') */\n cookieName?: string\n /**\n * Locale prefix strategy:\n * - `'always'`: all locales get a URL prefix\n * - `'as-needed'`: source locale has no prefix, others do\n * - `'never'`: no locale prefix in URLs; locale determined by detection chain\n *\n * Default: `'as-needed'`\n */\n localePrefix?: 'always' | 'as-needed' | 'never'\n /**\n * When true, bare paths are internally rewritten to include the locale prefix.\n * Required when using `app/[locale]/` directory structure.\n *\n * Default: `false`\n */\n rewriteDefaultLocale?: boolean\n /**\n * When true, detected locale is persisted in a cookie.\n * Disabled by default to keep responses CDN-cacheable.\n */\n setCookie?: boolean\n /** Fine-grained cookie configuration for multi-domain / secure deployments. */\n cookieOptions?: CookieOptions\n /**\n * Set to false to disable automatic locale detection.\n * Bare paths will always use `sourceLocale` instead of detecting from cookie / Accept-Language.\n *\n * Default: `true`\n */\n localeDetection?: boolean\n /**\n * Custom locale detection function. Called when no locale is present in the URL path.\n * Return a locale string to override the default chain, or `undefined` to fall through.\n */\n detectLocale?: (req: NextRequest) => string | undefined\n /**\n * Domain-based locale routing. Each domain maps to a default locale.\n * Domain matching is checked before cookie/Accept-Language detection.\n *\n * @example\n * ```ts\n * domains: [\n * { domain: 'fr.example.com', defaultLocale: 'fr' },\n * { domain: 'example.co.jp', defaultLocale: 'ja' },\n * ]\n * ```\n */\n domains?: DomainConfig[]\n /**\n * Map internal paths to localized paths per locale.\n * Supports dynamic segments: `[param]` and `[...slug]`.\n *\n * @example\n * ```ts\n * pathnames: {\n * '/about': { fr: '/a-propos' },\n * '/blog/[slug]': { fr: '/articles/[slug]' },\n * }\n * ```\n */\n pathnames?: Record<string, Record<string, string>>\n /**\n * When true, adds `Link` response headers with `rel=\"alternate\"` hreflang\n * and `rel=\"canonical\"` for SEO.\n *\n * Default: `false`\n */\n alternateLinks?: boolean\n /**\n * Custom function to build alternate link entries. Overrides default `alternateLinks` behavior.\n * Return an array of `{ href, hreflang }` entries.\n */\n getAlternateLinks?: (context: {\n pathname: string\n locale: string\n locales: string[]\n origin: string\n basePath: string\n }) => AlternateLinkEntry[]\n /**\n * Called before the middleware returns a response.\n * Modify headers, cookies, or return a replacement response.\n */\n beforeResponse?: (context: {\n response: NextResponseInstance\n request: NextRequest\n locale: string\n type: 'redirect' | 'rewrite' | 'next'\n }) => NextResponseInstance | void | undefined\n}\n\ntype NextRequest = {\n nextUrl: { pathname: string; search: string; basePath?: string }\n url: string\n cookies: { get(name: string): { value: string } | undefined }\n headers: Headers\n}\n\ntype NextResponseStatic<R extends NextResponseInstance = NextResponseInstance> = {\n redirect(url: URL): R\n rewrite(url: URL, init?: Record<string, unknown>): R\n next(init?: Record<string, unknown>): R\n}\n\ntype NextResponseInstance = {\n headers: { set(name: string, value: string): void }\n}\n\n/**\n * Create an i18n middleware function for Next.js.\n */\nexport function createI18nMiddleware<R extends NextResponseInstance = NextResponseInstance>(\n config: I18nMiddlewareConfig & { NextResponse: NextResponseStatic<R> },\n) {\n const { NextResponse } = config\n const resolvedLocales: string[] = config.locales ?? _configLocales\n const resolvedSourceLocale: string = config.sourceLocale ?? _configSourceLocale\n const cookieName = config.cookieName ?? _configCookieName\n const localePrefix = config.localePrefix ?? 'as-needed'\n const rewriteDefaultLocale = config.rewriteDefaultLocale ?? false\n const setCookieEnabled = config.setCookie ?? false\n const cookieOpts: CookieOptions = config.cookieOptions ?? {}\n const localeDetectionEnabled = config.localeDetection ?? true\n const alternateLinksEnabled = config.alternateLinks ?? false\n const pathnamesMap = config.pathnames\n const domainsConfig = config.domains\n const beforeResponse = config.beforeResponse\n\n function finalizeResponse(\n response: R,\n request: NextRequest,\n locale: string,\n type: 'redirect' | 'rewrite' | 'next',\n pathLocale: string | null,\n ): R {\n response.headers.set(LOCALE_HEADER, locale)\n if (setCookieEnabled) {\n maybeSetCookie(response, request, locale, cookieName, pathLocale, cookieOpts)\n }\n if (alternateLinksEnabled || config.getAlternateLinks) {\n const linkHeader = config.getAlternateLinks\n ? buildCustomAlternateLinks(config.getAlternateLinks, request, locale, resolvedLocales)\n : buildAlternateLinks(request, resolvedLocales, resolvedSourceLocale, localePrefix, request.nextUrl.basePath ?? '', pathnamesMap)\n response.headers.set('Link', linkHeader)\n }\n if (beforeResponse) {\n const replacement = beforeResponse({ response, request, locale, type })\n if (replacement) return replacement as R\n }\n return response\n }\n\n return function i18nMiddleware(request: NextRequest): R {\n const locales = resolvedLocales\n const sourceLocale = resolvedSourceLocale\n const { pathname, search } = request.nextUrl\n const basePath = request.nextUrl.basePath ?? ''\n\n // Extract locale from URL path ('never' mode skips this)\n const segments = pathname.split('/')\n const firstSegment = segments[1] ?? ''\n const pathLocale = localePrefix === 'never' ? null : findLocale(firstSegment, locales)\n\n // Determine the active locale\n let locale: string\n\n if (pathLocale) {\n locale = pathLocale\n } else if (!localeDetectionEnabled) {\n locale = sourceLocale\n } else {\n // Detection chain: custom → domains → cookie → Accept-Language → default\n const custom = config.detectLocale?.(request)\n if (custom !== undefined && findLocale(custom, locales) !== null) {\n locale = findLocale(custom, locales)!\n } else if (domainsConfig) {\n const domainLocale = detectFromDomain(request, domainsConfig, locales)\n locale = domainLocale ?? detectLocale(request, locales, sourceLocale, cookieName)\n } else {\n locale = detectLocale(request, locales, sourceLocale, cookieName)\n }\n }\n\n // Build request headers\n const requestHeaders = new Headers(request.headers)\n requestHeaders.set(LOCALE_HEADER, locale)\n\n // ── 'never' mode ──────────────────────────────────────────────────────\n if (localePrefix === 'never') {\n if (rewriteDefaultLocale) {\n const rewriteUrl = new URL(`${basePath}/${locale}${pathname}${search}`, request.url)\n return finalizeResponse(\n NextResponse.rewrite(rewriteUrl, { request: { headers: requestHeaders } }),\n request, locale, 'rewrite', null,\n )\n }\n return finalizeResponse(\n NextResponse.next({ request: { headers: requestHeaders } }),\n request, locale, 'next', null,\n )\n }\n\n // ── Pathnames mapping ─────────────────────────────────────────────────\n if (pathnamesMap && pathLocale) {\n const pathWithoutLocale = normalizeSlashes('/' + segments.slice(2).join('/'))\n const internalPath = resolveInternalPath(pathWithoutLocale, locale, pathnamesMap)\n\n if (internalPath) {\n const rewriteUrl = new URL(`${basePath}/${locale}${internalPath}${search}`, request.url)\n return finalizeResponse(\n NextResponse.rewrite(rewriteUrl, { request: { headers: requestHeaders } }),\n request, locale, 'rewrite', pathLocale,\n )\n }\n\n const localizedPath = resolveLocalizedPath(pathWithoutLocale, locale, pathnamesMap)\n if (localizedPath && localizedPath !== pathWithoutLocale) {\n const redirectUrl = new URL(`${basePath}/${locale}${localizedPath}${search}`, request.url)\n return finalizeResponse(\n NextResponse.redirect(redirectUrl),\n request, locale, 'redirect', pathLocale,\n )\n }\n }\n\n // ── Case 1: No locale in path → redirect ──────────────────────────────\n if (!pathLocale && (localePrefix === 'always' || locale !== sourceLocale)) {\n // If pathnames configured, redirect to localized path\n let targetPath = pathname\n if (pathnamesMap) {\n const localized = resolveLocalizedPath(pathname, locale, pathnamesMap)\n if (localized) targetPath = localized\n }\n const redirectUrl = new URL(`${basePath}/${locale}${targetPath}${search}`, request.url)\n return finalizeResponse(\n NextResponse.redirect(redirectUrl),\n request, locale, 'redirect', pathLocale,\n )\n }\n\n // ── Case 2: as-needed + source locale + rewriteDefaultLocale ──────────\n if (!pathLocale && locale === sourceLocale && rewriteDefaultLocale) {\n const rewriteUrl = new URL(`${basePath}/${sourceLocale}${pathname}${search}`, request.url)\n return finalizeResponse(\n NextResponse.rewrite(rewriteUrl, { request: { headers: requestHeaders } }),\n request, locale, 'rewrite', pathLocale,\n )\n }\n\n // ── Case 3: as-needed, source locale has explicit prefix → strip ──────\n if (localePrefix === 'as-needed' && pathLocale === sourceLocale) {\n const pathWithoutLocale = normalizeSlashes('/' + segments.slice(2).join('/'))\n if (rewriteDefaultLocale) {\n const redirectUrl = new URL(`${basePath}${pathWithoutLocale}${search}`, request.url)\n return finalizeResponse(\n NextResponse.redirect(redirectUrl),\n request, locale, 'redirect', pathLocale,\n )\n }\n const rewriteUrl = new URL(`${basePath}${pathWithoutLocale}${search}`, request.url)\n return finalizeResponse(\n NextResponse.rewrite(rewriteUrl, { request: { headers: requestHeaders } }),\n request, locale, 'rewrite', pathLocale,\n )\n }\n\n // ── Case 4/5: pass through ────────────────────────────────────────────\n return finalizeResponse(\n NextResponse.next({ request: { headers: requestHeaders } }),\n request, locale, 'next', pathLocale,\n )\n }\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction findLocale(candidate: string, locales: string[]): string | null {\n if (!candidate) return null\n const lower = candidate.toLowerCase()\n return locales.find(l => l.toLowerCase() === lower) ?? null\n}\n\nfunction normalizeSlashes(path: string): string {\n return path.replace(/\\/+/g, '/') || '/'\n}\n\nfunction maybeSetCookie(\n response: NextResponseInstance,\n request: NextRequest,\n locale: string,\n cookieName: string,\n pathLocale: string | null,\n opts: CookieOptions,\n): void {\n if (pathLocale) return\n if (request.cookies.get(cookieName)?.value === locale) return\n\n const parts = [`${cookieName}=${encodeURIComponent(locale)}`]\n parts.push(`path=${opts.path ?? '/'}`)\n parts.push(`max-age=${opts.maxAge ?? 31536000}`)\n parts.push(`samesite=${opts.sameSite ?? 'lax'}`)\n if (opts.domain) parts.push(`domain=${opts.domain}`)\n if (opts.secure ?? request.url.startsWith('https')) parts.push('secure')\n response.headers.set('set-cookie', parts.join(';'))\n}\n\nfunction detectFromDomain(\n request: NextRequest,\n domains: DomainConfig[],\n locales: string[],\n): string | null {\n const host = request.headers.get('host')?.split(':')[0] ?? ''\n for (const d of domains) {\n if (host === d.domain || host.endsWith('.' + d.domain)) {\n const found = findLocale(d.defaultLocale, d.locales ?? locales)\n if (found) return found\n }\n }\n return null\n}\n\nfunction detectLocale(\n request: NextRequest,\n locales: string[],\n defaultLocale: string,\n cookieName: string,\n): string {\n const cookieLocale = request.cookies.get(cookieName)?.value\n if (cookieLocale) {\n const found = findLocale(cookieLocale, locales)\n if (found) return found\n }\n\n const acceptLang = request.headers.get('accept-language')\n if (acceptLang) {\n for (const part of acceptLang.split(',')) {\n const lang = part.split(';')[0]!.trim()\n const exact = findLocale(lang, locales)\n if (exact) return exact\n const prefix = lang.split('-')[0]!.toLowerCase()\n const match = locales.find(l => {\n const ll = l.toLowerCase()\n return ll === prefix || ll.startsWith(prefix + '-')\n })\n if (match) return match\n }\n }\n\n return defaultLocale\n}\n\nfunction stripLocalePrefix(pathname: string, locales: string[]): string {\n const segments = pathname.split('/')\n const first = segments[1] ?? ''\n if (findLocale(first, locales)) {\n return normalizeSlashes('/' + segments.slice(2).join('/'))\n }\n return pathname\n}\n\n/** Match a pathname against a pattern with [param] and [...slug] segments. */\nfunction matchPattern(pattern: string, pathname: string): Record<string, string> | null {\n const patternParts = pattern.split('/').filter(Boolean)\n const pathParts = pathname.split('/').filter(Boolean)\n\n const params: Record<string, string> = {}\n\n for (let i = 0; i < patternParts.length; i++) {\n const pp = patternParts[i]!\n if (pp.startsWith('[...') && pp.endsWith(']')) {\n // Catch-all: consume the rest\n const key = pp.slice(4, -1)\n params[key] = pathParts.slice(i).join('/')\n return params\n }\n if (pp.startsWith('[') && pp.endsWith(']')) {\n // Dynamic segment\n if (i >= pathParts.length) return null\n params[pp.slice(1, -1)] = pathParts[i]!\n continue\n }\n // Static segment\n if (i >= pathParts.length || pp !== pathParts[i]) return null\n }\n\n if (patternParts.length !== pathParts.length) return null\n return params\n}\n\n/** Substitute params into a pattern. */\nfunction substituteParams(pattern: string, params: Record<string, string>): string {\n return pattern.replace(/\\[\\.\\.\\.(\\w+)\\]|\\[(\\w+)\\]/g, (_, catchAll, param) => {\n const key = catchAll ?? param\n return params[key] ?? ''\n })\n}\n\nfunction resolveInternalPath(\n localizedPath: string,\n locale: string,\n pathnames: Record<string, Record<string, string>>,\n): string | null {\n for (const [internal, mapping] of Object.entries(pathnames)) {\n const localized = mapping[locale]\n if (!localized) continue\n // Exact match\n if (localized === localizedPath) return internal\n // Pattern match\n const params = matchPattern(localized, localizedPath)\n if (params) return substituteParams(internal, params)\n }\n return null\n}\n\nfunction resolveLocalizedPath(\n internalPath: string,\n locale: string,\n pathnames: Record<string, Record<string, string>>,\n): string | null {\n for (const [internal, mapping] of Object.entries(pathnames)) {\n const localized = mapping[locale]\n if (!localized) continue\n // Exact match\n if (internal === internalPath) return localized\n // Pattern match\n const params = matchPattern(internal, internalPath)\n if (params) return substituteParams(localized, params)\n }\n return null\n}\n\nfunction buildAlternateLinks(\n request: NextRequest,\n locales: string[],\n sourceLocale: string,\n localePrefix: 'always' | 'as-needed' | 'never',\n basePath: string,\n pathnames?: Record<string, Record<string, string>>,\n): string {\n const origin = new URL(request.url).origin\n const cleanPath = stripLocalePrefix(request.nextUrl.pathname, locales)\n\n const links = locales.map(loc => {\n let localePath: string\n if (localePrefix === 'never') {\n localePath = cleanPath\n } else if (localePrefix === 'as-needed' && loc === sourceLocale) {\n localePath = cleanPath\n } else {\n localePath = `/${loc}${cleanPath}`\n }\n if (pathnames) {\n const mapped = resolveLocalizedPath(cleanPath, loc, pathnames)\n if (mapped) {\n localePath = localePrefix === 'never' || (localePrefix === 'as-needed' && loc === sourceLocale)\n ? mapped\n : `/${loc}${mapped}`\n }\n }\n return `<${origin}${basePath}${localePath}>; rel=\"alternate\"; hreflang=\"${loc}\"`\n })\n\n // x-default\n const defaultPath = localePrefix === 'always' ? `/${sourceLocale}${cleanPath}` : cleanPath\n links.push(`<${origin}${basePath}${defaultPath}>; rel=\"alternate\"; hreflang=\"x-default\"`)\n\n return links.join(', ')\n}\n\nfunction buildCustomAlternateLinks(\n getAlternateLinks: NonNullable<I18nMiddlewareConfig['getAlternateLinks']>,\n request: NextRequest,\n locale: string,\n locales: string[],\n): string {\n const origin = new URL(request.url).origin\n const cleanPath = stripLocalePrefix(request.nextUrl.pathname, locales)\n const basePath = request.nextUrl.basePath ?? ''\n const entries = getAlternateLinks({ pathname: cleanPath, locale, locales, origin, basePath })\n return entries.map(e => `<${e.href}>; rel=\"alternate\"; hreflang=\"${e.hreflang}\"`).join(', ')\n}\n"],"mappings":"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"}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -6,45 +6,59 @@
|
|
|
6
6
|
* Uses `x-fluenti-locale` request header to pass locale from middleware to
|
|
7
7
|
* server components — avoids `Set-Cookie` on every request (CDN-friendly).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @example Minimal — locales/sourceLocale/cookieName auto-read from fluenti.config.ts
|
|
9
|
+
* @example Minimal
|
|
12
10
|
* ```ts
|
|
13
|
-
* // src/middleware.ts
|
|
14
11
|
* import { NextResponse } from 'next/server'
|
|
15
12
|
* import { createI18nMiddleware } from '@fluenti/next/middleware'
|
|
16
13
|
*
|
|
17
14
|
* export default createI18nMiddleware({ NextResponse })
|
|
18
|
-
*
|
|
19
|
-
* export const config = {
|
|
20
|
-
* matcher: ['/((?!_next|api|favicon).*)'],
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @example With app/[locale]/ directory structure (rewriteDefaultLocale)
|
|
25
|
-
* ```ts
|
|
26
|
-
* import { NextResponse } from 'next/server'
|
|
27
|
-
* import { createI18nMiddleware } from '@fluenti/next/middleware'
|
|
28
|
-
*
|
|
29
|
-
* export default createI18nMiddleware({ NextResponse, rewriteDefaultLocale: true })
|
|
30
15
|
* ```
|
|
31
16
|
*
|
|
32
|
-
* @example
|
|
17
|
+
* @example With pathnames + alternateLinks + domains
|
|
33
18
|
* ```ts
|
|
34
19
|
* import { NextResponse } from 'next/server'
|
|
35
|
-
* import { clerkMiddleware } from '@clerk/nextjs/server'
|
|
36
20
|
* import { createI18nMiddleware } from '@fluenti/next/middleware'
|
|
37
21
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
22
|
+
* export default createI18nMiddleware({
|
|
23
|
+
* NextResponse,
|
|
24
|
+
* rewriteDefaultLocale: true,
|
|
25
|
+
* alternateLinks: true,
|
|
26
|
+
* pathnames: {
|
|
27
|
+
* '/about': { fr: '/a-propos' },
|
|
28
|
+
* '/blog/[slug]': { fr: '/articles/[slug]' },
|
|
29
|
+
* },
|
|
30
|
+
* domains: [
|
|
31
|
+
* { domain: 'fr.example.com', defaultLocale: 'fr' },
|
|
32
|
+
* ],
|
|
43
33
|
* })
|
|
44
34
|
* ```
|
|
45
35
|
*/
|
|
46
36
|
/** Header name used to pass resolved locale from middleware to RSC */
|
|
47
37
|
export declare const LOCALE_HEADER = "x-fluenti-locale";
|
|
38
|
+
export interface CookieOptions {
|
|
39
|
+
/** Cookie domain (e.g. '.example.com' for cross-subdomain) */
|
|
40
|
+
domain?: string;
|
|
41
|
+
/** Secure flag (default: auto-detect from request URL) */
|
|
42
|
+
secure?: boolean;
|
|
43
|
+
/** SameSite attribute (default: 'lax') */
|
|
44
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
45
|
+
/** Max age in seconds (default: 31536000 = 1 year) */
|
|
46
|
+
maxAge?: number;
|
|
47
|
+
/** Cookie path (default: '/') */
|
|
48
|
+
path?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface DomainConfig {
|
|
51
|
+
/** Domain hostname (e.g. 'fr.example.com') */
|
|
52
|
+
domain: string;
|
|
53
|
+
/** Default locale for this domain */
|
|
54
|
+
defaultLocale: string;
|
|
55
|
+
/** Optional subset of locales available on this domain */
|
|
56
|
+
locales?: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface AlternateLinkEntry {
|
|
59
|
+
href: string;
|
|
60
|
+
hreflang: string;
|
|
61
|
+
}
|
|
48
62
|
export interface I18nMiddlewareConfig {
|
|
49
63
|
/** Available locales. If omitted, reads from `fluenti.config.ts`. */
|
|
50
64
|
locales?: string[];
|
|
@@ -54,62 +68,93 @@ export interface I18nMiddlewareConfig {
|
|
|
54
68
|
cookieName?: string;
|
|
55
69
|
/**
|
|
56
70
|
* Locale prefix strategy:
|
|
57
|
-
* - `'always'`: all locales get a URL prefix
|
|
58
|
-
* - `'as-needed'`: source locale has no prefix, others do
|
|
71
|
+
* - `'always'`: all locales get a URL prefix
|
|
72
|
+
* - `'as-needed'`: source locale has no prefix, others do
|
|
73
|
+
* - `'never'`: no locale prefix in URLs; locale determined by detection chain
|
|
59
74
|
*
|
|
60
75
|
* Default: `'as-needed'`
|
|
61
76
|
*/
|
|
62
|
-
localePrefix?: 'always' | 'as-needed';
|
|
77
|
+
localePrefix?: 'always' | 'as-needed' | 'never';
|
|
63
78
|
/**
|
|
64
|
-
* When true, bare paths
|
|
65
|
-
*
|
|
66
|
-
* with `localePrefix: 'as-needed'`.
|
|
67
|
-
*
|
|
68
|
-
* Also changes the handling of explicit source-locale URLs (e.g. `/en/about`):
|
|
69
|
-
* instead of rewriting to `/about`, they are **redirected** to `/about` so the
|
|
70
|
-
* browser follows the canonical URL, which is then rewritten internally.
|
|
71
|
-
*
|
|
72
|
-
* Example: `GET /about` → internally rewritten to `/en/about` (URL stays `/about`)
|
|
73
|
-
* Example: `GET /en/about` → 302 redirect → `/about` → rewritten to `/en/about`
|
|
79
|
+
* When true, bare paths are internally rewritten to include the locale prefix.
|
|
80
|
+
* Required when using `app/[locale]/` directory structure.
|
|
74
81
|
*
|
|
75
82
|
* Default: `false`
|
|
76
83
|
*/
|
|
77
84
|
rewriteDefaultLocale?: boolean;
|
|
78
85
|
/**
|
|
79
|
-
* When true,
|
|
80
|
-
* preference is persisted across requests (useful when locale is detected from
|
|
81
|
-
* `Accept-Language` rather than an existing cookie).
|
|
82
|
-
*
|
|
86
|
+
* When true, detected locale is persisted in a cookie.
|
|
83
87
|
* Disabled by default to keep responses CDN-cacheable.
|
|
84
|
-
*
|
|
85
|
-
* The cookie is only written when the locale was **detected** (not read from the URL
|
|
86
|
-
* path), and only when it differs from the existing cookie value.
|
|
87
88
|
*/
|
|
88
89
|
setCookie?: boolean;
|
|
90
|
+
/** Fine-grained cookie configuration for multi-domain / secure deployments. */
|
|
91
|
+
cookieOptions?: CookieOptions;
|
|
89
92
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* Return `undefined` to fall through to built-in detection.
|
|
93
|
+
* Set to false to disable automatic locale detection.
|
|
94
|
+
* Bare paths will always use `sourceLocale` instead of detecting from cookie / Accept-Language.
|
|
93
95
|
*
|
|
94
|
-
*
|
|
96
|
+
* Default: `true`
|
|
97
|
+
*/
|
|
98
|
+
localeDetection?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Custom locale detection function. Called when no locale is present in the URL path.
|
|
101
|
+
* Return a locale string to override the default chain, or `undefined` to fall through.
|
|
102
|
+
*/
|
|
103
|
+
detectLocale?: (req: NextRequest) => string | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Domain-based locale routing. Each domain maps to a default locale.
|
|
106
|
+
* Domain matching is checked before cookie/Accept-Language detection.
|
|
95
107
|
*
|
|
96
|
-
* @example
|
|
108
|
+
* @example
|
|
97
109
|
* ```ts
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
110
|
+
* domains: [
|
|
111
|
+
* { domain: 'fr.example.com', defaultLocale: 'fr' },
|
|
112
|
+
* { domain: 'example.co.jp', defaultLocale: 'ja' },
|
|
113
|
+
* ]
|
|
102
114
|
* ```
|
|
115
|
+
*/
|
|
116
|
+
domains?: DomainConfig[];
|
|
117
|
+
/**
|
|
118
|
+
* Map internal paths to localized paths per locale.
|
|
119
|
+
* Supports dynamic segments: `[param]` and `[...slug]`.
|
|
103
120
|
*
|
|
104
|
-
* @example
|
|
121
|
+
* @example
|
|
105
122
|
* ```ts
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
123
|
+
* pathnames: {
|
|
124
|
+
* '/about': { fr: '/a-propos' },
|
|
125
|
+
* '/blog/[slug]': { fr: '/articles/[slug]' },
|
|
109
126
|
* }
|
|
110
127
|
* ```
|
|
111
128
|
*/
|
|
112
|
-
|
|
129
|
+
pathnames?: Record<string, Record<string, string>>;
|
|
130
|
+
/**
|
|
131
|
+
* When true, adds `Link` response headers with `rel="alternate"` hreflang
|
|
132
|
+
* and `rel="canonical"` for SEO.
|
|
133
|
+
*
|
|
134
|
+
* Default: `false`
|
|
135
|
+
*/
|
|
136
|
+
alternateLinks?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Custom function to build alternate link entries. Overrides default `alternateLinks` behavior.
|
|
139
|
+
* Return an array of `{ href, hreflang }` entries.
|
|
140
|
+
*/
|
|
141
|
+
getAlternateLinks?: (context: {
|
|
142
|
+
pathname: string;
|
|
143
|
+
locale: string;
|
|
144
|
+
locales: string[];
|
|
145
|
+
origin: string;
|
|
146
|
+
basePath: string;
|
|
147
|
+
}) => AlternateLinkEntry[];
|
|
148
|
+
/**
|
|
149
|
+
* Called before the middleware returns a response.
|
|
150
|
+
* Modify headers, cookies, or return a replacement response.
|
|
151
|
+
*/
|
|
152
|
+
beforeResponse?: (context: {
|
|
153
|
+
response: NextResponseInstance;
|
|
154
|
+
request: NextRequest;
|
|
155
|
+
locale: string;
|
|
156
|
+
type: 'redirect' | 'rewrite' | 'next';
|
|
157
|
+
}) => NextResponseInstance | void | undefined;
|
|
113
158
|
}
|
|
114
159
|
type NextRequest = {
|
|
115
160
|
nextUrl: {
|
|
@@ -125,10 +170,10 @@ type NextRequest = {
|
|
|
125
170
|
};
|
|
126
171
|
headers: Headers;
|
|
127
172
|
};
|
|
128
|
-
type NextResponseStatic = {
|
|
129
|
-
redirect(url: URL):
|
|
130
|
-
rewrite(url: URL, init?: Record<string, unknown>):
|
|
131
|
-
next(init?: Record<string, unknown>):
|
|
173
|
+
type NextResponseStatic<R extends NextResponseInstance = NextResponseInstance> = {
|
|
174
|
+
redirect(url: URL): R;
|
|
175
|
+
rewrite(url: URL, init?: Record<string, unknown>): R;
|
|
176
|
+
next(init?: Record<string, unknown>): R;
|
|
132
177
|
};
|
|
133
178
|
type NextResponseInstance = {
|
|
134
179
|
headers: {
|
|
@@ -137,20 +182,9 @@ type NextResponseInstance = {
|
|
|
137
182
|
};
|
|
138
183
|
/**
|
|
139
184
|
* Create an i18n middleware function for Next.js.
|
|
140
|
-
*
|
|
141
|
-
* Requires `NextResponse` to be passed in because the middleware module runs
|
|
142
|
-
* in Next.js Edge Runtime where `require('next/server')` is not available.
|
|
143
|
-
*
|
|
144
|
-
* @example
|
|
145
|
-
* ```ts
|
|
146
|
-
* import { NextResponse } from 'next/server'
|
|
147
|
-
* import { createI18nMiddleware } from '@fluenti/next/middleware'
|
|
148
|
-
*
|
|
149
|
-
* export default createI18nMiddleware({ NextResponse })
|
|
150
|
-
* ```
|
|
151
185
|
*/
|
|
152
|
-
export declare function createI18nMiddleware(config: I18nMiddlewareConfig & {
|
|
153
|
-
NextResponse: NextResponseStatic
|
|
154
|
-
}): (request: NextRequest) =>
|
|
186
|
+
export declare function createI18nMiddleware<R extends NextResponseInstance = NextResponseInstance>(config: I18nMiddlewareConfig & {
|
|
187
|
+
NextResponse: NextResponseStatic<R>;
|
|
188
|
+
}): (request: NextRequest) => R;
|
|
155
189
|
export {};
|
|
156
190
|
//# sourceMappingURL=middleware.d.ts.map
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAQH,sEAAsE;AACtE,eAAO,MAAM,aAAa,qBAAqB,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAA;IAC/C;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,CAAA;IACvD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;IACxB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC5B,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;KACjB,KAAK,kBAAkB,EAAE,CAAA;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE;QACzB,QAAQ,EAAE,oBAAoB,CAAA;QAC9B,OAAO,EAAE,WAAW,CAAA;QACpB,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAAA;KACtC,KAAK,oBAAoB,GAAG,IAAI,GAAG,SAAS,CAAA;CAC9C;AAED,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChE,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAC7D,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,KAAK,kBAAkB,CAAC,CAAC,SAAS,oBAAoB,GAAG,oBAAoB,IAAI;IAC/E,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,CAAA;IACrB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAA;IACpD,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAA;CACxC,CAAA;AAED,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CACpD,CAAA;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,oBAAoB,GAAG,oBAAoB,EACxF,MAAM,EAAE,oBAAoB,GAAG;IAAE,YAAY,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAA;CAAE,IAwCvC,SAAS,WAAW,KAAG,CAAC,CAwHxD"}
|
package/dist/middleware.js
CHANGED
|
@@ -2,34 +2,74 @@ import { cookieName as e, locales as t, sourceLocale as n } from "@fluenti/next/
|
|
|
2
2
|
//#region src/middleware.ts
|
|
3
3
|
var r = "x-fluenti-locale";
|
|
4
4
|
function i(i) {
|
|
5
|
-
let { NextResponse:
|
|
5
|
+
let { NextResponse: u } = i, d = i.locales ?? t, f = i.sourceLocale ?? n, _ = i.cookieName ?? e, v = i.localePrefix ?? "as-needed", y = i.rewriteDefaultLocale ?? !1, b = i.setCookie ?? !1, x = i.cookieOptions ?? {}, S = i.localeDetection ?? !0, C = i.alternateLinks ?? !1, w = i.pathnames, T = i.domains, E = i.beforeResponse;
|
|
6
|
+
function D(e, t, n, a, o) {
|
|
7
|
+
if (e.headers.set(r, n), b && s(e, t, n, _, o, x), C || i.getAlternateLinks) {
|
|
8
|
+
let r = i.getAlternateLinks ? g(i.getAlternateLinks, t, n, d) : h(t, d, f, v, t.nextUrl.basePath ?? "", w);
|
|
9
|
+
e.headers.set("Link", r);
|
|
10
|
+
}
|
|
11
|
+
if (E) {
|
|
12
|
+
let r = E({
|
|
13
|
+
response: e,
|
|
14
|
+
request: t,
|
|
15
|
+
locale: n,
|
|
16
|
+
type: a
|
|
17
|
+
});
|
|
18
|
+
if (r) return r;
|
|
19
|
+
}
|
|
20
|
+
return e;
|
|
21
|
+
}
|
|
6
22
|
return function(e) {
|
|
7
|
-
let t =
|
|
8
|
-
if (
|
|
23
|
+
let t = d, n = f, { pathname: s, search: h } = e.nextUrl, g = e.nextUrl.basePath ?? "", b = s.split("/"), x = b[1] ?? "", C = v === "never" ? null : a(x, t), E;
|
|
24
|
+
if (C) E = C;
|
|
25
|
+
else if (!S) E = n;
|
|
9
26
|
else {
|
|
10
27
|
let r = i.detectLocale?.(e);
|
|
11
|
-
|
|
28
|
+
E = r !== void 0 && a(r, t) !== null ? a(r, t) : T ? c(e, T, t) ?? l(e, t, n, _) : l(e, t, n, _);
|
|
12
29
|
}
|
|
13
|
-
let
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
let O = new Headers(e.headers);
|
|
31
|
+
if (O.set(r, E), v === "never") {
|
|
32
|
+
if (y) {
|
|
33
|
+
let t = new URL(`${g}/${E}${s}${h}`, e.url);
|
|
34
|
+
return D(u.rewrite(t, { request: { headers: O } }), e, E, "rewrite", null);
|
|
35
|
+
}
|
|
36
|
+
return D(u.next({ request: { headers: O } }), e, E, "next", null);
|
|
17
37
|
}
|
|
18
|
-
if (
|
|
19
|
-
let t =
|
|
20
|
-
|
|
38
|
+
if (w && C) {
|
|
39
|
+
let t = o("/" + b.slice(2).join("/")), n = p(t, E, w);
|
|
40
|
+
if (n) {
|
|
41
|
+
let t = new URL(`${g}/${E}${n}${h}`, e.url);
|
|
42
|
+
return D(u.rewrite(t, { request: { headers: O } }), e, E, "rewrite", C);
|
|
43
|
+
}
|
|
44
|
+
let r = m(t, E, w);
|
|
45
|
+
if (r && r !== t) {
|
|
46
|
+
let t = new URL(`${g}/${E}${r}${h}`, e.url);
|
|
47
|
+
return D(u.redirect(t), e, E, "redirect", C);
|
|
48
|
+
}
|
|
21
49
|
}
|
|
22
|
-
if (
|
|
23
|
-
let t =
|
|
24
|
-
if (
|
|
25
|
-
let
|
|
26
|
-
|
|
50
|
+
if (!C && (v === "always" || E !== n)) {
|
|
51
|
+
let t = s;
|
|
52
|
+
if (w) {
|
|
53
|
+
let e = m(s, E, w);
|
|
54
|
+
e && (t = e);
|
|
27
55
|
}
|
|
28
|
-
let n = new URL(`${
|
|
29
|
-
return
|
|
56
|
+
let n = new URL(`${g}/${E}${t}${h}`, e.url);
|
|
57
|
+
return D(u.redirect(n), e, E, "redirect", C);
|
|
58
|
+
}
|
|
59
|
+
if (!C && E === n && y) {
|
|
60
|
+
let t = new URL(`${g}/${n}${s}${h}`, e.url);
|
|
61
|
+
return D(u.rewrite(t, { request: { headers: O } }), e, E, "rewrite", C);
|
|
30
62
|
}
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
if (v === "as-needed" && C === n) {
|
|
64
|
+
let t = o("/" + b.slice(2).join("/"));
|
|
65
|
+
if (y) {
|
|
66
|
+
let n = new URL(`${g}${t}${h}`, e.url);
|
|
67
|
+
return D(u.redirect(n), e, E, "redirect", C);
|
|
68
|
+
}
|
|
69
|
+
let n = new URL(`${g}${t}${h}`, e.url);
|
|
70
|
+
return D(u.rewrite(n, { request: { headers: O } }), e, E, "rewrite", C);
|
|
71
|
+
}
|
|
72
|
+
return D(u.next({ request: { headers: O } }), e, E, "next", C);
|
|
33
73
|
};
|
|
34
74
|
}
|
|
35
75
|
function a(e, t) {
|
|
@@ -37,10 +77,23 @@ function a(e, t) {
|
|
|
37
77
|
let n = e.toLowerCase();
|
|
38
78
|
return t.find((e) => e.toLowerCase() === n) ?? null;
|
|
39
79
|
}
|
|
40
|
-
function o(e
|
|
41
|
-
|
|
80
|
+
function o(e) {
|
|
81
|
+
return e.replace(/\/+/g, "/") || "/";
|
|
82
|
+
}
|
|
83
|
+
function s(e, t, n, r, i, a) {
|
|
84
|
+
if (i || t.cookies.get(r)?.value === n) return;
|
|
85
|
+
let o = [`${r}=${encodeURIComponent(n)}`];
|
|
86
|
+
o.push(`path=${a.path ?? "/"}`), o.push(`max-age=${a.maxAge ?? 31536e3}`), o.push(`samesite=${a.sameSite ?? "lax"}`), a.domain && o.push(`domain=${a.domain}`), (a.secure ?? t.url.startsWith("https")) && o.push("secure"), e.headers.set("set-cookie", o.join(";"));
|
|
42
87
|
}
|
|
43
|
-
function
|
|
88
|
+
function c(e, t, n) {
|
|
89
|
+
let r = e.headers.get("host")?.split(":")[0] ?? "";
|
|
90
|
+
for (let e of t) if (r === e.domain || r.endsWith("." + e.domain)) {
|
|
91
|
+
let t = a(e.defaultLocale, e.locales ?? n);
|
|
92
|
+
if (t) return t;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function l(e, t, n, r) {
|
|
44
97
|
let i = e.cookies.get(r)?.value;
|
|
45
98
|
if (i) {
|
|
46
99
|
let e = a(i, t);
|
|
@@ -58,6 +111,71 @@ function s(e, t, n, r) {
|
|
|
58
111
|
}
|
|
59
112
|
return n;
|
|
60
113
|
}
|
|
114
|
+
function u(e, t) {
|
|
115
|
+
let n = e.split("/");
|
|
116
|
+
return a(n[1] ?? "", t) ? o("/" + n.slice(2).join("/")) : e;
|
|
117
|
+
}
|
|
118
|
+
function d(e, t) {
|
|
119
|
+
let n = e.split("/").filter(Boolean), r = t.split("/").filter(Boolean), i = {};
|
|
120
|
+
for (let e = 0; e < n.length; e++) {
|
|
121
|
+
let t = n[e];
|
|
122
|
+
if (t.startsWith("[...") && t.endsWith("]")) {
|
|
123
|
+
let n = t.slice(4, -1);
|
|
124
|
+
return i[n] = r.slice(e).join("/"), i;
|
|
125
|
+
}
|
|
126
|
+
if (t.startsWith("[") && t.endsWith("]")) {
|
|
127
|
+
if (e >= r.length) return null;
|
|
128
|
+
i[t.slice(1, -1)] = r[e];
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (e >= r.length || t !== r[e]) return null;
|
|
132
|
+
}
|
|
133
|
+
return n.length === r.length ? i : null;
|
|
134
|
+
}
|
|
135
|
+
function f(e, t) {
|
|
136
|
+
return e.replace(/\[\.\.\.(\w+)\]|\[(\w+)\]/g, (e, n, r) => t[n ?? r] ?? "");
|
|
137
|
+
}
|
|
138
|
+
function p(e, t, n) {
|
|
139
|
+
for (let [r, i] of Object.entries(n)) {
|
|
140
|
+
let n = i[t];
|
|
141
|
+
if (!n) continue;
|
|
142
|
+
if (n === e) return r;
|
|
143
|
+
let a = d(n, e);
|
|
144
|
+
if (a) return f(r, a);
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function m(e, t, n) {
|
|
149
|
+
for (let [r, i] of Object.entries(n)) {
|
|
150
|
+
let n = i[t];
|
|
151
|
+
if (!n) continue;
|
|
152
|
+
if (r === e) return n;
|
|
153
|
+
let a = d(r, e);
|
|
154
|
+
if (a) return f(n, a);
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function h(e, t, n, r, i, a) {
|
|
159
|
+
let o = new URL(e.url).origin, s = u(e.nextUrl.pathname, t), c = t.map((e) => {
|
|
160
|
+
let t;
|
|
161
|
+
if (t = r === "never" || r === "as-needed" && e === n ? s : `/${e}${s}`, a) {
|
|
162
|
+
let i = m(s, e, a);
|
|
163
|
+
i && (t = r === "never" || r === "as-needed" && e === n ? i : `/${e}${i}`);
|
|
164
|
+
}
|
|
165
|
+
return `<${o}${i}${t}>; rel="alternate"; hreflang="${e}"`;
|
|
166
|
+
}), l = r === "always" ? `/${n}${s}` : s;
|
|
167
|
+
return c.push(`<${o}${i}${l}>; rel="alternate"; hreflang="x-default"`), c.join(", ");
|
|
168
|
+
}
|
|
169
|
+
function g(e, t, n, r) {
|
|
170
|
+
let i = new URL(t.url).origin;
|
|
171
|
+
return e({
|
|
172
|
+
pathname: u(t.nextUrl.pathname, r),
|
|
173
|
+
locale: n,
|
|
174
|
+
locales: r,
|
|
175
|
+
origin: i,
|
|
176
|
+
basePath: t.nextUrl.basePath ?? ""
|
|
177
|
+
}).map((e) => `<${e.href}>; rel="alternate"; hreflang="${e.hreflang}"`).join(", ");
|
|
178
|
+
}
|
|
61
179
|
//#endregion
|
|
62
180
|
export { r as LOCALE_HEADER, i as createI18nMiddleware };
|
|
63
181
|
|