Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit 5836142

Browse files
authored
feat(core): support locale-based domain redirects [wip] (#1299)
1 parent 99d0b56 commit 5836142

File tree

3 files changed

+164
-12
lines changed

3 files changed

+164
-12
lines changed

packages/libs/core/src/route/locale.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import { DomainData, Manifest, RoutesManifest } from "../types";
1+
import { Manifest, Request, RoutesManifest } from "../types";
22
import { IncomingMessage } from "http";
3+
import { parse } from "cookie";
34

45
export const findDomainLocale = (
56
req: IncomingMessage,
67
manifest: RoutesManifest
78
): string | null => {
8-
const domains =
9-
manifest.i18n && manifest.i18n.domains ? manifest.i18n.domains : null;
9+
const domains = manifest.i18n?.domains;
1010
if (domains) {
1111
const hostHeaders = req.headers.host?.split(",");
1212
if (hostHeaders && hostHeaders.length > 0) {
1313
const host = hostHeaders[0];
14-
const matchedDomain = domains.find(
15-
(d): DomainData | boolean => d.domain === host
16-
);
14+
const matchedDomain = domains.find((d) => d.domain === host);
1715

1816
if (matchedDomain) {
1917
return matchedDomain.defaultLocale;
@@ -87,7 +85,7 @@ export const getAcceptLanguageLocale = async (
8785
routesManifest: RoutesManifest
8886
) => {
8987
if (routesManifest.i18n) {
90-
const defaultLocale = routesManifest.i18n.defaultLocale;
88+
const defaultLocale = routesManifest.i18n.defaultLocale?.toLowerCase();
9189
const locales = new Set(
9290
routesManifest.i18n.locales.map((locale) => locale.toLowerCase())
9391
);
@@ -128,3 +126,85 @@ export function getLocalePrefixFromUri(
128126

129127
return "";
130128
}
129+
130+
/**
131+
* Get a redirect to the locale-specific domain. Returns undefined if no redirect found.
132+
* @param req
133+
* @param routesManifest
134+
*/
135+
export async function getLocaleDomainRedirect(
136+
req: Request,
137+
routesManifest: RoutesManifest
138+
): Promise<string | undefined> {
139+
// Redirect to correct domain based on user's language
140+
const domains = routesManifest.i18n?.domains;
141+
const hostHeaders = req.headers.host;
142+
if (domains && hostHeaders && hostHeaders.length > 0) {
143+
const host = hostHeaders[0].value.split(":")[0];
144+
const languageHeader = req.headers["accept-language"];
145+
const acceptLanguage = languageHeader && languageHeader[0]?.value;
146+
147+
const headerCookies = req.headers.cookie
148+
? req.headers.cookie[0]?.value
149+
: undefined;
150+
// Use cookies first, otherwise use the accept-language header
151+
let acceptLanguages: string[] = [];
152+
let nextLocale;
153+
if (headerCookies) {
154+
const cookies = parse(headerCookies);
155+
nextLocale = cookies["NEXT_LOCALE"];
156+
}
157+
158+
if (nextLocale) {
159+
acceptLanguages = [nextLocale.toLowerCase()];
160+
} else {
161+
const Accept = await import("@hapi/accept");
162+
acceptLanguages = Accept.languages(acceptLanguage).map((lang) =>
163+
lang.toLowerCase()
164+
);
165+
}
166+
167+
// Try to find the right domain to redirect to if needed
168+
// First check current domain can support any preferred language, if so do not redirect
169+
const currentDomainData = domains.find(
170+
(domainData) => domainData.domain === host
171+
);
172+
173+
if (currentDomainData) {
174+
for (const language of acceptLanguages) {
175+
if (
176+
currentDomainData.defaultLocale?.toLowerCase() === language ||
177+
currentDomainData.locales
178+
?.map((locale) => locale.toLowerCase())
179+
.includes(language)
180+
) {
181+
return undefined;
182+
}
183+
}
184+
}
185+
186+
// Try to find domain whose default locale matched preferred language in order
187+
for (const language of acceptLanguages) {
188+
for (const domainData of domains) {
189+
if (domainData.defaultLocale.toLowerCase() === language) {
190+
return `${domainData.domain}${req.uri}`;
191+
}
192+
}
193+
}
194+
195+
// Try to find domain whose supported locales matches preferred language in order
196+
for (const language of acceptLanguages) {
197+
for (const domainData of domains) {
198+
if (
199+
domainData.locales
200+
?.map((locale) => locale.toLowerCase())
201+
.includes(language)
202+
) {
203+
return `${domainData.domain}${req.uri}`;
204+
}
205+
}
206+
}
207+
}
208+
209+
return undefined;
210+
}

packages/libs/core/src/route/redirect.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { STATUS_CODES } from "http";
2-
import { addDefaultLocaleToPath, getAcceptLanguageLocale } from "./locale";
2+
import {
3+
addDefaultLocaleToPath,
4+
getAcceptLanguageLocale,
5+
getLocaleDomainRedirect
6+
} from "./locale";
37
import { compileDestination, matchPath } from "../match";
48
import { Manifest, Request, RedirectRoute, RoutesManifest } from "../types";
59
import { parse } from "cookie";
@@ -85,7 +89,7 @@ export function getDomainRedirectPath(
8589

8690
/**
8791
* Redirect from root to locale.
88-
* @param request
92+
* @param req
8993
* @param routesManifest
9094
* @param manifest
9195
*/
@@ -96,7 +100,16 @@ export async function getLanguageRedirectPath(
96100
): Promise<string | undefined> {
97101
// Check for disabled locale detection: https://nextjs.org/docs/advanced-features/i18n-routing#disabling-automatic-locale-detection
98102
if (routesManifest.i18n?.localeDetection === false) {
99-
return;
103+
return undefined;
104+
}
105+
106+
// Try to get locale domain redirect
107+
const localeDomainRedirect = await getLocaleDomainRedirect(
108+
req,
109+
routesManifest
110+
);
111+
if (localeDomainRedirect) {
112+
return localeDomainRedirect;
100113
}
101114

102115
const basePath = routesManifest.basePath;

packages/libs/core/tests/route/locale.test.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Manifest, RoutesManifest } from "../../src";
1+
import { Headers, Manifest, RoutesManifest } from "../../src";
22
import {
33
addDefaultLocaleToPath,
44
dropLocaleFromPath,
55
getAcceptLanguageLocale,
6-
findDomainLocale
6+
findDomainLocale,
7+
getLocaleDomainRedirect
78
} from "../../src/route/locale";
89
import { IncomingMessage } from "http";
910

@@ -88,6 +89,64 @@ describe("Locale Utils Tests", () => {
8889
});
8990
});
9091

92+
describe("getLocaleDomainRedirect()", () => {
93+
let routesManifest: RoutesManifest;
94+
95+
beforeAll(() => {
96+
routesManifest = {
97+
basePath: "",
98+
headers: [],
99+
redirects: [],
100+
rewrites: [],
101+
i18n: {
102+
locales: ["en", "fr", "nl"],
103+
defaultLocale: "en",
104+
domains: [
105+
{
106+
domain: "next-serverless.fr",
107+
defaultLocale: "fr"
108+
},
109+
{
110+
domain: "next-serverless.com",
111+
defaultLocale: "en",
112+
locales: ["es", "en-GB"]
113+
},
114+
{
115+
domain: "next-serverless.nl",
116+
defaultLocale: "nl"
117+
}
118+
]
119+
}
120+
};
121+
});
122+
123+
it.each`
124+
host | acceptLang | cookie | expectedRedirect
125+
${"next-serverless.com"} | ${"en"} | ${undefined} | ${undefined}
126+
${"next-serverless.com"} | ${"fr"} | ${undefined} | ${"next-serverless.fr/test"}
127+
${"next-serverless.com"} | ${"fr;q=0.7, nl;q=0.9"} | ${undefined} | ${"next-serverless.nl/test"}
128+
${"next-serverless.fr"} | ${"es"} | ${undefined} | ${"next-serverless.com/test"}
129+
${"next-serverless.fr"} | ${"en-GB"} | ${undefined} | ${"next-serverless.com/test"}
130+
${"next-serverless.com"} | ${"en"} | ${"NEXT_LOCALE=fr"} | ${"next-serverless.fr/test"}
131+
`(
132+
"host: $host with accept-language: $acceptLang and cookie: $cookie redirects to $expectedRedirect",
133+
async ({ host, acceptLang, cookie, expectedRedirect }) => {
134+
const req = {
135+
headers: {
136+
host: [{ key: "Host", value: host }],
137+
"accept-language": [{ key: "Accept-Language", value: acceptLang }],
138+
cookie: [{ key: "Cookie", value: cookie }]
139+
},
140+
uri: "/test"
141+
};
142+
143+
expect(await getLocaleDomainRedirect(req, routesManifest)).toBe(
144+
expectedRedirect
145+
);
146+
}
147+
);
148+
});
149+
91150
describe("dropLocaleFromPath()", () => {
92151
let routesManifest: RoutesManifest;
93152

0 commit comments

Comments
 (0)