Skip to content

Commit 34511ff

Browse files
conico974vicb
andauthored
Fix i18n for domain (#759)
* fix locale middleware * handle locale redirect * add test * lint fix * review fix * changeset & lint * Update packages/open-next/src/core/routing/util.ts Co-authored-by: Victor Berchet <[email protected]> --------- Co-authored-by: Victor Berchet <[email protected]>
1 parent 87b59e5 commit 34511ff

File tree

8 files changed

+370
-22
lines changed

8 files changed

+370
-22
lines changed

.changeset/cold-sloths-vanish.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
Fix locale not properly defined when used in middleware with domains
6+
Handle locale redirect directly in the routing layer

packages/open-next/src/core/routing/i18n/index.ts

+108-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { NextConfig } from "config/index.js";
2-
import type { i18nConfig } from "types/next-types";
3-
import type { InternalEvent } from "types/open-next";
2+
import type { DomainLocale, i18nConfig } from "types/next-types";
3+
import type { InternalEvent, InternalResult } from "types/open-next";
44

5+
import { emptyReadableStream } from "utils/stream.js";
56
import { debug } from "../../../adapters/logger.js";
7+
import { constructNextUrl } from "../util.js";
68
import { acceptLanguage } from "./accept-header";
79

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

25+
// Inspired by https://github.com/vercel/next.js/blob/6d93d652e0e7ba72d9a3b66e78746dce2069db03/packages/next/src/shared/lib/i18n/detect-domain-locale.ts#L3-L25
26+
export function detectDomainLocale({
27+
hostname,
28+
detectedLocale,
29+
}: {
30+
hostname?: string;
31+
detectedLocale?: string;
32+
}): DomainLocale | undefined {
33+
const i18n = NextConfig.i18n;
34+
if (!i18n || i18n.localeDetection === false || !i18n.domains) {
35+
return;
36+
}
37+
const lowercasedLocale = detectedLocale?.toLowerCase();
38+
for (const domain of i18n.domains) {
39+
// We remove the port if present
40+
const domainHostname = domain.domain.split(":", 1)[0].toLowerCase();
41+
if (
42+
hostname === domainHostname ||
43+
lowercasedLocale === domain.defaultLocale.toLowerCase() ||
44+
domain.locales?.some(
45+
(locale) => lowercasedLocale === locale.toLowerCase(),
46+
)
47+
) {
48+
return domain;
49+
}
50+
}
51+
}
52+
2353
export function detectLocale(
2454
internalEvent: InternalEvent,
2555
i18n: i18nConfig,
@@ -39,9 +69,16 @@ export function detectLocale(
3969
defaultLocale: i18n.defaultLocale,
4070
});
4171

42-
return cookiesLocale ?? preferredLocale ?? i18n.defaultLocale;
72+
const domainLocale = detectDomainLocale({
73+
hostname: internalEvent.headers.host,
74+
});
4375

44-
// TODO: handle domain based locale detection
76+
return (
77+
domainLocale?.defaultLocale ??
78+
cookiesLocale ??
79+
preferredLocale ??
80+
i18n.defaultLocale
81+
);
4582
}
4683

4784
export function localizePath(internalEvent: InternalEvent): string {
@@ -52,6 +89,73 @@ export function localizePath(internalEvent: InternalEvent): string {
5289
if (isLocalizedPath(internalEvent.rawPath)) {
5390
return internalEvent.rawPath;
5491
}
92+
5593
const detectedLocale = detectLocale(internalEvent, i18n);
94+
5695
return `/${detectedLocale}${internalEvent.rawPath}`;
5796
}
97+
98+
/**
99+
*
100+
* @param internalEvent
101+
* In this function, for domain locale redirect we need to rely on the host to be present and correct
102+
* @returns `false` if no redirect is needed, `InternalResult` if a redirect is needed
103+
*/
104+
export function handleLocaleRedirect(
105+
internalEvent: InternalEvent,
106+
): false | InternalResult {
107+
const i18n = NextConfig.i18n;
108+
if (
109+
!i18n ||
110+
i18n.localeDetection === false ||
111+
internalEvent.rawPath !== "/"
112+
) {
113+
return false;
114+
}
115+
const preferredLocale = acceptLanguage(
116+
internalEvent.headers["accept-language"],
117+
i18n?.locales,
118+
);
119+
120+
const detectedLocale = detectLocale(internalEvent, i18n);
121+
122+
const domainLocale = detectDomainLocale({
123+
hostname: internalEvent.headers.host,
124+
});
125+
const preferredDomain = detectDomainLocale({
126+
detectedLocale: preferredLocale,
127+
});
128+
129+
if (domainLocale && preferredDomain) {
130+
const isPDomain = preferredDomain.domain === domainLocale.domain;
131+
const isPLocale = preferredDomain.defaultLocale === preferredLocale;
132+
if (!isPDomain || !isPLocale) {
133+
const scheme = `http${preferredDomain.http ? "" : "s"}`;
134+
const rlocale = isPLocale ? "" : preferredLocale;
135+
return {
136+
type: "core",
137+
statusCode: 307,
138+
headers: {
139+
Location: `${scheme}://${preferredDomain.domain}/${rlocale}`,
140+
},
141+
body: emptyReadableStream(),
142+
isBase64Encoded: false,
143+
};
144+
}
145+
}
146+
147+
const defaultLocale = domainLocale?.defaultLocale ?? i18n.defaultLocale;
148+
149+
if (detectedLocale.toLowerCase() !== defaultLocale.toLowerCase()) {
150+
return {
151+
type: "core",
152+
statusCode: 307,
153+
headers: {
154+
Location: constructNextUrl(internalEvent.url, `/${detectedLocale}`),
155+
},
156+
body: emptyReadableStream(),
157+
isBase64Encoded: false,
158+
};
159+
}
160+
return false;
161+
}

packages/open-next/src/core/routing/matcher.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { InternalEvent, InternalResult } from "types/open-next";
1212
import { emptyReadableStream, toReadableStream } from "utils/stream";
1313

1414
import { debug } from "../../adapters/logger";
15-
import { localizePath } from "./i18n";
15+
import { handleLocaleRedirect, localizePath } from "./i18n";
1616
import {
1717
constructNextUrl,
1818
convertFromQueryString,
@@ -317,6 +317,10 @@ export function handleRedirects(
317317
): InternalResult | undefined {
318318
const trailingSlashRedirect = handleTrailingSlashRedirect(event);
319319
if (trailingSlashRedirect) return trailingSlashRedirect;
320+
321+
const localeRedirect = handleLocaleRedirect(event);
322+
if (localeRedirect) return localeRedirect;
323+
320324
const { internalEvent, __rewrite } = handleRewrites(
321325
event,
322326
redirects.filter((r) => !r.internal),

packages/open-next/src/core/routing/util.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ export function getUrlParts(url: string, isExternal: boolean) {
8787
* @__PURE__
8888
*/
8989
export function constructNextUrl(baseUrl: string, path: string) {
90-
const nextBasePath = NextConfig.basePath;
90+
// basePath is generated as "" if not provided on Next.js 15 (not sure about older versions)
91+
const nextBasePath = NextConfig.basePath ?? "";
9192
const url = new URL(`${nextBasePath}${path}`, baseUrl);
9293
return url.href;
9394
}

packages/open-next/src/types/next-types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,17 @@ export type Header = {
6161
missing?: RouteHas[];
6262
};
6363

64+
export interface DomainLocale {
65+
defaultLocale: string;
66+
domain: string;
67+
http?: true;
68+
locales: readonly string[];
69+
}
70+
6471
export interface i18nConfig {
6572
locales: string[];
6673
defaultLocale: string;
74+
domains?: DomainLocale[];
6775
localeDetection?: false;
6876
}
6977
export interface NextConfig {

0 commit comments

Comments
 (0)