Skip to content

Commit eaac57b

Browse files
s00dcursoragent
andcommitted
fix(runtime): replace history on no_prefix locale switch
router.push duplicated identical history entries under no_prefix (#238). Use replace; keep push for prefixes. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a59c72e commit eaac57b

4 files changed

Lines changed: 103 additions & 1 deletion

File tree

docs/news/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ outline: 'deep'
66

77
# News
88

9+
## Unreleased — no_prefix locale switch history (#238)
10+
11+
**Date**: 2026-07-31
12+
13+
Switching locale under `strategy: 'no_prefix'` no longer pushes a duplicate identical history entry. `switchLocalePath` / locale switch now uses `router.replace` when the URL path is unchanged; prefix strategies still use `router.push`.
14+
15+
---
16+
917
## Nuxt I18n Micro v3.24.0 — Node SSR from `public/`, no Rollup `raw:`
1018

1119
**Date**: 2026-07-30

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nuxt-i18n-micro",
3-
"version": "3.24.1",
3+
"version": "3.24.2",
44
"description": "Nuxt I18n Micro is a lightweight, high-performance internationalization module for Nuxt, designed to handle multi-language support with minimal overhead, fast build times, and efficient runtime performance.",
55
"keywords": [
66
"i18n",

src/runtime/utils/nuxt-i18n.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,9 @@ export function createNuxtI18nPluginApi(deps: NuxtI18nPluginApiDeps) {
433433
}
434434

435435
if (isNoPrefix) {
436+
// URL stays the same under no_prefix — push would append a duplicate history entry (#238).
436437
;(switchedRoute as RouteLocationRaw & { force?: boolean }).force = true
438+
return router.replace(switchedRoute as RouteLocationRaw)
437439
}
438440

439441
return router.push(switchedRoute as RouteLocationRaw)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { PathStrategy } from '@i18n-micro/path-strategy'
2+
import type { ModuleOptionsExtend } from '@i18n-micro/types'
3+
import { describe, expect, it, vi } from 'vitest'
4+
import type { Router } from 'vue-router'
5+
import { createNuxtI18nPluginApi, NuxtI18n, NuxtTranslationLoader } from '../src/runtime/utils/nuxt-i18n'
6+
7+
/**
8+
* Issue #238: under `no_prefix` the URL does not change on locale switch, so
9+
* `router.push` appends a duplicate history entry. Switching must `replace`.
10+
*/
11+
function makeApi(strategy: ModuleOptionsExtend['strategy']) {
12+
const push = vi.fn<(to?: unknown) => Promise<undefined>>(async () => undefined)
13+
const replace = vi.fn<(to?: unknown) => Promise<undefined>>(async () => undefined)
14+
const currentRoute = {
15+
path: '/',
16+
fullPath: '/',
17+
name: 'index',
18+
params: {},
19+
query: {},
20+
hash: '',
21+
matched: [],
22+
meta: {},
23+
redirectedFrom: undefined,
24+
}
25+
26+
const router = {
27+
push,
28+
replace,
29+
currentRoute: { value: currentRoute },
30+
resolve: (to: unknown) => (typeof to === 'object' && to !== null ? { ...currentRoute, ...to } : currentRoute),
31+
} as unknown as Router
32+
33+
const i18n = new NuxtI18n({ missingWarn: false })
34+
const loader = new NuxtTranslationLoader({
35+
i18n,
36+
loadOptions: { apiBaseUrl: '_locales', baseURL: '/' },
37+
})
38+
vi.spyOn(loader, 'switchContext').mockResolvedValue(undefined)
39+
40+
const i18nStrategy = {
41+
switchLocaleRoute: () => ({ path: '/', name: 'index', force: undefined }),
42+
formatPathForResolve: (path: string) => path,
43+
} as unknown as PathStrategy
44+
45+
const { provide } = createNuxtI18nPluginApi({
46+
i18n,
47+
loader,
48+
i18nStrategy,
49+
i18nConfig: {
50+
strategy,
51+
defaultLocale: 'en',
52+
locales: [
53+
{ code: 'en', iso: 'en' },
54+
{ code: 'es', iso: 'es' },
55+
],
56+
} as ModuleOptionsExtend,
57+
router,
58+
getCurrentLocale: () => 'en',
59+
getEffectiveLocale: () => 'en',
60+
getPluginRouteName: () => 'index',
61+
getRouteName: () => 'index',
62+
i18nRouteParams: { value: {} },
63+
setLocale: vi.fn(),
64+
isValidLocale: () => true,
65+
navigateTo: vi.fn(),
66+
setMissingHandler: vi.fn(),
67+
})
68+
69+
return { provide, push, replace }
70+
}
71+
72+
describe('switchLocale history (#238)', () => {
73+
it('uses router.replace under no_prefix so history.length does not grow', async () => {
74+
const { provide, push, replace } = makeApi('no_prefix')
75+
76+
await provide.switchLocale('es')
77+
78+
expect(replace).toHaveBeenCalledTimes(1)
79+
expect(push).not.toHaveBeenCalled()
80+
const arg = replace.mock.calls[0]?.[0] as { force?: boolean; path?: string } | undefined
81+
expect(arg).toEqual(expect.objectContaining({ force: true, path: '/' }))
82+
})
83+
84+
it('keeps router.push for prefixed strategies', async () => {
85+
const { provide, push, replace } = makeApi('prefix')
86+
87+
await provide.switchLocale('es')
88+
89+
expect(push).toHaveBeenCalledTimes(1)
90+
expect(replace).not.toHaveBeenCalled()
91+
})
92+
})

0 commit comments

Comments
 (0)