next-intlayer
Version:
Simplify internationalization i18n in Next.js with context providers, hooks, locale detection, and multilingual content integration.
226 lines (224 loc) • 10.1 kB
JavaScript
import { localeDetector as localeDetector$1 } from "./localeDetector.mjs";
import configuration from "@intlayer/config/built";
import { getLocaleFromStorage, setLocaleInStorage } from "@intlayer/core";
import { DefaultValues } from "@intlayer/config/client";
import { NextResponse } from "next/server";
//#region src/proxy/intlayerProxy.ts
const { internationalization, routing } = configuration ?? {};
const { locales, defaultLocale } = internationalization ?? {};
const { basePath, mode } = routing ?? {};
const effectiveMode = mode ?? DefaultValues.Routing.ROUTING_MODE;
const noPrefix = effectiveMode === "no-prefix" || effectiveMode === "search-params";
const prefixDefault = effectiveMode === "prefix-all";
/**
* Detects if the request is a prefetch request from Next.js.
*
* Next.js prefetch requests can be identified by several headers:
* - purpose: 'prefetch' (standard prefetch header)
* - next-router-prefetch: '1' (Next.js router prefetch)
* - next-url: present (Next.js internal navigation)
*
* During prefetch, we should ignore cookie-based locale detection
* to prevent unwanted redirects when users are switching locales.
*
* @param request - The incoming Next.js request object.
* @returns - True if the request is a prefetch request, false otherwise.
*/
const isPrefetchRequest = (request) => {
const purpose = request.headers.get("purpose");
const nextRouterPrefetch = request.headers.get("next-router-prefetch");
const nextUrl = request.headers.get("next-url");
const xNextjsData = request.headers.get("x-nextjs-data");
return purpose === "prefetch" || nextRouterPrefetch === "1" || !!nextUrl || !!xNextjsData;
};
const appendLocaleSearchIfNeeded = (search, locale) => {
if (effectiveMode !== "search-params") return search;
const params = new URLSearchParams(search ?? "");
params.set("locale", locale);
return `?${params.toString()}`;
};
/**
* Proxy that handles the internationalization layer
*
* Usage:
*
* ```ts
* // ./src/proxy.ts
*
* export { intlayerProxy as proxy } from '@intlayer/next/proxy';
*
* // applies this proxy only to files in the app directory
* export const config = {
* matcher: '/((?!api|static|.*\\..*|_next).*)',
* };
* ```
*
* Main proxy function for handling internationalization.
*
* @param request - The incoming Next.js request object.
* @param event - The Next.js fetch event (optional).
* @param response - The Next.js response object (optional).
* @returns - The response to be returned to the client.
*/
const intlayerProxy = (request, _event, _response) => {
const pathname = request.nextUrl.pathname;
const localLocale = getLocalLocale(request);
if (noPrefix) return handleNoPrefix(request, localLocale, pathname);
return handlePrefix(request, localLocale, getPathLocale(pathname), pathname);
};
/**
* Retrieves the locale from the request cookies if available and valid.
*
* @param request - The incoming Next.js request object.
* @returns - The locale found in the cookies, or undefined if not found or invalid.
*/
const getLocalLocale = (request) => getLocaleFromStorage({
getCookie: (name) => request.cookies.get(name)?.value ?? null,
getHeader: (name) => request.headers.get(name) ?? null
});
/**
* Handles the case where URLs do not have locale prefixes.
*/
const handleNoPrefix = (request, localLocale, pathname) => {
const pathLocale = getPathLocale(pathname);
const locale = localLocale ?? defaultLocale;
if (pathLocale) return redirectUrl(request, `${pathname.slice(`/${pathLocale}`.length) || "/"}${appendLocaleSearchIfNeeded(request.nextUrl.search, pathLocale)}`);
if (effectiveMode === "search-params") {
if (request.nextUrl.searchParams.get("locale") !== locale) return redirectUrl(request, `${pathname}${appendLocaleSearchIfNeeded(request.nextUrl.search, locale)}`);
}
return rewriteUrl(request, pathname, locale);
};
/**
* Extracts the locale from the URL pathname if present.
*
* @param pathname - The pathname from the request URL.
* @returns - The locale found in the pathname, or undefined if not found.
*/
const getPathLocale = (pathname) => locales.find((locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`);
/**
* Handles the case where URLs have locale prefixes.
*
* @param request - The incoming Next.js request object.
* @param localLocale - The locale from the cookie.
* @param pathLocale - The locale extracted from the pathname.
* @param pathname - The pathname from the request URL.
* @param basePathTrailingSlash - Indicates if the basePath ends with a slash.
* @returns - The response to be returned to the client.
*/
const handlePrefix = (request, localLocale, pathLocale, pathname) => {
if (!pathLocale) {
if (isPrefetchRequest(request) && true) return handleMissingPathLocale(request, defaultLocale, pathname);
return handleMissingPathLocale(request, localLocale, pathname);
}
return handleExistingPathLocale(request, localLocale, pathLocale, pathname);
};
/**
* Handles requests where the locale is missing from the URL pathname.
*
* @param request - The incoming Next.js request object.
* @param localLocale - The locale from the cookie.
* @param pathname - The pathname from the request URL.
* @param basePathTrailingSlash - Indicates if the basePath ends with a slash.
* @returns - The response to be returned to the client.
*/
const handleMissingPathLocale = (request, localLocale, pathname) => {
let locale = localLocale ?? localeDetector$1?.(request) ?? defaultLocale;
if (!locales.includes(locale)) locale = defaultLocale;
if (prefixDefault || locale !== defaultLocale) return redirectUrl(request, constructPath(locale, pathname, basePath, appendLocaleSearchIfNeeded(request.nextUrl.search, locale)));
return rewriteUrl(request, pathname, locale);
};
/**
* Handles requests where the locale exists in the URL pathname.
*
* @param request - The incoming Next.js request object.
* @param localLocale - The locale from the cookie.
* @param pathLocale - The locale extracted from the pathname.
* @param pathname - The pathname from the request URL.
* @returns - The response to be returned to the client.
*/
/**
* Handles requests where the locale exists in the URL pathname.
*/
const handleExistingPathLocale = (request, localLocale, pathLocale, pathname) => {
if (localLocale && localLocale !== pathLocale) return redirectUrl(request, handleCookieLocaleMismatch(request, pathname, pathLocale, localLocale, basePath));
return handleDefaultLocaleRedirect(request, pathLocale, pathname);
};
/**
* The key fix for 404s without [locale] folders
*/
const handleDefaultLocaleRedirect = (request, pathLocale, pathname) => {
if (!prefixDefault && pathLocale === defaultLocale) return redirectUrl(request, `${basePath}${pathname.slice(`/${pathLocale}`.length) || "/"}`);
const internalPathname = pathname.slice(`/${pathLocale}`.length) || "/";
const searchWithLocale = appendLocaleSearchIfNeeded(request.nextUrl.search, pathLocale);
return rewriteUrl(request, searchWithLocale ? `${internalPathname}${searchWithLocale}` : internalPathname, pathLocale);
};
/**
* Handles the scenario where the locale in the cookie does not match the locale in the URL pathname.
*
* @param request - The incoming Next.js request object.
* @param pathname - The pathname from the request URL.
* @param pathLocale - The locale extracted from the pathname.
* @param localLocale - The locale from the cookie.
* @param basePath - The base path of the application.
* @returns - The new URL path with the correct locale.
*/
const handleCookieLocaleMismatch = (request, pathname, pathLocale, localLocale, basePath$1) => {
return constructPath(localLocale, pathname.replace(`/${pathLocale}`, `/${localLocale}`), basePath$1, appendLocaleSearchIfNeeded(request.nextUrl.search, localLocale));
};
/**
* Constructs a new path by combining the locale, path, basePath, and search parameters.
*
* @param locale - The locale to include in the path.
* @param path - The original path from the request.
* @param basePath - The base path of the application.
* @param [search] - The query string from the request URL (optional).
* @returns - The constructed new path.
*/
const constructPath = (locale, path, basePath$1, search) => {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
let finalPath = normalizedPath;
if (effectiveMode === "no-prefix" || effectiveMode === "search-params") {
for (const loc of locales) if (normalizedPath.startsWith(`/${loc}/`) || normalizedPath === `/${loc}`) {
finalPath = normalizedPath.slice(`/${loc}`.length) || "/";
break;
}
} else finalPath = normalizedPath.startsWith(`/${locale}`) ? normalizedPath : `/${locale}${normalizedPath}`;
const result = `${basePath$1.replace(/\/$/, "")}${finalPath}`;
return search ? `${result}${search.startsWith("?") ? "" : "?"}${search}` : result;
};
/**
* This handles the internal path Next.js sees.
* To support optional [locale] folders, we need to decide if we
* keep the locale prefix or strip it.
*/
const rewriteUrl = (request, newPath, locale) => {
const url = request.nextUrl.clone();
const pathname = newPath.split("?")[0];
if (effectiveMode !== "no-prefix" && effectiveMode !== "search-params") if (!getPathLocale(pathname)) url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`;
else url.pathname = pathname;
else {
const pathLocale = getPathLocale(pathname);
if (pathLocale) url.pathname = pathname.slice(`/${pathLocale}`.length) || "/";
else url.pathname = pathname;
}
const response = NextResponse.rewrite(url);
setLocaleInStorage(locale, { setHeader: (name, value) => {
response.headers.set(name, value);
} });
return response;
};
/**
* Redirects the request to the new path.
*
* @param request - The incoming Next.js request object.
* @param newPath - The new path to redirect to.
* @returns - The redirect response.
*/
const redirectUrl = (request, newPath) => {
const search = request.nextUrl.search;
const pathWithSearch = search && !newPath.includes("?") ? `${newPath}${search}` : newPath;
return NextResponse.redirect(new URL(pathWithSearch, request.url));
};
//#endregion
export { intlayerProxy };
//# sourceMappingURL=intlayerProxy.mjs.map