11import { NextConfig } from "@opennextjs/aws/adapters/config/index.js" ;
2- import { localizePath } from "@opennextjs/aws/core/routing/i18n/index.js" ;
2+ import {
3+ handleLocaleRedirect ,
4+ localizePath ,
5+ } from "@opennextjs/aws/core/routing/i18n/index.js" ;
36import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js" ;
47import type { InternalEvent } from "@opennextjs/aws/types/open-next.js" ;
58import { vi } from "vitest" ;
@@ -15,16 +18,14 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => {
1518 } ;
1619} ) ;
1720
18- vi . mock ( "@opennextjs/aws/core/routing/i18n/accept-header.js" , ( ) => ( {
19- acceptLanguage : ( header : string , _ ?: string [ ] ) => ( header ? "fr" : undefined ) ,
20- } ) ) ;
21-
2221type PartialEvent = Partial <
2322 Omit < InternalEvent , "body" | "rawPath" | "query" >
2423> & { body ?: string } ;
2524
2625function createEvent ( event : PartialEvent ) : InternalEvent {
27- const [ rawPath , qs ] = ( event . url ?? "/" ) . split ( "?" , 2 ) ;
26+ const url = new URL ( event . url ?? "/" ) ;
27+ const rawPath = url . pathname ;
28+ const qs = url . search ;
2829 return {
2930 type : "core" ,
3031 method : event . method ?? "GET" ,
@@ -49,7 +50,7 @@ describe("localizePath", () => {
4950 . mockReturnValue ( undefined ) ;
5051
5152 const event = createEvent ( {
52- url : "/foo" ,
53+ url : "http://localhost /foo" ,
5354 } ) ;
5455
5556 const result = localizePath ( event ) ;
@@ -67,7 +68,7 @@ describe("localizePath", () => {
6768 } ) ;
6869
6970 const event = createEvent ( {
70- url : "/foo" ,
71+ url : "http://localhost /foo" ,
7172 headers : {
7273 "accept-language" : "fr" ,
7374 } ,
@@ -85,7 +86,7 @@ describe("localizePath", () => {
8586
8687 it ( "should return the same path if the path is already localized" , ( ) => {
8788 const event = createEvent ( {
88- url : "/fr/foo" ,
89+ url : "http://localhost /fr/foo" ,
8990 } ) ;
9091
9192 const result = localizePath ( event ) ;
@@ -95,7 +96,7 @@ describe("localizePath", () => {
9596
9697 it ( "should get locale from cookies if NEXT_LOCALE cookie is set to a valid locale" , ( ) => {
9798 const event = createEvent ( {
98- url : "/foo" ,
99+ url : "http://localhost /foo" ,
99100 cookies : {
100101 NEXT_LOCALE : "fr" ,
101102 } ,
@@ -108,7 +109,7 @@ describe("localizePath", () => {
108109
109110 it ( "should fallback on default locale if NEXT_LOCALE cookie is set to an invalid locale" , ( ) => {
110111 const event = createEvent ( {
111- url : "/foo" ,
112+ url : "http://localhost /foo" ,
112113 cookies : {
113114 NEXT_LOCALE : "pt" ,
114115 } ,
@@ -121,7 +122,7 @@ describe("localizePath", () => {
121122
122123 it ( "should use accept-language header if no cookie are present" , ( ) => {
123124 const event = createEvent ( {
124- url : "/foo" ,
125+ url : "http://localhost /foo" ,
125126 headers : {
126127 "accept-language" : "fr" ,
127128 } ,
@@ -134,11 +135,214 @@ describe("localizePath", () => {
134135
135136 it ( "should fallback to default locale if no cookie or header are set" , ( ) => {
136137 const event = createEvent ( {
137- url : "/foo" ,
138+ url : "http://localhost /foo" ,
138139 } ) ;
139140
140141 const result = localizePath ( event ) ;
141142
142143 expect ( result ) . toEqual ( "/en/foo" ) ;
143144 } ) ;
144145} ) ;
146+
147+ describe ( "handleLocaleRedirect" , ( ) => {
148+ it ( "should redirect to the localized path if the path is not localized" , ( ) => {
149+ const event = createEvent ( {
150+ url : "http://localhost" ,
151+
152+ headers : {
153+ "accept-language" : "fr" ,
154+ } ,
155+ } ) ;
156+
157+ const result = handleLocaleRedirect ( event ) ;
158+
159+ expect ( result ) . toBeTruthy ( ) ;
160+ expect ( result ! . statusCode ) . toEqual ( 307 ) ;
161+ expect ( result ! . headers ) . toEqual ( {
162+ Location : "http://localhost/fr" ,
163+ } ) ;
164+ } ) ;
165+
166+ it ( "should not redirect if the path is already localized" , ( ) => {
167+ const event = createEvent ( {
168+ url : "http://localhost/fr" ,
169+ } ) ;
170+
171+ const result = handleLocaleRedirect ( event ) ;
172+
173+ expect ( result ) . toBeFalsy ( ) ;
174+ } ) ;
175+
176+ it ( "should not redirect if not the root path" , ( ) => {
177+ const event = createEvent ( {
178+ url : "http://localhost/foo" ,
179+ } ) ;
180+
181+ const result = handleLocaleRedirect ( event ) ;
182+
183+ expect ( result ) . toBeFalsy ( ) ;
184+ } ) ;
185+
186+ describe ( "using domain" , ( ) => {
187+ it ( "should redirect to the preferred domain if the domain is different" , ( ) => {
188+ vi . spyOn ( NextConfig , "i18n" , "get" ) . mockReturnValue ( {
189+ defaultLocale : "en" ,
190+ locales : [ "en" , "en-US" , "en-CA" , "fr" ] ,
191+ domains : [
192+ {
193+ domain : "mydomain.com" ,
194+ defaultLocale : "en" ,
195+ } ,
196+ {
197+ domain : "localhost" ,
198+ defaultLocale : "fr" ,
199+ http : true ,
200+ } ,
201+ ] ,
202+ } ) ;
203+ const event = createEvent ( {
204+ url : "http://mydomain.com" ,
205+ headers : {
206+ host : "mydomain.com" ,
207+ "accept-language" : "fr" ,
208+ } ,
209+ } ) ;
210+
211+ const result = handleLocaleRedirect ( event ) ;
212+
213+ expect ( result ) . toBeTruthy ( ) ;
214+ expect ( result ! . statusCode ) . toEqual ( 307 ) ;
215+ expect ( result ! . headers ) . toEqual ( {
216+ Location : "http://localhost/" ,
217+ } ) ;
218+ } ) ;
219+
220+ it ( "should redirect to the same domain with not default locale" , ( ) => {
221+ vi . spyOn ( NextConfig , "i18n" , "get" ) . mockReturnValue ( {
222+ defaultLocale : "en" ,
223+ locales : [ "en" , "fr" , "fr-FR" ] ,
224+ domains : [
225+ {
226+ domain : "mydomain.com" ,
227+ defaultLocale : "en" ,
228+ } ,
229+ {
230+ domain : "localhost" ,
231+ defaultLocale : "fr" ,
232+ locales : [ "fr-FR" ] ,
233+ http : true ,
234+ } ,
235+ ] ,
236+ } ) ;
237+ const event = createEvent ( {
238+ url : "http://localhost" ,
239+ headers : {
240+ host : "localhost" ,
241+ "accept-language" : "fr-FR,fr;q=0.9,en;q=0.8" ,
242+ } ,
243+ } ) ;
244+
245+ const result = handleLocaleRedirect ( event ) ;
246+
247+ expect ( result ) . toBeTruthy ( ) ;
248+ expect ( result ! . statusCode ) . toEqual ( 307 ) ;
249+ expect ( result ! . headers ) . toEqual ( {
250+ Location : "http://localhost/fr-FR" ,
251+ } ) ;
252+ } ) ;
253+
254+ it ( "should redirect to different domain with not default locale" , ( ) => {
255+ vi . spyOn ( NextConfig , "i18n" , "get" ) . mockReturnValue ( {
256+ defaultLocale : "en" ,
257+ locales : [ "en" , "fr" , "fr-FR" ] ,
258+ domains : [
259+ {
260+ domain : "mydomain.com" ,
261+ defaultLocale : "en" ,
262+ } ,
263+ {
264+ domain : "localhost" ,
265+ defaultLocale : "fr" ,
266+ locales : [ "fr-FR" ] ,
267+ http : true ,
268+ } ,
269+ ] ,
270+ } ) ;
271+ const event = createEvent ( {
272+ url : "http://mydomain.com" ,
273+ headers : {
274+ host : "mydomain.com" ,
275+ "accept-language" : "fr-FR,fr;q=0.9,en;q=0.8" ,
276+ } ,
277+ } ) ;
278+
279+ const result = handleLocaleRedirect ( event ) ;
280+
281+ expect ( result ) . toBeTruthy ( ) ;
282+ expect ( result ! . statusCode ) . toEqual ( 307 ) ;
283+ expect ( result ! . headers ) . toEqual ( {
284+ Location : "http://localhost/fr-FR" ,
285+ } ) ;
286+ } ) ;
287+
288+ it ( "should not redirect if the domain and locale are the same" , ( ) => {
289+ vi . spyOn ( NextConfig , "i18n" , "get" ) . mockReturnValue ( {
290+ defaultLocale : "en" ,
291+ locales : [ "en" , "fr" , "fr-FR" ] ,
292+ domains : [
293+ {
294+ domain : "mydomain.com" ,
295+ defaultLocale : "en" ,
296+ } ,
297+ {
298+ domain : "localhost" ,
299+ defaultLocale : "fr" ,
300+ locales : [ "fr-FR" ] ,
301+ http : true ,
302+ } ,
303+ ] ,
304+ } ) ;
305+ const event = createEvent ( {
306+ url : "http://localhost/fr-FR" ,
307+ headers : {
308+ host : "localhost" ,
309+ "accept-language" : "fr-FR,fr;q=0.9,en;q=0.8" ,
310+ } ,
311+ } ) ;
312+
313+ const result = handleLocaleRedirect ( event ) ;
314+
315+ expect ( result ) . toBeFalsy ( ) ;
316+ } ) ;
317+
318+ it ( "should not redirect if locale is not found" , ( ) => {
319+ vi . spyOn ( NextConfig , "i18n" , "get" ) . mockReturnValue ( {
320+ defaultLocale : "en" ,
321+ locales : [ "en" , "fr" , "fr-FR" ] ,
322+ domains : [
323+ {
324+ domain : "mydomain.com" ,
325+ defaultLocale : "en" ,
326+ } ,
327+ {
328+ domain : "localhost" ,
329+ defaultLocale : "fr" ,
330+ locales : [ "fr-FR" ] ,
331+ http : true ,
332+ } ,
333+ ] ,
334+ } ) ;
335+ const event = createEvent ( {
336+ url : "http://localhost" ,
337+ headers : {
338+ host : "localhost" ,
339+ "accept-language" : "es" ,
340+ } ,
341+ } ) ;
342+
343+ const result = handleLocaleRedirect ( event ) ;
344+
345+ expect ( result ) . toBeFalsy ( ) ;
346+ } ) ;
347+ } ) ;
348+ } ) ;
0 commit comments