diff --git a/packages/next/src/shared/lib/i18n/get-locale-redirect.ts b/packages/next/src/shared/lib/i18n/get-locale-redirect.ts
index 6016b8e2a0580..102b9997e4a37 100644
--- a/packages/next/src/shared/lib/i18n/get-locale-redirect.ts
+++ b/packages/next/src/shared/lib/i18n/get-locale-redirect.ts
@@ -5,6 +5,7 @@ import { denormalizePagePath } from '../page-path/denormalize-page-path'
import { detectDomainLocale } from './detect-domain-locale'
import { formatUrl } from '../router/utils/format-url'
import { getCookieParser } from '../../../server/api-utils/get-cookie-parser'
+import { removePathPrefix } from '../router/utils/remove-path-prefix'
interface Options {
defaultLocale: string
@@ -105,7 +106,21 @@ export function getLocaleRedirect({
}
}
- if (detectedLocale.toLowerCase() !== defaultLocale.toLowerCase()) {
+ let invokePath =
+ typeof headers?.['x-invoke-path'] === 'string'
+ ? headers['x-invoke-path']
+ : undefined
+ if (nextConfig.basePath && invokePath) {
+ invokePath = removePathPrefix(invokePath, nextConfig.basePath)
+ }
+
+ // Avoid infinite redirects when the detected user locale is same as a non-default domain locale
+ const isDetectedLocaleSameAsInvokePath = invokePath === `/${detectedLocale}`
+
+ if (
+ !isDetectedLocaleSameAsInvokePath &&
+ detectedLocale.toLowerCase() !== defaultLocale.toLowerCase()
+ ) {
return formatUrl({
...urlParsed,
pathname: `${nextConfig.basePath || ''}/${detectedLocale}${
diff --git a/test/e2e/i18n-preferred-locale-detection/app/middleware.js b/test/e2e/i18n-preferred-locale-detection/app/middleware.js
new file mode 100644
index 0000000000000..6f6759070eb1c
--- /dev/null
+++ b/test/e2e/i18n-preferred-locale-detection/app/middleware.js
@@ -0,0 +1,4 @@
+export async function middleware() {
+ const noop = () => {}
+ noop()
+}
diff --git a/test/e2e/i18n-preferred-locale-detection/app/next.config.js b/test/e2e/i18n-preferred-locale-detection/app/next.config.js
new file mode 100644
index 0000000000000..34fa51cf311e7
--- /dev/null
+++ b/test/e2e/i18n-preferred-locale-detection/app/next.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ i18n: {
+ locales: ['en', 'id'],
+ defaultLocale: 'en',
+ },
+ experimental: {
+ clientRouterFilter: true,
+ clientRouterFilterRedirects: true,
+ },
+}
diff --git a/test/e2e/i18n-preferred-locale-detection/app/pages/index.js b/test/e2e/i18n-preferred-locale-detection/app/pages/index.js
new file mode 100644
index 0000000000000..5fda4389161c3
--- /dev/null
+++ b/test/e2e/i18n-preferred-locale-detection/app/pages/index.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+export const getServerSideProps = async ({ locale }) => {
+ return {
+ props: {
+ locale,
+ },
+ }
+}
+
+export default function Home({ locale }) {
+ return (
+
+
Index
+
{locale}
+
+ To new
+
+
+ )
+}
diff --git a/test/e2e/i18n-preferred-locale-detection/app/pages/new.js b/test/e2e/i18n-preferred-locale-detection/app/pages/new.js
new file mode 100644
index 0000000000000..fa58d3f8c6c1b
--- /dev/null
+++ b/test/e2e/i18n-preferred-locale-detection/app/pages/new.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+export const getServerSideProps = async ({ locale }) => {
+ return {
+ props: {
+ locale,
+ },
+ }
+}
+
+export default function New({ locale }) {
+ return (
+
+
New
+
{locale}
+
+ To index (No Locale Specified)
+
+
+ )
+}
diff --git a/test/e2e/i18n-preferred-locale-detection/i18n-preferred-locale-detection.test.ts b/test/e2e/i18n-preferred-locale-detection/i18n-preferred-locale-detection.test.ts
new file mode 100644
index 0000000000000..457968d4aba78
--- /dev/null
+++ b/test/e2e/i18n-preferred-locale-detection/i18n-preferred-locale-detection.test.ts
@@ -0,0 +1,63 @@
+import type { Request } from 'playwright'
+import { join } from 'path'
+import { FileRef, nextTestSetup } from 'e2e-utils'
+
+describe('i18-preferred-locale-redirect', () => {
+ const { next } = nextTestSetup({
+ files: new FileRef(join(__dirname, './app/')),
+ })
+
+ it('should request a path prefixed with my preferred detected locale when accessing index', async () => {
+ const browser = await next.browser('/new', {
+ locale: 'id',
+ })
+
+ let requestedPreferredLocalePathCount = 0
+ browser.on('request', (request: Request) => {
+ if (new URL(request.url(), 'http://n').pathname === '/id') {
+ requestedPreferredLocalePathCount++
+ }
+ })
+
+ const goToIndex = async () => {
+ await browser.get(next.url)
+ }
+
+ await expect(goToIndex()).resolves.not.toThrow(/ERR_TOO_MANY_REDIRECTS/)
+
+ await browser.waitForElementByCss('#index')
+
+ expect(await browser.elementByCss('#index').text()).toBe('Index')
+ expect(await browser.elementByCss('#current-locale').text()).toBe('id')
+
+ expect(requestedPreferredLocalePathCount).toBe(1)
+ })
+
+ it('should not request a path prefixed with my preferred detected locale when clicking link to index from a non-locale-prefixed path', async () => {
+ const browser = await next.browser('/new', {
+ locale: 'id',
+ })
+
+ await browser
+ .waitForElementByCss('#to-index')
+ .click()
+ .waitForElementByCss('#index')
+
+ expect(await browser.elementByCss('#index').text()).toBe('Index')
+ expect(await browser.elementByCss('#current-locale').text()).toBe('en')
+ })
+
+ it('should request a path prefixed with my preferred detected locale when clicking link to index from a locale-prefixed path', async () => {
+ const browser = await next.browser('/id/new', {
+ locale: 'id',
+ })
+
+ await browser
+ .waitForElementByCss('#to-index')
+ .click()
+ .waitForElementByCss('#index')
+
+ expect(await browser.elementByCss('#index').text()).toBe('Index')
+ expect(await browser.elementByCss('#current-locale').text()).toBe('id')
+ })
+})