From 1b9e048033ad8ef49f5d95b2c9580bbf455468f6 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:28:18 -0700 Subject: [PATCH 1/2] fix i18n data pathname resolving --- .../shared/lib/i18n/normalize-locale-path.ts | 20 +++++++-- .../i18n-navigations-middleware.test.ts | 43 +++++++++++++++++++ .../i18n-navigations-middleware/middleware.js | 6 +++ .../next.config.js | 9 ++++ .../pages/dynamic/[id].js | 11 +++++ .../pages/index.js | 37 ++++++++++++++++ .../pages/static.js | 11 +++++ 7 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 test/e2e/i18n-navigations-middleware/i18n-navigations-middleware.test.ts create mode 100644 test/e2e/i18n-navigations-middleware/middleware.js create mode 100644 test/e2e/i18n-navigations-middleware/next.config.js create mode 100644 test/e2e/i18n-navigations-middleware/pages/dynamic/[id].js create mode 100644 test/e2e/i18n-navigations-middleware/pages/index.js create mode 100644 test/e2e/i18n-navigations-middleware/pages/static.js diff --git a/packages/next/src/shared/lib/i18n/normalize-locale-path.ts b/packages/next/src/shared/lib/i18n/normalize-locale-path.ts index d687605e14a28..01b0fa2ab6bfe 100644 --- a/packages/next/src/shared/lib/i18n/normalize-locale-path.ts +++ b/packages/next/src/shared/lib/i18n/normalize-locale-path.ts @@ -17,8 +17,17 @@ export function normalizeLocalePath( locales?: string[] ): PathLocale { let detectedLocale: string | undefined - // first item will be empty string from splitting at first char - const pathnameParts = pathname.split('/') + const isDataRoute = pathname.startsWith('/_next/data') + + // Create a pathname for locale detection, removing /_next/data/[buildId] if present + // This is because locale detection relies on path splitting and so the first part + // should be the locale. + const pathNameNoDataPrefix = isDataRoute + ? pathname.replace(/^\/_next\/data\/[^/]+/, '') + : pathname + + // Split the path for locale detection + const pathnameParts = pathNameNoDataPrefix.split('/') ;(locales || []).some((locale) => { if ( @@ -27,12 +36,17 @@ export function normalizeLocalePath( ) { detectedLocale = locale pathnameParts.splice(1, 1) - pathname = pathnameParts.join('/') || '/' return true } return false }) + // For non-data routes, we return the path with the locale stripped out. + // For data routes, we keep the path as is, since we only want to extract the locale. + if (detectedLocale && !isDataRoute) { + pathname = pathnameParts.join('/') || '/' + } + return { pathname, detectedLocale, diff --git a/test/e2e/i18n-navigations-middleware/i18n-navigations-middleware.test.ts b/test/e2e/i18n-navigations-middleware/i18n-navigations-middleware.test.ts new file mode 100644 index 0000000000000..18b84238bd616 --- /dev/null +++ b/test/e2e/i18n-navigations-middleware/i18n-navigations-middleware.test.ts @@ -0,0 +1,43 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('i18n-navigations-middleware', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should respect selected locale when navigating to a dynamic route', async () => { + const browser = await next.browser('/') + // change to "de" locale + await browser.elementByCss("[href='/de']").click() + const dynamicLink = await browser.waitForElementByCss( + "[href='/de/dynamic/1']" + ) + expect(await browser.elementById('current-locale').text()).toBe( + 'Current locale: de' + ) + + // navigate to dynamic route + await dynamicLink.click() + + // the locale should still be "de" + expect(await browser.elementById('dynamic-locale').text()).toBe( + 'Locale: de' + ) + }) + + it('should respect selected locale when navigating to a static route', async () => { + const browser = await next.browser('/') + // change to "de" locale + await browser.elementByCss("[href='/de']").click() + const staticLink = await browser.waitForElementByCss("[href='/de/static']") + expect(await browser.elementById('current-locale').text()).toBe( + 'Current locale: de' + ) + + // navigate to static route + await staticLink.click() + + // the locale should still be "de" + expect(await browser.elementById('static-locale').text()).toBe('Locale: de') + }) +}) diff --git a/test/e2e/i18n-navigations-middleware/middleware.js b/test/e2e/i18n-navigations-middleware/middleware.js new file mode 100644 index 0000000000000..cf8566123cde8 --- /dev/null +++ b/test/e2e/i18n-navigations-middleware/middleware.js @@ -0,0 +1,6 @@ +import { NextResponse } from 'next/server' + +export const config = { matcher: ['/foo'] } +export async function middleware(req) { + return NextResponse.next() +} diff --git a/test/e2e/i18n-navigations-middleware/next.config.js b/test/e2e/i18n-navigations-middleware/next.config.js new file mode 100644 index 0000000000000..73755ea908a1a --- /dev/null +++ b/test/e2e/i18n-navigations-middleware/next.config.js @@ -0,0 +1,9 @@ +/** + * @type {import('next').NextConfig} + */ +module.exports = { + i18n: { + defaultLocale: 'default', + locales: ['default', 'en', 'de'], + }, +} diff --git a/test/e2e/i18n-navigations-middleware/pages/dynamic/[id].js b/test/e2e/i18n-navigations-middleware/pages/dynamic/[id].js new file mode 100644 index 0000000000000..7f71bab87d7f5 --- /dev/null +++ b/test/e2e/i18n-navigations-middleware/pages/dynamic/[id].js @@ -0,0 +1,11 @@ +export const getServerSideProps = async ({ locale }) => { + return { + props: { + locale, + }, + } +} + +export default function Dynamic({ locale }) { + return
Locale: {locale}
+} diff --git a/test/e2e/i18n-navigations-middleware/pages/index.js b/test/e2e/i18n-navigations-middleware/pages/index.js new file mode 100644 index 0000000000000..a97c006085cb9 --- /dev/null +++ b/test/e2e/i18n-navigations-middleware/pages/index.js @@ -0,0 +1,37 @@ +import Link from 'next/link' + +export const getServerSideProps = async ({ locale }) => { + return { + props: { + locale, + }, + } +} + +export default function Home({ locale }) { + return ( +
+

Current locale: {locale}

+ Locale switch: + + Default + + + English + + + German + +
+ Test links: + Dynamic 1 + Static +
+ ) +} diff --git a/test/e2e/i18n-navigations-middleware/pages/static.js b/test/e2e/i18n-navigations-middleware/pages/static.js new file mode 100644 index 0000000000000..d2653988ff04f --- /dev/null +++ b/test/e2e/i18n-navigations-middleware/pages/static.js @@ -0,0 +1,11 @@ +export const getServerSideProps = async ({ locale }) => { + return { + props: { + locale, + }, + } +} + +export default function Static({ locale }) { + return
Locale: {locale}
+} From b1d527043f9b36f9a8206260fe87906de57a0fb4 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:41:11 -0700 Subject: [PATCH 2/2] add handling to resolveRoutes instead --- .../server/lib/router-utils/resolve-routes.ts | 11 ++++++++++ .../shared/lib/i18n/normalize-locale-path.ts | 20 +++---------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index e084205fbd0bd..f846853a58010 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -395,6 +395,17 @@ export function getResolveRoutes( normalized = normalizers.postponed.normalize(normalized, true) } + if (config.i18n) { + const curLocaleResult = normalizeLocalePath( + normalized, + config.i18n.locales + ) + + if (curLocaleResult.detectedLocale) { + parsedUrl.query.__nextLocale = curLocaleResult.detectedLocale + } + } + // If we updated the pathname, and it had a base path, re-add the // base path. if (updated) { diff --git a/packages/next/src/shared/lib/i18n/normalize-locale-path.ts b/packages/next/src/shared/lib/i18n/normalize-locale-path.ts index 01b0fa2ab6bfe..d687605e14a28 100644 --- a/packages/next/src/shared/lib/i18n/normalize-locale-path.ts +++ b/packages/next/src/shared/lib/i18n/normalize-locale-path.ts @@ -17,17 +17,8 @@ export function normalizeLocalePath( locales?: string[] ): PathLocale { let detectedLocale: string | undefined - const isDataRoute = pathname.startsWith('/_next/data') - - // Create a pathname for locale detection, removing /_next/data/[buildId] if present - // This is because locale detection relies on path splitting and so the first part - // should be the locale. - const pathNameNoDataPrefix = isDataRoute - ? pathname.replace(/^\/_next\/data\/[^/]+/, '') - : pathname - - // Split the path for locale detection - const pathnameParts = pathNameNoDataPrefix.split('/') + // first item will be empty string from splitting at first char + const pathnameParts = pathname.split('/') ;(locales || []).some((locale) => { if ( @@ -36,17 +27,12 @@ export function normalizeLocalePath( ) { detectedLocale = locale pathnameParts.splice(1, 1) + pathname = pathnameParts.join('/') || '/' return true } return false }) - // For non-data routes, we return the path with the locale stripped out. - // For data routes, we keep the path as is, since we only want to extract the locale. - if (detectedLocale && !isDataRoute) { - pathname = pathnameParts.join('/') || '/' - } - return { pathname, detectedLocale,