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') + }) +})