News

v4.0.0

Release notes: v4.0.0 — One route model, one build for every domain, rewritten packages

Date: 2026-11-03

Version: v4.0.0

v4.0.0 release

v4.0.0 is out. Routing, the translation core, dictionary files, locale detection and every framework package were rewritten. Options, $t, $localePath, $switchLocale, the components and the composables keep their names. What changes is behaviour, and most of those changes are bugs that are gone. Most apps need few code changes, but read Breaking Changes before you upgrade: URLs, SEO tags and framework package APIs moved in places. Upgrading has every detail.

What's New?

One Build for Every Domain

locales[].baseUrl and baseDefault used to shape links only. Now they route requests too, under all four strategies and in nuxt generate:

export default defineNuxtConfig({
  i18n: {
    locales: [
      { code: 'en', baseUrl: 'https://example.com', baseDefault: true },
      { code: 'fr', baseUrl: 'https://example.com' },
      { code: 'de', baseUrl: 'https://example.de', baseDefault: true },
    ],
  },
})
  • A host serves its own locales. Links to another domain's locale become absolute URLs on that domain, and switching to it navigates there.
  • trustForwardedHost picks the domain from X-Forwarded-Host behind a proxy that overwrites it. Off by default.
  • NUXT_I18N_DOMAIN gives nuxt generate its host: run it once per domain, and each output holds only that domain's pages and dictionaries.
  • An app without baseUrl ships none of this code.

See Layers and domains and Deployment.

Routing That Agrees With the Router

The build and the runtime no longer work out localized URLs separately. Every answer comes from the routes the build generated, so a link, a switcher, hreflang and a redirect can't point at a page that doesn't exist.

  • A locale that doesn't serve a page links to its home, never to another locale's page.
  • hreflang lists only the locales that serve the page.
  • $switchLocale resolves true or false, so you can tell when a dictionary failed to load. A repeat call retries.
  • $setI18nRouteParams and useI18nHead set during SSR survive hydration. The switcher links to the translated slug, in the server HTML too.
  • A navigation takes effect when it commits: a cancelled or blocked one changes nothing.
  • Hash mode works under prefix.
  • <Greeting :locale="$getLocale()" /> on a server component sets its render locale.

One Locale Detection

One order everywhere, on the server, in the browser, in server API helpers and in Astro. Accept-Language honours q values, regional matching no longer depends on the order of locales, and it never crosses Chinese scripts. Runtime overrides now reach routing: NUXT_I18N_DEFAULT_LOCALE and runtime disabledLocales change what is actually served. See Locale detection.

One Dictionary in Every Mode

Premerged builds, mode: 'source' and dev used to disagree on many pages. They now give the same dictionary.

  • Merges are deep at every level, and fallback chains (fr → de → en) are followed to their end.
  • Translation edits in dev apply in place, without a page reload. Removed keys disappear, and a root file edit reaches every page.
  • $clearCache() reloads the current page and keeps the old text on screen until the new text lands.
  • A failed chunk keeps the current dictionary instead of showing raw keys.

Smaller Dictionary Files

  • A page's file now holds only what differs from the locale's root, in __root/<locale>/data.json. The browser loads the root once and merges each page over it, so disk use falls and the next page costs a fraction of the first.
  • Dictionary URLs carry a version. A URL with the current ?v= is pinned for a year on a Node server, and a host's static copy answers max-age=60. See Cache headers.
  • Every SSR page preloads its dictionary files, so they start with the HTML.
  • A failed read no longer sticks: the next request reads again. See When a dictionary cannot be read.

Better <i18n-t>, Plurals and Formats

  • Slots work with plural, number, date and relativeDate, and every {name} is filled, not only the first.
  • <i18n-t> is one renderer across Nuxt, Vue, React, Preact, Solid and Astro.
  • A first form starting with =0 is an explicit zero form, and the rest go by Intl.PluralRules: =0 no apples | one apple | {count} apples. See $tc.
  • New timeZone option: one calendar date on the server and in every browser.
  • {firstName}, {first-name} and {user.name} are valid placeholders.
  • $tn, $td and $tdr follow iso, as plurals do.
  • $tdr gives real future dates: in 3 days, or tomorrow with numeric: 'auto'.

Safer by Default

  • <i18n-t html> escapes params. Only the translation is trusted markup.
  • No link leaves the origin by accident: //host, /\host and blanks stay on this site.
  • The server translation cache is bounded, so URLs with made-up page names can't grow memory.
  • The DevTools save endpoints accept only existing .json files inside your translation directories, and refuse cross-site requests.

Framework Packages at 2.0

  • React, Preact, Solid: I18nT takes a components prop for rich text. New useT and useLocaleSwitch, and React Server Components support through @i18n-micro/react/server and /client. Preact no longer needs preact/compat.
  • Vue and VitePress: I18nLink keeps name, query and hash, and VitePress respects base.
  • Astro: i18nIntegration bundles your dictionaries and installs the middleware. The main entry has no node:* import, so it runs on the edge. base works.
  • Node and runtime: fork(locale) gives a cheap per-request instance, and run(locale, fn) on @i18n-micro/node scopes one to a request.
  • @i18n-micro/types-generator: no third-party dependencies, and a plain Vite and Rollup plugin.
  • @i18n-micro/devtools-ui: live updates over Vite's socket, and an Astro toolbar app.

@i18n-micro/cli

The CLI now lives in the monorepo as @i18n-micro/cli, rewritten with 16 commands instead of 28 and far fewer dependencies. check replaces validate, diff and check-duplicates, with --json and exit codes for CI. When it can't prove a key is unused, clean writes nothing. See CLI.

Documentation

The docs were rewritten from the code around one reading path: install, first translated page, translation files, routing, detection, then task guides. Old URLs redirect. Upgrading lists every change in detail.

Speed and Size

  • A server render reads dictionaries directly, with no local HTTP request and no re-parsing.
  • dir="auto" is no longer the default. Browsers spend a long time on it on large pages: with 20,000 $t elements, DOMContentLoaded in Chromium went from about 980 ms to about 115 ms.
  • serverTranslationPreload and i18n:register no longer slow every request.
  • The client bundle drops useCookie when no cookie is configured, and @i18n-micro/path-strategy is a fraction of its old size.

Test app against v3.30.3. A generated Nuxt app on one Node server, on one Mac, wrk with 24 connections for 20 s, three rotated rounds, on a pre-release build. Rows come from separate runs, so compare within a row. Hydration uses dir: 'ltr' on both sides.

Measurev3.30.3v4.0.0
4 locales, 10k catalog: requests per second384-395585-591
4 locales, 10k catalog: p95 latency106-110 ms70-72 ms
4 locales, 10k catalog: server CPU per request2.92-3.00 ms2.01-2.02 ms
12 locales, 100k catalog: requests per second362-370578-580
12 locales, 100k catalog: p95 latency115-117 ms72-73 ms
12 locales, 100k catalog: server CPU per request3.11-3.17 ms2.02-2.03 ms
Hydration, 100k page86-92 ms77-81 ms
Client JS of this app, gzip105.1 KB93.9 KB
Request time with an i18n:register subscriber, 100k chunk96-108 ms1.7 ms

A 30-minute run on the 100k catalog, on an earlier pre-release build, served 1.13 million requests with no errors, and the live heap stayed flat.

Three production apps against v3.29.5, the version they ran, on a pre-release build. Two are SSR sites and one is a SPA. Each ran on one Node server, 60 s, 8 connections, GET only, two rounds. Build time is one run each.

MeasureSSR app 1SSR app 2SPA app
Dictionary bytes on disk6.37 MB to 1.52 MB (-76%)4.43 MB to 1.52 MB (-66%)15.40 MB to 1.69 MB (-89%)
.output size-22%-14%-29%
Build time27 s to 22 s23 s to 19 s41 s to 28 s
Client JS, gzip+1.7%+0.9%+0.5%
Requests per second82 to 88237 to 240, within noise233 to 404, SPA shell only

Client JS. A real app with its own code gains a little, as above. Shared fixes grew the framework packages too, measured with Vue, React, Preact and Solid apps of minimal size on another pre-release build: about 1.4 KB to 2.5 KB gzip more. React, Preact and Solid also carry the shared rich text renderer.

Dictionary requests. A first load now takes two files, the root and the page. Its bytes stay the same (+0.3% over 103 and 89 first loads on the two SSR apps). The next page in the same document takes one small file: 4.7 KB to 1.5 KB gzip and 5.9 KB to 0.5 KB gzip on the two pages measured. Over a logged-in walk of the SPA, dictionary bytes fell 21%.

Edge. On a small demo app, the Cloudflare Worker upload went from 241.5 KiB to 250.3 KiB gzip, on a pre-release build. Dictionaries embedded in an Edge bundle use the same root and page files, but we have not measured that embed on a large catalog.

Breaking Changes

Requirements

  • Nuxt 3.18 or newer, and Node 20 or newer.
  • @i18n-micro/test-utils is no longer installed with the module. Add it yourself if your tests import it.
  • @i18n-micro/hmr is gone: translation HMR is built in. Remove the package if you installed it.
  • @i18n-micro/react needs React 18 or newer.
  • nuxt-i18n-micro-cli is replaced by @i18n-micro/cli.

Options and Config

  • i18n-plugin-hooks and i18n-plugin-define in a dependsOn no longer wait for anything, with no error. Use i18n-plugin-loader.
  • Server auto-imports are useLocaleServerMiddleware, useTranslationServerMiddleware, loadTranslationsFromServer and detectCurrentLocale. Others no longer auto-import.
  • resolveTranslationPayloadMode and the other resolveTranslationPayload* helpers are gone from nuxt-i18n-micro, along with shouldCopyTranslationPayloadsToPublic and shouldRegisterNitroServerAssets. The TranslationPayloadMode type stays.
  • $getI18nConfig() no longer has isSSG, hmr, hooks, noPrefixRedirect, routeLocales, routeDisableMeta or globalLocaleRoutes. The options themselves are unchanged.
  • locales[].baseUrl now routes requests: a locale of another domain is a 404 on this host, and links on the same host are relative. On such a build $getI18nConfig().locales holds what the host serves, and linkLocales holds them all.
  • baseUrl also sets the origin on any other host: canonical and hreflang use metaBaseUrl if set, else the default locale's baseUrl, not the request's origin.
  • excludePatterns match whole segments ('/admin' no longer matches /administrator), globs are anchored, and RegExp patterns now reach the runtime.
  • cacheMaxSize unset means at most 1000 entries on the server. cacheTtl counts from when an entry was stored, and reads no longer extend it.
  • Under no_prefix an explicit localeCookie: null in the i18n config stays off.
  • Under prefix_except_default and prefix_and_default a runtime defaultLocale is ignored with a warning. A runtime fallbackLocale must be an enabled locale.
  • routeDisableMeta in nuxt.config is honoured, and a nuxt.config entry beats $defineI18nRoute under any key form.
  • On a server in dev, the resolved config is frozen: a write to it throws.
  • A project's locales entry beats a layer's for the same code.

Composables and $ Methods

  • $switchLocale resolves true or false, it resolved nothing. $switchRoute is typed Promise<void>.
  • $localeRoute, $switchLocalePath and the switcher link a locale that doesn't serve a page to that locale's home.
  • $localePath returns a string with an http(s):, mailto: or tel: scheme as is, and $localeRoute carries it in path and fullPath. Don't pass an unchecked value to them as a link target. //host and bare names like x.com stay paths on this origin.
  • A link with no path stays on the current page: $localePath('#top'), '?p=2', '' and { query }. '#top' used to read from the site root. A bare 'about' still means /about.
  • $switchRoute stays in the app. Only a locale's own baseUrl, mailto: and tel: leave it.
  • A link to a locale with a baseUrl goes through $localePath or <i18n-link>. <NuxtLink :to="$localeRoute(x, 'fr')"> and router.push($localeRoute(x, 'fr')) stay paths of this app. In code use navigateTo($localePath(x, 'fr'), { external: true }).
  • $switchLocaleRoute and redirects follow trailingSlash.
  • $setI18nRouteParams and useI18nHead set on the server are dropped when a navigation changes the path, including /news/1 to /news/4: set them again on the new page.
  • $setI18nRouteParams read in setup is bound to that page: a late call from a page already left does nothing. It has no setter, so stub useI18n itself in tests.
  • useI18n() returns one object per app. Writing to it reaches every caller.
  • The dictionary, locale, params and head change when a navigation commits. A beforeResolve, a beforeRouteEnter or a later global guard sees the old ones. Route middleware is unaffected.
  • $switchLocale restores the stored locale when its navigation is cancelled, under the URL strategies.
  • $t interpolates a defaultValue: $t('missing', { n: 1 }, 'Got {n}') gives Got 1. $tc does too, and on an empty translation it answers ''.
  • $has(key, route) honours the route.
  • $clearCache() reloads the current page's dictionary.
  • A {param} placeholder reads only the params you pass: {constructor} stays as written.
  • $tc with a negative, fractional or huge count takes the last form with the real count. { count: null } no longer throws, and a missing plural key reaches the missing handler.
  • $tdr gives Invalid Date for an invalid input. Under a second in the past is 0 seconds ago.
  • $tn, $td and $tdr go by iso: { code: 'en', iso: 'en-GB' } formats dates as 31/10/2026. A code Intl doesn't know formats as en, and plurals by the index rule: set iso ({ code: 'ua', iso: 'uk-UA' }).
  • detectCurrentLocale is deprecated: use useLocaleServerMiddleware(event).current.

Plurals

Templates keep their meaning, with one exception. A zero form with no placeholder, then one form per category, with {count} in each form several counts share, now goes by category in locales with three or more categories (ru, uk, pl, cs, ar and others): none | {count} one | {count} few | {count} many gives 3 few and 21 one in uk. To use categories in any other template, add the =0 marker. A custom plural rule reads the raw value, so a marked template reaches it with =0 in place. See the table in Upgrading.

Components

<i18n-t>

  • html escapes params in every case and ignores all slots.
  • A slot beats a param of the same name.
  • null means not set for plural, number, date and relativeDate.
  • plural counts like $tc, with Number: 1.5 stays 1.5, "2 items" is NaN, "" is 0.
  • number, date and relativeDate keep params.
  • hideIfEmpty applies in every branch, and defaultValue stands in for a missing key in every branch.
  • A missing key under html is escaped text.
  • date and relativeDate output carries data-allow-mismatch="text", and beside a named slot the text sits in its own <span>.
  • .innerHTML and textContent attributes are dropped.
  • It is a functional component: a ref on it gives no instance.
  • <i18n-t> and <i18n-group> read the active dictionary, as $t does.

<i18n-switcher>

  • A link is a plain <a> calling $switchLocale, so router-link-active classes are gone.
  • The button is type="button" with aria-expanded. Escape and a click outside close the dropdown.
  • New classes i18n-switcher-wrapper, i18n-switcher-dropdown and i18n-switcher-button. Each link has lang, and dir when the locale sets it.
  • Default styles use logical CSS properties, and the dropdown has a larger z-index, a minimum width and a shadow.
  • The server rewrites its links when the render ends, on pages with params. A hand-written $switchLocalePath link isn't rewritten.

<i18n-link>

  • https://, mailto: and tel: match in any case. A bare report.pdf is a route, and example.com stays external. A file ending that is also a TLD (example.md) counts as a file: write https://.
  • activeStyle ignores a trailing slash, the query and the hash.

Hooks and Types

  • i18n:register is a runtime hook: nuxtApp.hook('i18n:register', …) in a plugin.
  • The exported type ModuleHooks is renamed ModuleRuntimeHooks.
  • i18n:beforeLocaleSwitch and i18n:afterLocaleSwitch were never emitted and are no longer documented.
  • ModuleOptionsExtend.isSSG is optional and deprecated: nothing sets it.

URLs and Redirects

  • /en and /en/... are a 404 under prefix_except_default, with en as the default.
  • A locale prefix in another case or spelling is a 404 (/DE/about). So are /DE/x under [...slug] and /DE beside [slug], so a CMS slug equal to a locale code in another case loses its page.
  • An unknown URL stays a 404 with autoDetectPath: '*' or 'no_prefix'. prefix keeps the redirect, as does / everywhere.
  • Another locale's custom path, unprefixed, is a 302 to the default locale's URL under prefix_except_default. Under prefix the redirect now reaches a live page. prefix_and_default stays a 404.
  • prefix with redirects: false answers 404 on an unprefixed URL, including the original URL of a page with a custom default path.
  • autoDetectPath: '*' applies the cookie and Accept-Language to unprefixed paths too.
  • Under prefix an unprefixed URL keeps the visitor's locale, on every path, whatever autoDetectPath says.
  • Under prefix_and_default an unprefixed URL is the default locale.
  • Under no_prefix a custom path renders its own locale, a restricted page renders in a locale it serves, and locales that share a path share one record.
  • A routeRules redirect without a page is not localized: /de/old is a 404, it was an empty 200.
  • Route rules reach custom paths, and are copied under no_prefix too. A static sibling such as /de/artikel/news matches /de/artikel/:slug.
  • A locale with disabled: true answers 404 on its prefix at once, and links to it stay on the page.
  • A page path in another case is a 302 to its own spelling: /T/BASIC goes to /t/basic.
  • The original URL of a page with a custom default path is a 302 to that path under prefix_except_default. It was a 404.
  • noPrefixRedirect goes by the visitor's locale: with cookie de, /about goes to /ueber-uns, not to the default locale's path.
  • A host's own prefix is a 404 on its baseDefault host under prefix_except_default: /de/x on the de host was a 200.
  • customRegexMatcher no longer 404s a real page whose first segment looks like a locale.
  • nuxi generate prerenders every static page in each locale that serves it, unless a route rule sets prerender: false: more files in the output. With baseUrl, run it once per domain with NUXT_I18N_DOMAIN.
  • A client redirect keeps the mode of its navigation: a plain <NuxtLink> adds a history entry.
  • Localized route names are consistent. Only code hard-coding localized-… names in router.push({ name }) is affected: pass the page's own name to $localeRoute.
  • Unnamed pages from extendPages are localized and get a name.
  • Under prefix, a link to another domain keeps the locale prefix.
  • The home of a domain is written without a trailing slash everywhere.
  • ?locale= counts on pages only under no_prefix. /about?locale=de no longer renders German under lang="en".
  • A plugin's setLocale('de') on /en/... no longer loads the de dictionary.

SEO and HTML Output

  • dir="auto" is no longer the default. A known RTL language gets dir="rtl", and any other locale without dir gets no attribute. To keep the old output set dir: 'auto', at a parsing cost on large pages. See Text direction.
  • disableMeta now works: the page has no hreflang, canonical or og:locale. lang and dir stay.
  • hreflang lists only the locales that serve the page. x-default appears only when the default locale serves it.
  • An unlocalized page has no hreflang or og:locale:alternate.
  • 404 pages carry <html lang dir> in the locale of the URL, and no canonical or hreflang.
  • Canonical, og:url and hreflang keep a whitelisted query (/about?page=2), on the server too.
  • Canonical and hreflang include app.baseURL when the origin comes from the request.
  • Every SSR page has <link rel="preload" as="fetch" crossorigin="anonymous"> for its dictionary files, 404 pages too. Allow it in a strict CSP. With apiBaseClientHost that host needs CORS.
  • Vary is set on every page under no_prefix, and on the locale redirects of the other strategies. Check your CDN's cache key.
  • The locale cookie is written only by a served page.

Detection and Server

  • Accept-Language honours q values, and q=0 excludes a language.
  • useLocaleServerMiddleware and useTranslationServerMiddleware use one order on every route (?locale, [locale] param or prefix, cookie, header, default), respect localeCookie: null and autoDetectLanguage: false, and cache a dictionary per locale.
  • A malformed locale cookie is ignored and left as it is, instead of failing SSR.
  • Hash mode reads only the hash-locale cookie. A lone user-locale=de no longer counts.
  • NUXT_I18N_DEFAULT_LOCALE and runtime disabledLocales change routing.
  • premerged output changed. /{apiBaseUrl}/<page>/<locale>/data.json holds only what differs from __root/<locale>/data.json. Anything that reads those files directly must merge the two. A full dictionary is still a valid file.
  • Dictionaries follow app.cdnURL. The browser fetches them from <cdnURL>/_locales/..., where it used app.baseURL. The CDN must hold _locales/ and allow your site's origin. See App baseURL and payload hosts.
  • No Nitro route cache on /{apiBaseUrl}/**, and the s-maxage=60, stale-while-revalidate header is gone. If you relied on that cache, put a CDN in front of the payload route.
  • /{apiBaseUrl}/<page without a folder>/<locale>/data.json answers with the root dictionary, it was {}. A host's 404 now passes through as a 404, where it answered 200 {}.
  • A prerender that cannot load its dictionary fails with a 500 and a warning, and with Nuxt's default failOnError the build exits 1.
  • A changed translation without a rebuild reaches a browser only when v moves: set NUXT_I18N_DATE_BUILD.
  • SSR no longer requests /{apiBaseUrl} over HTTP, unless apiBaseServerHost is set.

Translations

  • Merges are deep at every level, and fallback chains are followed to their end.
  • Merging goes by locale: the active locale's root file beats a fallback locale's page file.
  • A page without its own pages/<name>/ folder reads the root dictionary in production. It used to get {}.
  • disablePageLocales: true reads the root files only.
  • The leaving page's keys stop resolving after navigation. If a layout used keys only the previous page's file had, move them to the root file.
  • A broken locale file warns once with its path in every mode. Dev used to throw.

@i18n-micro/* Packages

Every @i18n-micro/* package is at 2.0.0, @i18n-micro/types and the new @i18n-micro/cli included.

@i18n-micro/react, preact, solid

  • Router adapters moved to /router: createReactRouterAdapter, createSolidRouterAdapter, and Preact's createBrowserHistoryAdapter. createPreactRouterAdapter is removed.
  • I18nT follows <i18n-t> above. dangerouslySetInnerHTML, innerHTML and textContent attributes are dropped.
  • I18nSwitcher renders <a href> items with aria-current="page", ▾, logical CSS and language-switcher on the button. No aria-haspopup. className adds to i18n-switcher-wrapper.
  • A link to / or a locale root is active on an exact match only (React, Preact).
  • I18nLink keeps a user onClick and style, and leaves a modified click to the browser. Query and hash don't count for the active check. Without a router, no link is active.
  • React and Preact: I18nLocalesContext, I18nDefaultLocaleContext and I18nRouterContext are one I18nConfigContext. Solid: one I18nContext holds { i18n, locales, defaultLocale, router }.
  • getCurrentPath of the adapters includes the query and hash, and a repeated key in getRoute().query is an array.
  • A nested I18nProvider without i18n keeps the outer instance, and a provider with no instance at all throws.
  • defaultLocale() without a value is fallbackLocale.
  • Solid: getCurrentPathAccessor is removed. useI18n() no longer throws without locales.
  • React: use-sync-external-store is no longer a dependency, and react-dom is an optional peer.

@i18n-micro/vue, vitepress

  • I18nT is a functional component.
  • I18nSwitcher and I18nLink follow the sections above. aria-haspopup is gone.
  • I18nLink and createVueRouterAdapter resolve name, query and hash. An unknown name throws, as RouterLink does.
  • useLocaleHead no longer builds //de/. Canonical and hreflang of one page agree (/de), and whitelisted query params reach them.
  • I18nRoutingStrategy.resolvePath may get a location object and a path with a query and hash. New optional toHref.
  • VitePress: links, the switcher, push and replace carry site.base.

@i18n-micro/astro

  • i18nIntegration bundles translationDir and installs the middleware: delete a src/middleware.ts that only copies the setup, or pass middleware: false. A src/middleware.ts of your own passes base: config.base.
  • i18nIntegration throws on messages, plural, missingHandler and routingStrategy: pass them to createI18n or createI18nMiddleware, with middleware: false. Options that never did anything are off its type.
  • loadTranslationsFromDir and loadTranslationsIntoI18n moved to @i18n-micro/astro/node, which is ESM only.
  • getGlobalRoutingStrategy, setGlobalRoutingStrategy and runWithRoutingStrategy are removed: read Astro.locals.routingStrategy.
  • clone() is fork(), and the _storage option is gone.
  • Dynamic pages are one route: /blog/hello is blog-slug, it was blog-hello. Move pages/blog-hello/ to pages/blog-slug/. A custom getRouteName gets the pattern as a third argument.
  • The URL decides the page locale. autoDetect is a 302 from / to the preferred locale's home. New localeCookie option, and redirectToDefault is implemented.
  • A page view from a same-origin page of another locale writes the locale cookie and doesn't redirect. Server islands and actions never redirect and carry Vary: Referer.
  • config.localeCodes leaves disabled locales out, and prepareLocaleRewrite answers 404 for them.
  • getI18nProps() without keys sends everything t() can answer on the page, with a dev warning above 20 KB. Pass the keys the island uses.
  • useLocaleHead: default baseUrl is site, else root-relative. Links and metas carry id.
  • <I18nT html> escapes params, and an empty translation renders empty.
  • <I18nLink> and localizePath leave external URLs, #hash and ?query alone and keep a trailing slash.
  • <I18nSwitcher> keeps the query and uses data-i18n-switcher instead of generated ids.
  • @i18n-micro/astro/client only has translate and hasTranslation: import adapters from /client/vue, /client/react, /client/preact and /client/svelte. client/vue needs vue installed.
  • Islands: has() is true for a subtree, t('') is ''. provideI18n takes a getter to follow new props.
  • getLocale and getDefaultLocale throw without the middleware.

@i18n-micro/runtime, node

  • A write to a locale that is neither active nor fallback doesn't notify subscribers.
  • addRouteTranslations(locale, route, messages, false) replaces the chunk.
  • loadFromUrls fetches in parallel and writes nothing if one fails.
  • Node: reload() swaps dictionaries instead of emptying them first, and logs no success message. A directory that is gone keeps the old ones.
  • Node, Astro, VitePress: one reader of a locales folder. A missing folder warns, a file that isn't a JSON object is reported and skipped, and messages start with [i18n].

@i18n-micro/test-utils

  • The mock follows the real translation rules: tc('a', 1.5) takes the last form, td('nope') is Invalid Date, t(miss, undefined, '') is the key, and a miss no longer logs.
  • isolated is gone from createI18nTestContext, and ./publish-smoke is removed.

@i18n-micro/types-generator

  • I18nTypesPlugin(options) is a plain Vite and Rollup plugin: no .vite(), .webpack() or .esbuild(). For webpack or esbuild, call generateTypes in a script.
  • getTypesString and generateTypes are sync. flattenKeys and isPlainTranslationObject are removed.
  • Keys are what t() reads: object paths are added, and files outside the locale layout are dropped.

@i18n-micro/devtools-ui

  • Saving accepts only an existing .json inside translationDir, and no longer creates files. A cross-site request is a 403.
  • A failed save to a reachable endpoint throws and rolls back.
  • createBridge takes a base option. Astro uses i18nDevToolsIntegration.

@i18n-micro/utils, core, path-strategy, route-strategy

For custom adapters and tooling only.

  • utils: subpaths ./route, ./route-pattern, ./build, ./payload-stats and ./payload-config are removed, along with resolveServerLocale and resolvePreferredLocale: call resolveLocale. deepMergeTranslations and mergeRouteTranslationsWithRoot are gone: use deepMergeTranslationsRecursive.
  • core: TranslationHelper drops setCache, getGeneration and lookupIn (use lookup(chunkKey, key)). BaseI18n drops ensurePinnedChunk and invalidatePin. useTranslationHelper(storage) is gone: use new TranslationHelper(storage).
  • path-strategy: switchLocaleRoute(fromLocale, toLocale, route, options) is now switchLocaleRoute(toLocale, route, options). An old JavaScript call returns a wrong result with no error. Nuxt's $switchLocaleRoute is not affected. The /prefix, /no-prefix, /prefix-except-default and /prefix-and-default entries are gone, as are getRedirect and getCanonicalPath. RouterAdapter needs getRoutes. createPathStrategy builds no absolute URLs: use new DomainPathStrategy(ctx) when locales have baseUrl.
  • route-strategy: only RouteGenerator, generateRoutes, Rules and the path helpers are exported.

Migration

  1. Check the requirements: Nuxt 3.18, Node 20, and React 18 if you use @i18n-micro/react.
  2. Update nuxt-i18n-micro and every @i18n-micro/* package together. Remove @i18n-micro/hmr. Add @i18n-micro/test-utils as a dev dependency if your tests import it. Replace nuxt-i18n-micro-cli with @i18n-micro/cli and follow its command table.
  3. Search your code for switchLocaleRoute( from @i18n-micro/path-strategy, i18n-plugin-hooks, i18n-plugin-define, localized- route names, $getI18nConfig().globalLocaleRoutes and the other removed config keys, ModuleHooks, and @ts-expect-error on i18n:register.
  4. Move markup out of <i18n-t html> params into the translation, and pass data as params.
  5. If you set baseUrl: review links built with $localeRoute, set trustForwardedHost behind a proxy, and run nuxi generate once per domain with NUXT_I18N_DOMAIN.
  6. Check your URLs against your strategy: /en under prefix_except_default, locale-like CMS slugs, routeRules redirects, and excludePatterns.
  7. Check your CDN cache key for Vary, and allow the preload link in a strict CSP. With app.cdnURL, put _locales/ on the CDN and allow CORS.
  8. If a tool reads /_locales/... files directly, merge the page file over __root/<locale>/data.json.
  9. If a global guard reads the locale or dictionary, confirm it can run before the navigation commits. Move keys that a layout took from the previous page's file into the root file.
  10. Compare a nuxi generate output or a sitemap crawl before and after: hreflang, x-default, canonical and the page list change by design.
  11. Update imports for framework packages: /router for React, Solid and Preact adapters, /node for Astro's loaders, and I18nTypesPlugin(o) for the types plugin.
  12. For Astro, delete a src/middleware.ts that only copies the setup, and rename pages/blog-hello/ style dictionaries to their pattern.
  13. Update tests that pinned router-link-active, li button, ▼, or old mock behaviour.

Details and every edge case: Upgrading.

Why It Matters

  • Correct by construction: links, redirects and SEO tags come from the routes the app really has.
  • One answer everywhere: a build, source mode and dev give the same dictionary, and every caller uses one detection order.
  • Domains: one deployment for every locale domain, with no second build.
  • Safer defaults: escaped params, bounded caches, links that stay on your origin.
  • Less to ship: smaller dictionary files, smaller routing packages, and shared logic in one place.

For the full list of changes, see Upgrading and the changelog.