Skip to content

Commit 05fdb76

Browse files
cursoragents00d
andcommitted
fix(i18n): enforce cacheMaxSize: 1 on client chunk Map
Protecting both the just-written and active keys left the Map at size 2 when max was 1. Update context before setChunk so navigation sees keep === current, and fall back to keeping only the just-written key when the bound still cannot hold both. Co-authored-by: Pavel Kuzmin <Virus191288@gmail.com>
1 parent 2c9ae70 commit 05fdb76

5 files changed

Lines changed: 39 additions & 6 deletions

File tree

docs/guide/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1297,7 +1297,7 @@ For most projects the default (unlimited, no expiration) is fine — route names
12971297
`cacheMaxSize` caps:
12981298

12991299
1. **Fetch / server `CacheControl`** (HTTP payload and server loader caches)
1300-
2. **Client chunk Map** (`NuxtI18n.storage.translations`) — oldest unused `(locale, route)` chunks are evicted; the active page chunk is kept
1300+
2. **Client chunk Map** (`NuxtI18n.storage.translations`) — oldest unused `(locale, route)` chunks are evicted; the just-written and active page keys are preferred. If the limit cannot hold both (e.g. `cacheMaxSize: 1`), the just-written key wins so the bound is enforced — active `$t` still uses the view layer
13011301

13021302
- **`cacheMaxSize`** — caps entries in those caches. Useful for bounding memory.
13031303
- **`cacheTtl`** — expires fetch/server cache entries (not the live view layer). Useful for serverless or runtime-updated translations.

docs/news/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ outline: 'deep'
1212

1313
**Version**: unreleased
1414

15-
`i18n.cacheMaxSize` already limited fetch/server `CacheControl` caches. It now also FIFO-evicts entries in the client `NuxtI18n.storage.translations` Map (`locale:route` chunks), keeping the active page. Default `0` remains unlimited — route names from Vue Router are finite, so this is an optional bound for long SPA sessions with many page dictionaries.
15+
`i18n.cacheMaxSize` already limited fetch/server `CacheControl` caches. It now also FIFO-evicts entries in the client `NuxtI18n.storage.translations` Map (`locale:route` chunks), preferring the active and just-written keys. When the limit cannot hold both (e.g. `1`), the just-written key wins so the bound is enforced. Default `0` remains unlimited — route names from Vue Router are finite, so this is an optional bound for long SPA sessions with many page dictionaries.
1616

1717
## Nuxt I18n Micro v3.26.1 — Vue DevTools live inspector
1818

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.27.0",
3+
"version": "3.27.1",
44
"description": "Fast, lightweight i18n for Nuxt with strategy-based routing and minimal overhead.",
55
"keywords": [
66
"i18n",

src/runtime/utils/nuxt-i18n.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,10 @@ export class NuxtI18n extends BaseI18n {
171171

172172
/**
173173
* FIFO eviction for `storage.translations` when `cacheMaxSize` is set.
174-
* Keeps the just-written key and the active page chunk so `$t` stays valid.
174+
* Prefers keeping the just-written key and the active page chunk. When those
175+
* two differ and `max` cannot hold both (e.g. `cacheMaxSize: 1`), the just-written
176+
* key wins so the configured bound is always enforced. Active `$t` still resolves
177+
* via `cachedTranslations` / `outgoingTranslations` even if the Map drops the old key.
175178
*/
176179
private evictChunks(keep: string): void {
177180
const max = this.cacheMaxSize
@@ -183,6 +186,15 @@ export class NuxtI18n extends BaseI18n {
183186
if (key === keep || key === current) continue
184187
this.storage.translations.delete(key)
185188
}
189+
190+
// Protected set can exceed max when keep !== current (prefetch / seed while another
191+
// page is active). Drop everything except `keep` until the bound holds.
192+
if (this.storage.translations.size <= max) return
193+
for (const key of this.storage.translations.keys()) {
194+
if (this.storage.translations.size <= max) break
195+
if (key === keep) continue
196+
this.storage.translations.delete(key)
197+
}
186198
}
187199

188200
getLocale(): string {
@@ -238,13 +250,14 @@ export class NuxtI18n extends BaseI18n {
238250
* and correct for the short hot-reload window.
239251
*/
240252
applySwitchContext(locale: string, routeName: string | undefined, data: Record<string, unknown>): void {
241-
this.setChunk(locale, routeName, data)
242-
253+
// Stash / swap the view layer before setChunk so eviction sees keep === current
254+
// (otherwise cacheMaxSize: 1 would protect both the old and new route keys).
243255
this.outgoingTranslations = this.currentLocale === locale ? this.cachedTranslations : null
244256
this.cachedTranslations = data
245257

246258
this.currentLocale = locale
247259
this.currentRouteName = routeName || ''
260+
this.setChunk(locale, routeName, data)
248261
triggerRef(this.contextSignal)
249262
this.notifyContextChange('load')
250263
}

test/nuxt-i18n.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,26 @@ describe('NuxtI18n', () => {
173173
expect(i18n.storage.translations.size).toBe(20)
174174
})
175175

176+
it('cacheMaxSize 1 enforces a single Map entry across navigations', () => {
177+
const i18n = new NuxtI18n({ missingWarn: false, cacheMaxSize: 1 })
178+
179+
i18n.applySwitchContext('en', 'page-a', { title: 'A' })
180+
expect(i18n.storage.translations.size).toBe(1)
181+
expect(i18n.t('title')).toBe('A')
182+
183+
i18n.applySwitchContext('en', 'page-b', { title: 'B' })
184+
expect(i18n.storage.translations.size).toBe(1)
185+
expect(i18n.storage.translations.has(i18n.getCacheKey('en', 'page-b'))).toBe(true)
186+
expect(i18n.t('title')).toBe('B')
187+
188+
// Prefetch of another route while still on page-b: just-written wins, bound stays 1.
189+
i18n.setChunk('en', 'page-c', { title: 'C' })
190+
expect(i18n.storage.translations.size).toBe(1)
191+
expect(i18n.storage.translations.has(i18n.getCacheKey('en', 'page-c'))).toBe(true)
192+
// View layer is independent of the Map eviction.
193+
expect(i18n.t('title')).toBe('B')
194+
})
195+
176196
it('onContextChange reports load vs refresh reasons', () => {
177197
const i18n = new NuxtI18n({ missingWarn: false })
178198
const reasons: string[] = []

0 commit comments

Comments
 (0)