Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cold-sloths-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@opennextjs/aws": patch
---

Fix locale not properly defined when used in middleware with domains
Handle locale redirect directly in the routing layer
112 changes: 108 additions & 4 deletions packages/open-next/src/core/routing/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { NextConfig } from "config/index.js";
import type { i18nConfig } from "types/next-types";
import type { InternalEvent } from "types/open-next";
import type { DomainLocale, i18nConfig } from "types/next-types";
import type { InternalEvent, InternalResult } from "types/open-next";

import { emptyReadableStream } from "utils/stream.js";
import { debug } from "../../../adapters/logger.js";
import { constructNextUrl } from "../util.js";
import { acceptLanguage } from "./accept-header";

function isLocalizedPath(path: string): boolean {
Expand All @@ -20,6 +22,34 @@ function getLocaleFromCookie(cookies: Record<string, string>) {
: undefined;
}

// Inspired by https://github.com/vercel/next.js/blob/6d93d652e0e7ba72d9a3b66e78746dce2069db03/packages/next/src/shared/lib/i18n/detect-domain-locale.ts#L3-L25
export function detectDomainLocale({
hostname,
detectedLocale,
}: {
hostname?: string;
detectedLocale?: string;
}): DomainLocale | undefined {
const i18n = NextConfig.i18n;
if (!i18n || i18n.localeDetection === false || !i18n.domains) {
return;
}
const lowercasedLocale = detectedLocale?.toLowerCase();
for (const domain of i18n.domains) {
// We remove the port if present
const domainHostname = domain.domain.split(":", 1)[0].toLowerCase();
if (
hostname === domainHostname ||
lowercasedLocale === domain.defaultLocale.toLowerCase() ||
domain.locales?.some(
(locale) => lowercasedLocale === locale.toLowerCase(),
)
) {
return domain;
}
}
}

export function detectLocale(
internalEvent: InternalEvent,
i18n: i18nConfig,
Expand All @@ -39,9 +69,16 @@ export function detectLocale(
defaultLocale: i18n.defaultLocale,
});

return cookiesLocale ?? preferredLocale ?? i18n.defaultLocale;
const domainLocale = detectDomainLocale({
hostname: internalEvent.headers.host,
});

// TODO: handle domain based locale detection
return (
domainLocale?.defaultLocale ??
cookiesLocale ??
preferredLocale ??
i18n.defaultLocale
);
}

export function localizePath(internalEvent: InternalEvent): string {
Expand All @@ -52,6 +89,73 @@ export function localizePath(internalEvent: InternalEvent): string {
if (isLocalizedPath(internalEvent.rawPath)) {
return internalEvent.rawPath;
}

const detectedLocale = detectLocale(internalEvent, i18n);

return `/${detectedLocale}${internalEvent.rawPath}`;
}

/**
*
* @param internalEvent
* In this function, for domain locale redirect we need to rely on the host to be present and correct
* @returns `false` if no redirect is needed, `InternalResult` if a redirect is needed
*/
export function handleLocaleRedirect(
internalEvent: InternalEvent,
): false | InternalResult {
const i18n = NextConfig.i18n;
if (
!i18n ||
i18n.localeDetection === false ||
internalEvent.rawPath !== "/"
) {
return false;
}
const preferredLocale = acceptLanguage(
internalEvent.headers["accept-language"],
i18n?.locales,
);

const detectedLocale = detectLocale(internalEvent, i18n);

const domainLocale = detectDomainLocale({
hostname: internalEvent.headers.host,
});
const preferredDomain = detectDomainLocale({
detectedLocale: preferredLocale,
});

if (domainLocale && preferredDomain) {
const isPDomain = preferredDomain.domain === domainLocale.domain;
const isPLocale = preferredDomain.defaultLocale === preferredLocale;
if (!isPDomain || !isPLocale) {
const scheme = `http${preferredDomain.http ? "" : "s"}`;
const rlocale = isPLocale ? "" : preferredLocale;
return {
type: "core",
statusCode: 307,
headers: {
Location: `${scheme}://${preferredDomain.domain}/${rlocale}`,
},
body: emptyReadableStream(),
isBase64Encoded: false,
};
}
}

const defaultLocale = domainLocale?.defaultLocale ?? i18n.defaultLocale;

if (detectedLocale.toLowerCase() !== defaultLocale.toLowerCase()) {
return {
type: "core",
statusCode: 307,
headers: {
Location: constructNextUrl(internalEvent.url, `/${detectedLocale}`),
},
body: emptyReadableStream(),
isBase64Encoded: false,
};
}
return false;
}
6 changes: 5 additions & 1 deletion packages/open-next/src/core/routing/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { InternalEvent, InternalResult } from "types/open-next";
import { emptyReadableStream, toReadableStream } from "utils/stream";

import { debug } from "../../adapters/logger";
import { localizePath } from "./i18n";
import { handleLocaleRedirect, localizePath } from "./i18n";
import {
constructNextUrl,
convertFromQueryString,
Expand Down Expand Up @@ -317,6 +317,10 @@ export function handleRedirects(
): InternalResult | undefined {
const trailingSlashRedirect = handleTrailingSlashRedirect(event);
if (trailingSlashRedirect) return trailingSlashRedirect;

const localeRedirect = handleLocaleRedirect(event);
if (localeRedirect) return localeRedirect;

const { internalEvent, __rewrite } = handleRewrites(
event,
redirects.filter((r) => !r.internal),
Expand Down
3 changes: 2 additions & 1 deletion packages/open-next/src/core/routing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export function getUrlParts(url: string, isExternal: boolean) {
* @__PURE__
*/
export function constructNextUrl(baseUrl: string, path: string) {
const nextBasePath = NextConfig.basePath;
// basePath is generated as "" if not provided on latest versions of Next.js (not sure about older versions)
const nextBasePath = NextConfig.basePath ?? "";
const url = new URL(`${nextBasePath}${path}`, baseUrl);
return url.href;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/open-next/src/types/next-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,17 @@ export type Header = {
missing?: RouteHas[];
};

export interface DomainLocale {
defaultLocale: string;
domain: string;
http?: true;
locales: readonly string[];
}

export interface i18nConfig {
locales: string[];
defaultLocale: string;
domains?: DomainLocale[];
localeDetection?: false;
}
export interface NextConfig {
Expand Down
Loading
Loading