News

v3.22.0

Release notes: v3.22.0 — Formats, Cache & Translation Memory

Nuxt I18n Micro v3.22.0 — Formats, Cache & Translation Memory

Date: 2026-07-30

Version: v3.22.0

v3.22.0 release

We're announcing v3.22.0: Vue I18n-style named number/date formats with a formatter cache, CDN-friendly translation payload caching (content fingerprint + httpCacheDuration), and a Translation Memory API to read and replace the active dictionary at runtime.

What's New?

Named numberFormats / datetimeFormats + formatter cache

Vue I18n-compatible named formats and faster Intl formatting:

  • Config: numberFormats / datetimeFormats per locale
  • API: $tn(1000, 'currency'), $td(date, 'short') (+ locale override and option merges)
  • FormatService caches Intl.NumberFormat / DateTimeFormat / RelativeTimeFormat by locale+options

See Methods — $tn / $td and Module options.

dateBuild defaults to a content fingerprint

?v= on /_locales requests used to default to Date.now(), while responses are served immutable for a year. Every rebuild therefore invalidated every client's dictionary, even when no translation had changed.

The default is now a SHA-256 fingerprint of the translation sources, in layer order. Rebuilds with untouched translations keep the same URL. Setting dateBuild explicitly still wins. See Module options — dateBuild.

httpCacheDuration for /_locales payloads

The built-in /{apiBaseUrl}/:page/:locale/data.json route now sends HTTP Cache-Control for browsers and CDN:

  • Option: httpCacheDuration (seconds, default 31536000 / 1 year)
  • Header: Cache-Control: public, max-age=…, immutable
  • Safe with existing dateBuild cache-busting (?v=...); 0 disables the header; not applied in development

Analog of @nuxtjs/i18n v10.2.0 experimental.httpCacheDuration, as an explicit response header.

Compressed public payloads

nitro.compressPublicAssets now reaches the translation payloads copied into the public directory (.gz + .br). Nitro compresses public assets before the copy happens, so previously the payloads were the one uncompressed part of a static build — 6.65 MB raw versus 1.01 MB gzip on the playground index. The module still never enables compression on its own. See Performance.

resolveTranslations() and setTranslation(key, value)

Runtime helpers to read and replace the active translation dictionary in memory:

  • resolveTranslations() returns the full tree that $t() reads from for the active locale and route — useful for DevTools, tests, and runtime editors. When a second layer is live (fallback locale or Nuxt page-transition hot-reload), the dump keeps nested shape via mergeTranslationLayers and only uses flat dotted keys for scalar-vs-object collisions that a single tree cannot otherwise express.
  • setTranslation(key, value) replaces the value at a key (top-level, dotted path, object, string, or scalar). This is a replace, not a merge; use $mergeTranslations when siblings should survive.
  • Available on useI18n(), useNuxtApp(), and framework i18n instances inherited from @i18n-micro/core.
  • @i18n-micro/test-utils adds matching resolveTranslations / setTranslation helpers for unit tests.

getAllTranslations() removed from Vue package

Use resolveTranslations() for the active lookup tree. For a full storage dump in Vue, use getStorage() or getRouteCache().

Breaking Changes

Nothing in the public config surface requires app code edits for typical usage. These matter if you reached into runtime internals:

  • The SSR payload moved from useState('i18n-ssr-chunks') to nuxtApp.payload.data['i18n-ssr-chunks']. It still carries full translation chunks for every (locale, route) loaded during SSR — the same dictionaries as before, in a slot Nuxt can externalize to _payload.json on prerender. useState('i18n-ssr-chunks') now returns nothing. Reading translations that way was never supported, but it was described in the cache API docs, so it is called out here.
  • NuxtTranslationLoaderOptions exposes setSsrChunk. The loader writes each merged chunk into payload.data on the server as it loads. getSsrChunks is not used.
  • nitro.compressPublicAssets now emits .gz / .br next to the copied payloads. Expect more files in public output if you had it enabled. Deploy tooling that enumerates the translation directory may need to account for them.
  • Payload URLs change once on upgrade, because ?v= switches from a build timestamp to a content fingerprint. Clients re-download the dictionary on the first request after the upgrade and then keep it across subsequent deploys — which is the point.
  • getAllTranslations() removed from @i18n-micro/vue. Use resolveTranslations() / getStorage() / getRouteCache() instead.

Bug Fixes

  • <i18n-switcher> — omits locales with disabled: true from the dropdown. $getLocales() still returns the full list for your own code, while built-in SEO/meta output excludes disabled locales.
  • Switcher styles — cursor: not-allowed applies to the current locale via activeLinkStyle / customActiveLinkStyle; restored customDisabledLinkStyle for backward compatibility and apply it to the switcher button when the current locale is disabled: true.
  • Dev HMR — a write that leaves file content unchanged (editor save-on-blur, formatter rewrite) no longer triggers a re-merge. Root-locale changes rebuild every page for that locale, so this was expensive to spend on a no-op.
  • Dev HMR — pages are re-merged concurrently on a root-locale change instead of one after another.
  • Page transitions — same-locale navigation keeps the outgoing chunk as a fallthrough layer until page:transition:finish, instead of an expensive deep merge on every nav.

Why It Matters

  • Vue I18n-style formatting without pulling in a heavier stack.
  • CDN-friendly translation URLs that stay stable across rebuilds when the dictionary did not change.
  • A supported runtime API for inspecting and patching the live dictionary — DevTools, tests, and editors no longer need private helpers.

Full changelog will be published with the release on GitHub.


Fix: infinite redirect loop with app.baseURL + strategy: 'prefix' (#234)

Date: 2026-07-03

Server-side redirect plugin (06.redirect.ts) now strips runtimeConfig.app.baseURL from getRequestURL(event).pathname before locale detection. Fixes ERR_TOO_MANY_REDIRECTS when the app is served under a sub-path (e.g. /examples → /examples/ja once, not /examples/ja/examples/ja/...).