Skip to content

๐Ÿ—„๏ธ 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/build
  • source โ€” compact source files are bundled into Nitro assets and merged at runtime via @i18n-micro/utils/source-loader and @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:

MethodDescription
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)

typescript
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:

ts
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:

ModeServer 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).
sourceReads 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.
typescript
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) โ€‹

ts
// 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) โ€‹

ts
// 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:

vue
<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 dateBuild value)

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:

ts
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 โ€‹

Aspectv2v3
Client cacheuseStorage('cache')TranslationStorage singleton (Symbol.for on globalThis)
SSR transferRuntime configuseState('i18n-ssr-chunks') via Nuxt payload
Server cacheNitro cache storageProcess-global Map via Symbol.for
Merge logicClient-sideBuild-time (premerged) or runtime (source) via @i18n-micro/utils/*
Cache key formati18n:merged:{page}:{locale}{locale}:{routeName}

Released under the MIT License.