Added support for single localization

This commit is contained in:
2026-09-08 18:12:58 +02:00
parent e30ac71044
commit 419207ac12
16 changed files with 287 additions and 102 deletions
+21 -13
View File
@@ -1,16 +1,15 @@
import type { I18nConfig } from './types.js'
import type { I18nConfig } from '../i18n/index.js'
import { negotiateLocale, isValidLocale, LOCALE_COOKIE_NAME } from '../i18n/index.js'
import { CONSENT_COOKIE, parseConsent } from '../consent/storage.js'
import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/index.js'
/**
* Minimal request shape the middleware reads. Kept structural so the plugin
* doesn't hard-depend on next/server types; a Next.js `NextRequest` satisfies it.
*/
type MiddlewareRequest = {
nextUrl: { pathname: string; search: string; clone: () => URL }
cookies: { get: (name: string) => { value: string } | undefined }
headers: { get: (name: string) => null | string }
nextUrl: { clone: () => URL; pathname: string; search: string }
headers: { get: (name: string) => string | null }
url: string
}
@@ -20,11 +19,13 @@ type MiddlewareRequest = {
* `next` means let the request pass through untouched.
*/
export type LocaleMiddlewareResult =
| { cookie?: { name: string; value: string }; location: string; type: 'redirect' }
| { cookie?: { name: string; value: string }; type: 'next' }
| { type: 'next'; cookie?: { name: string; value: string } }
| { type: 'redirect'; location: string; cookie?: { name: string; value: string } }
type CreateLocaleMiddlewareArgs = {
config: I18nConfig
/** Cookie name for the locale choice. Defaults to LOCALE_COOKIE_NAME. */
cookieName?: string
/**
* Consent category that gates *persisting* the locale cookie. The locale is
* always detected (routing works regardless), but the choice is only written
@@ -32,11 +33,9 @@ type CreateLocaleMiddlewareArgs = {
* 'functional'. Pass 'necessary' to always persist (treat locale as strictly
* necessary), which restores the pre-consent behaviour.
*/
consentCategory?: 'functional' | 'necessary'
consentCategory?: 'necessary' | 'functional'
/** 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
}
/**
@@ -76,14 +75,14 @@ function firstSegment(pathname: string): string {
*/
export function createLocaleMiddleware({
config,
cookieName = LOCALE_COOKIE_NAME,
consentCategory = 'functional',
consentCookieName = CONSENT_COOKIE,
cookieName = LOCALE_COOKIE_NAME,
}: CreateLocaleMiddlewareArgs) {
// Whether the locale cookie may be written: 'necessary' is always granted;
// 'functional' (default) requires the visitor to have consented.
function mayPersistLocale(request: MiddlewareRequest): boolean {
if (consentCategory === 'necessary') {return true}
if (consentCategory === 'necessary') return true
const consent = parseConsent(request.cookies.get(consentCookieName)?.value)
return consent?.[consentCategory] === true
}
@@ -91,6 +90,15 @@ export function createLocaleMiddleware({
return function localeMiddleware(request: MiddlewareRequest): LocaleMiddlewareResult {
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
@@ -108,9 +116,9 @@ export function createLocaleMiddleware({
// Resolve the locale to use
const locale = negotiateLocale({
cookieLocale: request.cookies.get(cookieName)?.value ?? null,
acceptLanguage: request.headers.get('accept-language'),
config,
cookieLocale: request.cookies.get(cookieName)?.value ?? null,
})
// Redirect to the locale-prefixed path, preserving the rest
+11 -3
View File
@@ -71,6 +71,13 @@ export function buildLocalizedPath({
return undefined
}
// Single-locale sites have no /pl, /en prefix — the language segment is
// dropped entirely (path is /o-nas, not /pl/o-nas). Detected automatically:
// one configured locale means one language, so no prefix is needed. The
// project's folder structure matches (app/[[...slug]] without [locale]).
const singleLocale = config.locales.length === 1
const localeSegment = singleLocale ? '' : `/${locale}`
if (prefix) {
const segment = prefix[locale]
// No archive slug in this locale means the entry is unreachable there —
@@ -79,14 +86,15 @@ export function buildLocalizedPath({
if (!segment) {
return undefined
}
return `/${locale}/${segment}/${slug}`
return `${localeSegment}/${segment}/${slug}`
}
if (slug === homeSlug) {
return `/${locale}`
// Home collapses to the root: '/' for single-locale, '/pl' otherwise.
return localeSegment || '/'
}
return `/${locale}/${slug}`
return `${localeSegment}/${slug}`
}
type SwitchLocaleArgs = {