UNPKG

next-intl

Version:

Internationalization (i18n) for Next.js

160 lines (156 loc) 8.2 kB
import { NextResponse } from 'next/server'; import { receiveRoutingConfig } from '../routing/config.js'; import { HEADER_LOCALE_NAME } from '../shared/constants.js'; import { matchesPathname, normalizeTrailingSlash, getLocalePrefix, getLocalizedTemplate } from '../shared/utils.js'; import getAlternateLinksHeaderValue from './getAlternateLinksHeaderValue.js'; import resolveLocale from './resolveLocale.js'; import syncCookie from './syncCookie.js'; import { sanitizePathname, isLocaleSupportedOnDomain, getNormalizedPathname, getPathnameMatch, getInternalTemplate, formatTemplatePathname, formatPathname, getBestMatchingDomain, applyBasePath, getLocaleAsPrefix } from './utils.js'; function createMiddleware(routing) { const resolvedRouting = receiveRoutingConfig(routing); return function middleware(request) { let unsafeExternalPathname; try { // Resolve potential foreign symbols (e.g. /ja/%E7%B4%84 → /ja/約)) unsafeExternalPathname = decodeURI(request.nextUrl.pathname); } catch { // In case an invalid pathname is encountered, forward // it to Next.js which in turn responds with a 400 return NextResponse.next(); } // Sanitize malicious URIs to prevent open redirect attacks due to // decodeURI doesn't escape encoded backslashes ('%5C' & '%5c') const externalPathname = sanitizePathname(unsafeExternalPathname); const { domain, locale } = resolveLocale(resolvedRouting, request.headers, request.cookies, externalPathname); const hasMatchedDefaultLocale = domain ? domain.defaultLocale === locale : locale === resolvedRouting.defaultLocale; const domainsConfig = resolvedRouting.domains?.filter(curDomain => isLocaleSupportedOnDomain(locale, curDomain)) || []; const hasUnknownHost = resolvedRouting.domains != null && !domain; function rewrite(url) { const urlObj = new URL(url, request.url); if (request.nextUrl.basePath) { urlObj.pathname = applyBasePath(urlObj.pathname, request.nextUrl.basePath); } const headers = new Headers(request.headers); headers.set(HEADER_LOCALE_NAME, locale); return NextResponse.rewrite(urlObj, { request: { headers } }); } function redirect(url, redirectDomain) { const urlObj = new URL(url, request.url); urlObj.pathname = normalizeTrailingSlash(urlObj.pathname); if (domainsConfig.length > 0 && !redirectDomain && domain) { const bestMatchingDomain = getBestMatchingDomain(domain, locale, domainsConfig); if (bestMatchingDomain) { redirectDomain = bestMatchingDomain.domain; if (bestMatchingDomain.defaultLocale === locale && resolvedRouting.localePrefix.mode === 'as-needed') { urlObj.pathname = getNormalizedPathname(urlObj.pathname, resolvedRouting.locales, resolvedRouting.localePrefix); } } } if (redirectDomain) { urlObj.host = redirectDomain; if (request.headers.get('x-forwarded-host')) { urlObj.protocol = request.headers.get('x-forwarded-proto') ?? request.nextUrl.protocol; const redirectDomainPort = redirectDomain.split(':')[1]; urlObj.port = redirectDomainPort ?? request.headers.get('x-forwarded-port') ?? ''; } } if (request.nextUrl.basePath) { urlObj.pathname = applyBasePath(urlObj.pathname, request.nextUrl.basePath); } hasRedirected = true; return NextResponse.redirect(urlObj.toString()); } const unprefixedExternalPathname = getNormalizedPathname(externalPathname, resolvedRouting.locales, resolvedRouting.localePrefix); const pathnameMatch = getPathnameMatch(externalPathname, resolvedRouting.locales, resolvedRouting.localePrefix, domain); const hasLocalePrefix = pathnameMatch != null; const isUnprefixedRouting = resolvedRouting.localePrefix.mode === 'never' || hasMatchedDefaultLocale && resolvedRouting.localePrefix.mode === 'as-needed'; let response; let internalTemplateName; let hasRedirected; let unprefixedInternalPathname = unprefixedExternalPathname; const pathnames = resolvedRouting.pathnames; if (pathnames) { let resolvedTemplateLocale; [resolvedTemplateLocale, internalTemplateName] = getInternalTemplate(pathnames, unprefixedExternalPathname, locale); if (internalTemplateName) { const pathnameConfig = pathnames[internalTemplateName]; const localeTemplate = getLocalizedTemplate(pathnameConfig, locale, internalTemplateName); if (matchesPathname(localeTemplate, unprefixedExternalPathname)) { unprefixedInternalPathname = formatTemplatePathname(unprefixedExternalPathname, localeTemplate, internalTemplateName); } else { let sourceTemplate; if (resolvedTemplateLocale) { // A localized pathname from another locale has matched sourceTemplate = getLocalizedTemplate(pathnameConfig, resolvedTemplateLocale, internalTemplateName); } else { // An internal pathname has matched that // doesn't have a localized pathname sourceTemplate = internalTemplateName; } const localePrefix = isUnprefixedRouting ? undefined : getLocalePrefix(locale, resolvedRouting.localePrefix); const template = formatTemplatePathname(unprefixedExternalPathname, sourceTemplate, localeTemplate); response = redirect(formatPathname(template, localePrefix, request.nextUrl.search)); } } } if (!response) { if (unprefixedInternalPathname === '/' && !hasLocalePrefix) { if (isUnprefixedRouting) { response = rewrite(formatPathname(unprefixedInternalPathname, getLocaleAsPrefix(locale), request.nextUrl.search)); } else { response = redirect(formatPathname(unprefixedExternalPathname, getLocalePrefix(locale, resolvedRouting.localePrefix), request.nextUrl.search)); } } else { const internalHref = formatPathname(unprefixedInternalPathname, getLocaleAsPrefix(locale), request.nextUrl.search); if (hasLocalePrefix) { const externalHref = formatPathname(unprefixedExternalPathname, pathnameMatch.prefix, request.nextUrl.search); if (resolvedRouting.localePrefix.mode === 'never') { response = redirect(formatPathname(unprefixedExternalPathname, undefined, request.nextUrl.search)); } else if (pathnameMatch.exact) { if (hasMatchedDefaultLocale && isUnprefixedRouting) { response = redirect(formatPathname(unprefixedExternalPathname, undefined, request.nextUrl.search)); } else { if (resolvedRouting.domains) { const pathDomain = getBestMatchingDomain(domain, pathnameMatch.locale, domainsConfig); if (domain?.domain !== pathDomain?.domain && !hasUnknownHost) { response = redirect(externalHref, pathDomain?.domain); } else { response = rewrite(internalHref); } } else { response = rewrite(internalHref); } } } else { response = redirect(externalHref); } } else { if (isUnprefixedRouting) { response = rewrite(internalHref); } else { response = redirect(formatPathname(unprefixedExternalPathname, getLocalePrefix(locale, resolvedRouting.localePrefix), request.nextUrl.search)); } } } } syncCookie(request, response, locale, resolvedRouting, domain); if (!hasRedirected && resolvedRouting.localePrefix.mode !== 'never' && resolvedRouting.alternateLinks && resolvedRouting.locales.length > 1) { response.headers.set('Link', getAlternateLinksHeaderValue({ routing: resolvedRouting, internalTemplateName, localizedPathnames: internalTemplateName != null && pathnames ? pathnames[internalTemplateName] : undefined, request, resolvedLocale: locale })); } return response; }; } export { createMiddleware as default };