Skip to content

Commit 5ff89d0

Browse files
committed
refactor: re-run middleware process if language is not set
1 parent bcaacc0 commit 5ff89d0

File tree

2 files changed

+51
-14
lines changed

2 files changed

+51
-14
lines changed

packages/i18n/src/detect-language-middleware.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import prependString from '@louffee/lib/prepend-string'
12
import { NextResponse, type NextRequest } from 'next/server'
23

34
import constants from './constants'
@@ -19,8 +20,9 @@ export interface DetectLanguageMiddlewareOptions extends MatchLanguageOptions {
1920
}
2021

2122
/**
22-
* Detects the language from the request and redirects to the correct language
23-
* if the language is not set.
23+
* The `detectLanguageMiddleware()` creates a function which detects the
24+
* language from the request and redirects to the correct language if the
25+
* language is not set.
2426
*
2527
* If the language is not set, it will redirect to the `default` language. The
2628
* language is defined in the `NEXT_language` cookie and the user is redirected
@@ -55,27 +57,35 @@ function detectLanguageMiddleware({ defaultLanguage, languages, ignoredPaths }:
5557
// of the object must be "accept-language" (case sensitive). For
5658
// reference, see:
5759
// https://github.com/jshttp/negotiator/blob/master/index.js#L62C1-L63C1
58-
request.headers.forEach((value, key) => {
59-
negotiatorHeaders[key] = value
60-
})
60+
for (const [key, value] of request.headers) {
61+
if (key.toLowerCase() === 'accept-language') {
62+
negotiatorHeaders[key] = value
63+
}
64+
}
6165

6266
const isPathnameMissingLocale = !hasPathnameAnyLanguage(pathname, languages)
6367

6468
if (isPathnameMissingLocale) {
6569
const locale = matchLanguage(negotiatorHeaders, { defaultLanguage, languages })
6670

67-
const slashAfterLocale = `${pathname.startsWith('/') ? '' : '/'}`
6871
const searchParams = request.nextUrl.searchParams?.length ? `?${request.nextUrl.searchParams}` : ''
72+
const pathnameWithSlash = prependString('/', pathname)
6973

70-
const redirectPath = `${locale}${slashAfterLocale}${pathname}${searchParams}`
74+
const redirectPath = `${locale}${pathnameWithSlash}${searchParams}`
7175
const redirectBase = request.url
7276

7377
const href = `${redirectBase}${redirectPath}`
7478

75-
const response = NextResponse.next()
79+
// NOTE: We have to set the language cookie here because the `redirect()`.
80+
// So we create the NextResponse from the redirect() static method,
81+
// inject the LANGUAGE_COOKIE_NAME and return the response, which
82+
// Next.js interprets as the end of the middleware chain. See the
83+
// official documentation on how to use the `redirect()` method:
84+
// https://nextjs.org/docs/app/api-reference/functions/next-response#redirect
85+
const response = NextResponse.redirect(href)
7686
response.cookies.set(constants.LANGUAGE_COOKIE_NAME, locale)
7787

78-
return NextResponse.rewrite(href, response)
88+
return response
7989
}
8090

8191
const response = NextResponse.next()

packages/i18n/src/should-ignore-path.ts

+32-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @internal The list containing the paths to be ignored by default.
3+
*/
14
const DEFAULT_IGNORE_PATHS = [
25
'/api',
36
'/_next',
@@ -9,9 +12,35 @@ const DEFAULT_IGNORE_PATHS = [
912
'/apple-touch-icon.png',
1013
'/android-chrome-',
1114
'/safari-pinned-tab.svg',
12-
'site.webmanifest',
15+
'/site.webmanifest',
1316
] as const
1417

18+
/**
19+
* @internal The list containing the extensions of the files to be ignored.
20+
*/
21+
const IGNORED_FILE_EXTENSIONS = ['.svg', '.png', '.ico', '.xml', '.webmanifest'] as const
22+
23+
/**
24+
* @internal The `isIgnoredFileExtension()` function checks if the `pathname` is
25+
* an ignored file extension, returning the boolean-ish value.
26+
*/
27+
function isIgnoredFileExtension(pathname: string): boolean {
28+
return IGNORED_FILE_EXTENSIONS.some((extension) => pathname.endsWith(extension))
29+
}
30+
31+
/**
32+
* @internal The list containing the folder names to be ignored.
33+
*/
34+
const IGNORED_FOLDER_NAMES = ['assets'] as const
35+
36+
/**
37+
* @internal The `isIgnoredFolderName()` function checks if the `pathname` is an
38+
* ignored folder name, returning the boolean-ish value.
39+
*/
40+
function isIgnoredFolderName(pathname: string): boolean {
41+
return IGNORED_FOLDER_NAMES.some((folderName) => pathname.includes(`/${folderName}/`))
42+
}
43+
1544
/**
1645
* The `shouldIgnorePath()` function checks if the `pathname` should be ignored
1746
* based on the `ignoredPaths` array and ignored paths by default, returning the
@@ -22,14 +51,12 @@ const DEFAULT_IGNORE_PATHS = [
2251
* shouldIgnorePath('/_next/data/development/...', ['/_next', '/api']) // true
2352
* ```
2453
*/
25-
function shouldIgnorePath(pathname: string, ignoredPaths: string[] = []): boolean {
26-
if (pathname.endsWith('.svg') || pathname.includes('/assets/')) {
54+
export default function shouldIgnorePath(pathname: string, ignoredPaths: string[] = []): boolean {
55+
if (isIgnoredFileExtension(pathname) || isIgnoredFolderName(pathname)) {
2756
return true
2857
}
2958

3059
const paths = [...DEFAULT_IGNORE_PATHS, ...ignoredPaths]
3160

3261
return paths.some((ignoredPath) => pathname.startsWith(ignoredPath))
3362
}
34-
35-
export default shouldIgnorePath

0 commit comments

Comments
 (0)