diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cd95fa83e..64cfe72a8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,7 +30,7 @@ git checkout -b my-new-feature
```sh
pnpm fix
pnpm test:unit
-pnpm test:e2e
+pnpm test:spec
```
- Commit and push your changes
diff --git a/specs/fixtures/issues/2382/app.vue b/specs/fixtures/issues/2382/app.vue
new file mode 100644
index 000000000..8f62b8bf9
--- /dev/null
+++ b/specs/fixtures/issues/2382/app.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/specs/fixtures/issues/2382/locales/en-US.json b/specs/fixtures/issues/2382/locales/en-US.json
new file mode 100755
index 000000000..cb76ec7c4
--- /dev/null
+++ b/specs/fixtures/issues/2382/locales/en-US.json
@@ -0,0 +1,15 @@
+{
+ "Meta": {
+ "locale": "English"
+ },
+ "Pages": {
+ "Home": {
+ "title": "Home page",
+ "description": "Home page description"
+ },
+ "About": {
+ "title": "About page",
+ "description": "About page description"
+ }
+ }
+}
diff --git a/specs/fixtures/issues/2382/nuxt.config.ts b/specs/fixtures/issues/2382/nuxt.config.ts
new file mode 100644
index 000000000..ebdb6a1d4
--- /dev/null
+++ b/specs/fixtures/issues/2382/nuxt.config.ts
@@ -0,0 +1,19 @@
+import type { LocaleObject } from '#i18n'
+
+const locales = [{ code: 'en', iso: 'en-US', file: 'en-US.json' }] as LocaleObject[]
+
+const defaultLocale = locales[0]
+
+export default defineNuxtConfig({
+ modules: ['@nuxtjs/i18n'],
+ i18n: {
+ locales,
+ defaultLocale: defaultLocale.code,
+ langDir: 'locales/',
+ lazy: true,
+ strategy: 'no_prefix',
+ detectBrowserLanguage: {
+ fallbackLocale: defaultLocale.code
+ }
+ }
+})
diff --git a/specs/fixtures/issues/2382/package.json b/specs/fixtures/issues/2382/package.json
new file mode 100644
index 000000000..b6a6c3053
--- /dev/null
+++ b/specs/fixtures/issues/2382/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "nuxt3-test-issues-2382",
+ "private": true,
+ "scripts": {
+ "build": "nuxt build",
+ "dev": "nuxt dev",
+ "generate": "nuxt generate",
+ "preview": "nuxt preview"
+ },
+ "devDependencies": {
+ "@nuxtjs/i18n": "latest",
+ "nuxt": "latest"
+ }
+}
diff --git a/specs/fixtures/issues/2382/pages/index.vue b/specs/fixtures/issues/2382/pages/index.vue
new file mode 100644
index 000000000..e9616aada
--- /dev/null
+++ b/specs/fixtures/issues/2382/pages/index.vue
@@ -0,0 +1,5 @@
+
+ main page with link to sub page with route param
+ Some Title
+ Some Title
+
diff --git a/specs/fixtures/issues/2382/pages/level-1/[id]/index.vue b/specs/fixtures/issues/2382/pages/level-1/[id]/index.vue
new file mode 100644
index 000000000..70211b130
--- /dev/null
+++ b/specs/fixtures/issues/2382/pages/level-1/[id]/index.vue
@@ -0,0 +1,4 @@
+
+ sub page level-2 with route param id
+ Some Title
+
diff --git a/specs/fixtures/issues/2382/pages/level-1/index.vue b/specs/fixtures/issues/2382/pages/level-1/index.vue
new file mode 100644
index 000000000..6f44fff41
--- /dev/null
+++ b/specs/fixtures/issues/2382/pages/level-1/index.vue
@@ -0,0 +1,3 @@
+
+ sub page level-1
+
diff --git a/specs/issues/2382.spec.ts b/specs/issues/2382.spec.ts
new file mode 100644
index 000000000..3efe62780
--- /dev/null
+++ b/specs/issues/2382.spec.ts
@@ -0,0 +1,55 @@
+import { test, expect, describe } from 'vitest'
+import { fileURLToPath } from 'node:url'
+import { URL } from 'node:url'
+import { setup, url, createPage } from '../utils'
+import { getText } from '../helper'
+
+describe('#2382', async () => {
+ await setup({
+ rootDir: fileURLToPath(new URL(`../fixtures/issues/2382`, import.meta.url))
+ })
+
+ test('should handle navigation with dynamic routes without special character', async () => {
+ const home = url('/')
+ const page = await createPage(undefined, { locale: 'en' })
+ await page.goto(home)
+ await page.locator('#level-1-no-special-character').click()
+ await page.waitForURL('**/level-1')
+
+ expect(await getText(page, '#title')).toEqual(`sub page level-1`)
+ })
+
+ test('should handle navigation with dynamic routes with special character', async () => {
+ const home = url('/')
+ const page = await createPage(undefined, { locale: 'en' })
+ await page.goto(home)
+ await page.locator('#level-2-with-special-character').click()
+ await page.waitForURL(`**/level-1/${encodeURI('lövöl-2')}`)
+
+ expect(await getText(page, '#title')).toEqual(`sub page level-2 with route param id`)
+ })
+
+ test('should handle navigation from dynamic route with special character', async () => {
+ const home = url('/level-1/somepath-with-ö')
+ const page = await createPage(undefined, { locale: 'en' })
+ await page.goto(home)
+ await page.waitForURL(`**/${encodeURI('level-1/somepath-with-ö')}**`)
+ expect(await getText(page, '#title')).toEqual(`sub page level-2 with route param id`)
+
+ await page.locator('#home').click()
+ await page.waitForURL(/\/$/)
+ expect(await getText(page, '#title')).toEqual(`main page with link to sub page with route param`)
+ })
+
+ test('should handle navigation from dynamic route and query parameters with special character', async () => {
+ const home = url('/level-1/somepath-with-ö?foo=bär')
+ const page = await createPage(undefined, { locale: 'en' })
+ await page.goto(home)
+ await page.waitForURL(`**/${encodeURI('level-1/somepath-with-ö')}**`)
+ expect(await getText(page, '#title')).toEqual(`sub page level-2 with route param id`)
+
+ await page.locator('#home').click()
+ await page.waitForURL(/\/$/)
+ expect(await getText(page, '#title')).toEqual(`main page with link to sub page with route param`)
+ })
+})
diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts
index f27188e68..784bf7e97 100644
--- a/src/runtime/utils.ts
+++ b/src/runtime/utils.ts
@@ -315,12 +315,22 @@ export function detectRedirect({
const routePath = context.$switchLocalePath(targetLocale) || context.$localePath(toFullPath, targetLocale)
__DEBUG__ && console.log('detectRedirect: calculate routePath -> ', routePath, toFullPath)
if (isString(routePath) && routePath && !isEqual(routePath, toFullPath) && !routePath.startsWith('//')) {
+ /**
+ * NOTE: for #2382
+ * If the current path contains any special characters like whitespaces or äöü we have to make sure that these are encoded properly,
+ * otherwise the path won't match route.from.fullPath and falsy set as redirectPath.
+ * Since routePath already contains properly encoded query parameters, these have to be extracted from the route to ensure that the query is not encoded multiple times. *
+ * (Looks like an issue within vue-i18n-routing since query parameters are properly encoded)
+ */
+ const splitRoutePath = routePath.split('?')
+ splitRoutePath[0] = encodeURI(splitRoutePath[0])
+ const properlyEncodedRoutePath = splitRoutePath.join('?')
/**
* NOTE: for #1889, #2226
* If it's the same as the previous route path, respect the current route without redirecting.
* (If an empty string is set, the current route is respected. after this function return, it's pass navigate function)
*/
- redirectPath = !(route.from && route.from.fullPath === routePath) ? routePath : ''
+ redirectPath = !(route.from && route.from.fullPath === properlyEncodedRoutePath) ? properlyEncodedRoutePath : ''
}
}