๐๏ธ Translation Cache & Storage Architecture โ
๐ Overview โ
Nuxt I18n Micro v3 uses a multi-layer caching architecture for translations. Payload loading depends on translationPayloads.mode:
premerged(default) โ root, page, fallback, and layer files are merged at build time via@i18n-micro/utils/buildsourceโ compact source files are bundled into Nitro assets and merged at runtime via@i18n-micro/utils/source-loaderand@i18n-micro/utils/merge-source
This page describes how the built-in cache works and how to extend it for custom use cases (admin tools, external APIs, cache invalidation).
๐ Architecture Overview โ
Data Flow โ
๐งฑ Core Components โ
1. TranslationStorage (Client + Server) โ
File: src/runtime/utils/storage.ts
A singleton class that provides unified translation storage for both client and server. Uses Symbol.for('__NUXT_I18N_STORAGE_CACHE__') on globalThis to ensure only one instance exists, even when multiple bundles are loaded.
Key methods:
| Method | Description |
|---|---|
getFromCache(locale, routeName?) | Synchronous check: returns cached in-memory data, or null |
seedFromSsrChunks(chunks) | Seeds cache from useState('i18n-ssr-chunks') on client hydration |
load(locale, routeName?, options) | Async load with caching: checks cache first, then fetches via $fetch |
clear() | Clears the entire cache |
Cache key format: {locale}:{routeName} (e.g., en:index, fr:about)
import { translationStorage } from '../utils/storage'
// Synchronous cache check
const cached = translationStorage.getFromCache('en', 'index')
// Async load (with automatic caching)
const result = await translationStorage.load('en', 'index', {
apiBaseUrl: '_locales',
baseURL: '/',
dateBuild: '2024-01-01'
})
// result.data โ merged translations
// result.cacheKey โ cache key usedโ๏ธ Deterministic Cache Busting (i18n.dateBuild) โ
By default, this module generates dateBuild during build time using Date.now(). It is then embedded into the generated #build/i18n.strategy.mjs and used as a query parameter (?v=...) to invalidate translation fetch caches after rebuilds.
If you need reproducible builds (for example, to improve chunk cache hit rates in rolling deployments), set a stable value in nuxt.config:
export default defineNuxtConfig({
i18n: {
// Any stable string/number (git SHA, CI build number, release tag, etc.)
dateBuild: process.env.GIT_SHA ?? 'local-dev'
}
})2. loadTranslationsFromServer() (Server Only) โ
File: src/runtime/server/utils/server-loader.ts
Loads translations for a locale/page and caches the merged result in a process-global CacheControl map keyed by @i18n-micro/hmr/cache-keys (SERVER_CC_KEY).
Behavior depends on translationPayloads.mode:
| Mode | Server behavior |
|---|---|
premerged (default) | Reads a single pre-built file from Nitro storage (assets:i18n). Merging (root + page + fallback chains + layers) was done at build time by preMergeLocales in @i18n-micro/utils/build (invoked from src/module.ts). |
source | Reads compact source files from Nitro storage and merges root/page/fallback at runtime via @i18n-micro/utils/source-loader and @i18n-micro/utils/merge-source. |
import { loadTranslationsFromServer } from '../server/utils/server-loader'
// Returns { data: Translations, json: string }
const { data, json } = await loadTranslationsFromServer('en', 'index')3. SSR Payload Transfer (useState('i18n-ssr-chunks')) โ
During server-side rendering, the main plugin (01.plugin.ts) collects loaded translation chunks into useState('i18n-ssr-chunks'). Nuxt serializes this state into the HTML payload.
On the client, before the first fetch, the plugin calls translationStorage.seedFromSsrChunks() to populate TranslationStorage. This ensures zero additional HTTP requests on first page load.
NuxtI18n holds the active view-layer dictionary used by $t() and $has(). NuxtTranslationLoader switches locale/route context and merges chunks into that layer.
4. Server API Route โ
Route: /_locales/{page}/{locale}/data.json
File: src/runtime/server/routes/i18n.ts
This Nitro route serves merged translations for the active payload mode. It calls loadTranslationsFromServer() and returns the result as JSON. Cache headers are controlled by dateBuild version parameter.
๐ฅ Extending: Custom Translation Loading โ
Read from cache (server route) โ
// server/api/i18n/load-cache.[post].ts
import { defineEventHandler, readBody } from 'h3'
import { loadTranslationsFromServer } from '#imports'
export default defineEventHandler(async (event) => {
const { page, locale } = await readBody<{ page: string; locale: string }>(event)
const { data } = await loadTranslationsFromServer(locale, page)
return { locale, page, data }
})Update translations (file + invalidate cache) โ
// server/api/i18n/update.[post].ts
import { defineEventHandler, readBody, createError } from 'h3'
import { join } from 'node:path'
import { readFile, writeFile } from 'node:fs/promises'
import { deepMergeTranslations } from '@i18n-micro/utils/deep-merge'
export default defineEventHandler(async (event) => {
const { path, updates } = await readBody<{ path: string; updates: Record<string, unknown> }>(event)
if (!path || !updates) {
throw createError({ statusCode: 400, statusMessage: 'Missing path or updates' })
}
const fullPath = join('locales', path)
let existing: Record<string, unknown> = {}
try {
const content = await readFile(fullPath, 'utf-8')
existing = JSON.parse(content) as Record<string, unknown>
} catch {
// File does not exist โ create new
}
const merged = deepMergeTranslations(existing, updates)
await writeFile(fullPath, JSON.stringify(merged, null, 2), 'utf-8')
return { success: true, path, updated: merged }
})TIP
After updating translation files, the server cache is only invalidated on restart or new deployment (detected via dateBuild). In development, HMR (hmr: true) handles automatic cache invalidation when files change.
๐งน Clearing Cache โ
Programmatic cache clearing (client) โ
Use the built-in $clearCache method:
<script setup>
const { $clearCache } = useNuxtApp()
// Clears both TranslationStorage and plugin-level cache
$clearCache()
</script>Server cache behavior โ
The server-side cache (loadTranslationsFromServer) is process-global and persists until:
- The server process restarts
- A new deployment is detected (different
dateBuildvalue)
For serverless environments, each cold start has a fresh cache.
โ๏ธ Serverless Configuration โ
For serverless environments (Cloudflare Workers, AWS Lambda), the built-in cache uses in-memory Map objects. No external storage configuration is needed for the translation cache itself.
However, Nitro storage for source translation files may need configuration:
export default defineNuxtConfig({
nitro: {
storage: {
// Only needed if default file-system storage is unavailable
'assets:server': {
driver: 'cloudflare-kv-binding',
binding: 'MY_KV_NAMESPACE'
}
}
}
})๐ก Key Differences from v2 โ
| Aspect | v2 | v3 |
|---|---|---|
| Client cache | useStorage('cache') | TranslationStorage singleton (Symbol.for on globalThis) |
| SSR transfer | Runtime config | useState('i18n-ssr-chunks') via Nuxt payload |
| Server cache | Nitro cache storage | Process-global Map via Symbol.for |
| Merge logic | Client-side | Build-time (premerged) or runtime (source) via @i18n-micro/utils/* |
| Cache key format | i18n:merged:{page}:{locale} | {locale}:{routeName} |
๐ Related โ
- Performance Guide โ How caching impacts performance
- Server-Side Translations โ Using translations in server routes
- Firebase Deployment โ Deployment-specific cache considerations