v4.0.0
Date: 2026-11-03
Version: v4.0.0

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.
trustForwardedHostpicks the domain fromX-Forwarded-Hostbehind a proxy that overwrites it. Off by default.NUXT_I18N_DOMAINgivesnuxt generateits host: run it once per domain, and each output holds only that domain's pages and dictionaries.- An app without
baseUrlships 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.
hreflanglists only the locales that serve the page.$switchLocaleresolvestrueorfalse, so you can tell when a dictionary failed to load. A repeat call retries.$setI18nRouteParamsanduseI18nHeadset 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 answersmax-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,dateandrelativeDate, 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
=0is an explicit zero form, and the rest go byIntl.PluralRules:=0 no apples | one apple | {count} apples. See$tc. - New
timeZoneoption: one calendar date on the server and in every browser. {firstName},{first-name}and{user.name}are valid placeholders.$tn,$tdand$tdrfollowiso, as plurals do.$tdrgives real future dates:in 3 days, ortomorrowwithnumeric: 'auto'.
Safer by Default
<i18n-t html>escapesparams. Only the translation is trusted markup.- No link leaves the origin by accident:
//host,/\hostand 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
.jsonfiles inside your translation directories, and refuse cross-site requests.
Framework Packages at 2.0
- React, Preact, Solid:
I18nTtakes acomponentsprop for rich text. NewuseTanduseLocaleSwitch, and React Server Components support through@i18n-micro/react/serverand/client. Preact no longer needspreact/compat. - Vue and VitePress:
I18nLinkkeepsname,queryandhash, and VitePress respectsbase. - Astro:
i18nIntegrationbundles your dictionaries and installs the middleware. The main entry has nonode:*import, so it runs on the edge.baseworks. - Node and runtime:
fork(locale)gives a cheap per-request instance, andrun(locale, fn)on@i18n-micro/nodescopes 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$telements, DOMContentLoaded in Chromium went from about 980 ms to about 115 ms.serverTranslationPreloadandi18n:registerno longer slow every request.- The client bundle drops
useCookiewhen no cookie is configured, and@i18n-micro/path-strategyis 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.
| Measure | v3.30.3 | v4.0.0 |
|---|---|---|
| 4 locales, 10k catalog: requests per second | 384-395 | 585-591 |
| 4 locales, 10k catalog: p95 latency | 106-110 ms | 70-72 ms |
| 4 locales, 10k catalog: server CPU per request | 2.92-3.00 ms | 2.01-2.02 ms |
| 12 locales, 100k catalog: requests per second | 362-370 | 578-580 |
| 12 locales, 100k catalog: p95 latency | 115-117 ms | 72-73 ms |
| 12 locales, 100k catalog: server CPU per request | 3.11-3.17 ms | 2.02-2.03 ms |
| Hydration, 100k page | 86-92 ms | 77-81 ms |
| Client JS of this app, gzip | 105.1 KB | 93.9 KB |
Request time with an i18n:register subscriber, 100k chunk | 96-108 ms | 1.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.
| Measure | SSR app 1 | SSR app 2 | SPA app |
|---|---|---|---|
| Dictionary bytes on disk | 6.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 time | 27 s to 22 s | 23 s to 19 s | 41 s to 28 s |
| Client JS, gzip | +1.7% | +0.9% | +0.5% |
| Requests per second | 82 to 88 | 237 to 240, within noise | 233 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-utilsis no longer installed with the module. Add it yourself if your tests import it.@i18n-micro/hmris gone: translation HMR is built in. Remove the package if you installed it.@i18n-micro/reactneeds React 18 or newer.nuxt-i18n-micro-cliis replaced by@i18n-micro/cli.
Options and Config
i18n-plugin-hooksandi18n-plugin-definein adependsOnno longer wait for anything, with no error. Usei18n-plugin-loader.- Server auto-imports are
useLocaleServerMiddleware,useTranslationServerMiddleware,loadTranslationsFromServeranddetectCurrentLocale. Others no longer auto-import. resolveTranslationPayloadModeand the otherresolveTranslationPayload*helpers are gone fromnuxt-i18n-micro, along withshouldCopyTranslationPayloadsToPublicandshouldRegisterNitroServerAssets. TheTranslationPayloadModetype stays.$getI18nConfig()no longer hasisSSG,hmr,hooks,noPrefixRedirect,routeLocales,routeDisableMetaorglobalLocaleRoutes. The options themselves are unchanged.locales[].baseUrlnow 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().localesholds what the host serves, andlinkLocalesholds them all.baseUrlalso sets the origin on any other host: canonical andhreflangusemetaBaseUrlif set, else the default locale'sbaseUrl, not the request's origin.excludePatternsmatch whole segments ('/admin'no longer matches/administrator), globs are anchored, andRegExppatterns now reach the runtime.cacheMaxSizeunset means at most 1000 entries on the server.cacheTtlcounts from when an entry was stored, and reads no longer extend it.- Under
no_prefixan explicitlocaleCookie: nullin thei18nconfig stays off. - Under
prefix_except_defaultandprefix_and_defaulta runtimedefaultLocaleis ignored with a warning. A runtimefallbackLocalemust be an enabled locale. routeDisableMetainnuxt.configis honoured, and anuxt.configentry beats$defineI18nRouteunder any key form.- On a server in dev, the resolved config is frozen: a write to it throws.
- A project's
localesentry beats a layer's for the samecode.
Composables and $ Methods
$switchLocaleresolvestrueorfalse, it resolved nothing.$switchRouteis typedPromise<void>.$localeRoute,$switchLocalePathand the switcher link a locale that doesn't serve a page to that locale's home.$localePathreturns a string with anhttp(s):,mailto:ortel:scheme as is, and$localeRoutecarries it inpathandfullPath. Don't pass an unchecked value to them as a link target.//hostand bare names likex.comstay 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. $switchRoutestays in the app. Only a locale's ownbaseUrl,mailto:andtel:leave it.- A link to a locale with a
baseUrlgoes through$localePathor<i18n-link>.<NuxtLink :to="$localeRoute(x, 'fr')">androuter.push($localeRoute(x, 'fr'))stay paths of this app. In code usenavigateTo($localePath(x, 'fr'), { external: true }). $switchLocaleRouteand redirects followtrailingSlash.$setI18nRouteParamsanduseI18nHeadset on the server are dropped when a navigation changes the path, including/news/1to/news/4: set them again on the new page.$setI18nRouteParamsread in setup is bound to that page: a late call from a page already left does nothing. It has no setter, so stubuseI18nitself 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, abeforeRouteEnteror a later global guard sees the old ones. Route middleware is unaffected. $switchLocalerestores the stored locale when its navigation is cancelled, under the URL strategies.$tinterpolates adefaultValue:$t('missing', { n: 1 }, 'Got {n}')givesGot 1.$tcdoes 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. $tcwith 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.$tdrgivesInvalid Datefor an invalid input. Under a second in the past is0 seconds ago.$tn,$tdand$tdrgo byiso:{ code: 'en', iso: 'en-GB' }formats dates as31/10/2026. A codeIntldoesn't know formats asen, and plurals by the index rule: setiso({ code: 'ua', iso: 'uk-UA' }).detectCurrentLocaleis deprecated: useuseLocaleServerMiddleware(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>
htmlescapesparamsin every case and ignores all slots.- A slot beats a param of the same name.
nullmeans not set forplural,number,dateandrelativeDate.pluralcounts like$tc, withNumber:1.5stays1.5,"2 items"isNaN,""is0.number,dateandrelativeDatekeepparams.hideIfEmptyapplies in every branch, anddefaultValuestands in for a missing key in every branch.- A missing key under
htmlis escaped text. dateandrelativeDateoutput carriesdata-allow-mismatch="text", and beside a named slot the text sits in its own<span>..innerHTMLandtextContentattributes are dropped.- It is a functional component: a
refon it gives no instance. <i18n-t>and<i18n-group>read the active dictionary, as$tdoes.
<i18n-switcher>
- A link is a plain
<a>calling$switchLocale, sorouter-link-activeclasses are gone. - The button is
type="button"witharia-expanded. Escape and a click outside close the dropdown. - New classes
i18n-switcher-wrapper,i18n-switcher-dropdownandi18n-switcher-button. Each link haslang, anddirwhen 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
$switchLocalePathlink isn't rewritten.
<i18n-link>
https://,mailto:andtel:match in any case. A barereport.pdfis a route, andexample.comstays external. A file ending that is also a TLD (example.md) counts as a file: writehttps://.activeStyleignores a trailing slash, the query and the hash.
Hooks and Types
i18n:registeris a runtime hook:nuxtApp.hook('i18n:register', …)in a plugin.- The exported type
ModuleHooksis renamedModuleRuntimeHooks. i18n:beforeLocaleSwitchandi18n:afterLocaleSwitchwere never emitted and are no longer documented.ModuleOptionsExtend.isSSGis optional and deprecated: nothing sets it.
URLs and Redirects
/enand/en/...are a 404 underprefix_except_default, withenas the default.- A locale prefix in another case or spelling is a 404 (
/DE/about). So are/DE/xunder[...slug]and/DEbeside[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'.prefixkeeps the redirect, as does/everywhere. - Another locale's custom path, unprefixed, is a
302to the default locale's URL underprefix_except_default. Underprefixthe redirect now reaches a live page.prefix_and_defaultstays a 404. prefixwithredirects: falseanswers 404 on an unprefixed URL, including the original URL of a page with a custom default path.autoDetectPath: '*'applies the cookie andAccept-Languageto unprefixed paths too.- Under
prefixan unprefixed URL keeps the visitor's locale, on every path, whateverautoDetectPathsays. - Under
prefix_and_defaultan unprefixed URL is the default locale. - Under
no_prefixa 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
routeRulesredirect without a page is not localized:/de/oldis a 404, it was an empty 200. - Route rules reach custom paths, and are copied under
no_prefixtoo. A static sibling such as/de/artikel/newsmatches/de/artikel/:slug. - A locale with
disabled: trueanswers 404 on its prefix at once, and links to it stay on the page. - A page path in another case is a
302to its own spelling:/T/BASICgoes to/t/basic. - The original URL of a page with a custom default path is a
302to that path underprefix_except_default. It was a 404. noPrefixRedirectgoes by the visitor's locale: with cookiede,/aboutgoes to/ueber-uns, not to the default locale's path.- A host's own prefix is a 404 on its
baseDefaulthost underprefix_except_default:/de/xon thedehost was a 200. customRegexMatcherno longer 404s a real page whose first segment looks like a locale.nuxi generateprerenders every static page in each locale that serves it, unless a route rule setsprerender: false: more files in the output. WithbaseUrl, run it once per domain withNUXT_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 inrouter.push({ name })is affected: pass the page's own name to$localeRoute. - Unnamed pages from
extendPagesare 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 underno_prefix./about?locale=deno longer renders German underlang="en".- A plugin's
setLocale('de')on/en/...no longer loads thededictionary.
SEO and HTML Output
dir="auto"is no longer the default. A known RTL language getsdir="rtl", and any other locale withoutdirgets no attribute. To keep the old output setdir: 'auto', at a parsing cost on large pages. See Text direction.disableMetanow works: the page has nohreflang, canonical orog:locale.langanddirstay.hreflanglists only the locales that serve the page.x-defaultappears only when the default locale serves it.- An unlocalized page has no
hreflangorog:locale:alternate. - 404 pages carry
<html lang dir>in the locale of the URL, and no canonical orhreflang. - Canonical,
og:urlandhreflangkeep a whitelisted query (/about?page=2), on the server too. - Canonical and
hreflangincludeapp.baseURLwhen 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. WithapiBaseClientHostthat host needs CORS. Varyis set on every page underno_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-Languagehonoursqvalues, andq=0excludes a language.useLocaleServerMiddlewareanduseTranslationServerMiddlewareuse one order on every route (?locale,[locale]param or prefix, cookie, header, default), respectlocaleCookie: nullandautoDetectLanguage: 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-localecookie. A loneuser-locale=deno longer counts. NUXT_I18N_DEFAULT_LOCALEand runtimedisabledLocaleschange routing.premergedoutput changed./{apiBaseUrl}/<page>/<locale>/data.jsonholds 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 usedapp.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 thes-maxage=60, stale-while-revalidateheader is gone. If you relied on that cache, put a CDN in front of the payload route. /{apiBaseUrl}/<page without a folder>/<locale>/data.jsonanswers with the root dictionary, it was{}. A host's 404 now passes through as a 404, where it answered200 {}.- A prerender that cannot load its dictionary fails with a 500 and a warning, and with Nuxt's default
failOnErrorthe build exits 1. - A changed translation without a rebuild reaches a browser only when
vmoves: setNUXT_I18N_DATE_BUILD. - SSR no longer requests
/{apiBaseUrl}over HTTP, unlessapiBaseServerHostis 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: truereads 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'screateBrowserHistoryAdapter.createPreactRouterAdapteris removed. I18nTfollows<i18n-t>above.dangerouslySetInnerHTML,innerHTMLandtextContentattributes are dropped.I18nSwitcherrenders<a href>items witharia-current="page",▾, logical CSS andlanguage-switcheron the button. Noaria-haspopup.classNameadds toi18n-switcher-wrapper.- A link to
/or a locale root is active on an exact match only (React, Preact). I18nLinkkeeps a useronClickandstyle, 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,I18nDefaultLocaleContextandI18nRouterContextare oneI18nConfigContext. Solid: oneI18nContextholds{ i18n, locales, defaultLocale, router }. getCurrentPathof the adapters includes the query and hash, and a repeated key ingetRoute().queryis an array.- A nested
I18nProviderwithouti18nkeeps the outer instance, and a provider with no instance at all throws. defaultLocale()without a value isfallbackLocale.- Solid:
getCurrentPathAccessoris removed.useI18n()no longer throws withoutlocales. - React:
use-sync-external-storeis no longer a dependency, andreact-domis an optional peer.
@i18n-micro/vue, vitepress
I18nTis a functional component.I18nSwitcherandI18nLinkfollow the sections above.aria-haspopupis gone.I18nLinkandcreateVueRouterAdapterresolvename,queryandhash. An unknownnamethrows, asRouterLinkdoes.useLocaleHeadno longer builds//de/. Canonical andhreflangof one page agree (/de), and whitelisted query params reach them.I18nRoutingStrategy.resolvePathmay get a location object and a path with a query and hash. New optionaltoHref.- VitePress: links, the switcher,
pushandreplacecarrysite.base.
@i18n-micro/astro
i18nIntegrationbundlestranslationDirand installs the middleware: delete asrc/middleware.tsthat only copies the setup, or passmiddleware: false. Asrc/middleware.tsof your own passesbase: config.base.i18nIntegrationthrows onmessages,plural,missingHandlerandroutingStrategy: pass them tocreateI18norcreateI18nMiddleware, withmiddleware: false. Options that never did anything are off its type.loadTranslationsFromDirandloadTranslationsIntoI18nmoved to@i18n-micro/astro/node, which is ESM only.getGlobalRoutingStrategy,setGlobalRoutingStrategyandrunWithRoutingStrategyare removed: readAstro.locals.routingStrategy.clone()isfork(), and the_storageoption is gone.- Dynamic pages are one route:
/blog/helloisblog-slug, it wasblog-hello. Movepages/blog-hello/topages/blog-slug/. A customgetRouteNamegets the pattern as a third argument. - The URL decides the page locale.
autoDetectis a 302 from/to the preferred locale's home. NewlocaleCookieoption, andredirectToDefaultis 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.localeCodesleaves disabled locales out, andprepareLocaleRewriteanswers 404 for them.getI18nProps()without keys sends everythingt()can answer on the page, with a dev warning above 20 KB. Pass the keys the island uses.useLocaleHead: defaultbaseUrlissite, else root-relative. Links and metas carryid.<I18nT html>escapesparams, and an empty translation renders empty.<I18nLink>andlocalizePathleave external URLs,#hashand?queryalone and keep a trailing slash.<I18nSwitcher>keeps the query and usesdata-i18n-switcherinstead of generated ids.@i18n-micro/astro/clientonly hastranslateandhasTranslation: import adapters from/client/vue,/client/react,/client/preactand/client/svelte.client/vueneedsvueinstalled.- Islands:
has()is true for a subtree,t('')is''.provideI18ntakes a getter to follow new props. getLocaleandgetDefaultLocalethrow 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.loadFromUrlsfetches 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')isInvalid Date,t(miss, undefined, '')is the key, and a miss no longer logs. isolatedis gone fromcreateI18nTestContext, and./publish-smokeis removed.
@i18n-micro/types-generator
I18nTypesPlugin(options)is a plain Vite and Rollup plugin: no.vite(),.webpack()or.esbuild(). For webpack or esbuild, callgenerateTypesin a script.getTypesStringandgenerateTypesare sync.flattenKeysandisPlainTranslationObjectare 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
.jsoninsidetranslationDir, and no longer creates files. A cross-site request is a 403. - A failed save to a reachable endpoint throws and rolls back.
createBridgetakes abaseoption. Astro usesi18nDevToolsIntegration.
@i18n-micro/utils, core, path-strategy, route-strategy
For custom adapters and tooling only.
utils: subpaths./route,./route-pattern,./build,./payload-statsand./payload-configare removed, along withresolveServerLocaleandresolvePreferredLocale: callresolveLocale.deepMergeTranslationsandmergeRouteTranslationsWithRootare gone: usedeepMergeTranslationsRecursive.core:TranslationHelperdropssetCache,getGenerationandlookupIn(uselookup(chunkKey, key)).BaseI18ndropsensurePinnedChunkandinvalidatePin.useTranslationHelper(storage)is gone: usenew TranslationHelper(storage).path-strategy:switchLocaleRoute(fromLocale, toLocale, route, options)is nowswitchLocaleRoute(toLocale, route, options). An old JavaScript call returns a wrong result with no error. Nuxt's$switchLocaleRouteis not affected. The/prefix,/no-prefix,/prefix-except-defaultand/prefix-and-defaultentries are gone, as aregetRedirectandgetCanonicalPath.RouterAdapterneedsgetRoutes.createPathStrategybuilds no absolute URLs: usenew DomainPathStrategy(ctx)when locales havebaseUrl.route-strategy: onlyRouteGenerator,generateRoutes,Rulesand the path helpers are exported.
Migration
- Check the requirements: Nuxt 3.18, Node 20, and React 18 if you use
@i18n-micro/react. - Update
nuxt-i18n-microand every@i18n-micro/*package together. Remove@i18n-micro/hmr. Add@i18n-micro/test-utilsas a dev dependency if your tests import it. Replacenuxt-i18n-micro-cliwith@i18n-micro/cliand follow its command table. - Search your code for
switchLocaleRoute(from@i18n-micro/path-strategy,i18n-plugin-hooks,i18n-plugin-define,localized-route names,$getI18nConfig().globalLocaleRoutesand the other removed config keys,ModuleHooks, and@ts-expect-erroroni18n:register. - Move markup out of
<i18n-t html>params into the translation, and pass data as params. - If you set
baseUrl: review links built with$localeRoute, settrustForwardedHostbehind a proxy, and runnuxi generateonce per domain withNUXT_I18N_DOMAIN. - Check your URLs against your strategy:
/enunderprefix_except_default, locale-like CMS slugs,routeRulesredirects, andexcludePatterns. - Check your CDN cache key for
Vary, and allow the preload link in a strict CSP. Withapp.cdnURL, put_locales/on the CDN and allow CORS. - If a tool reads
/_locales/...files directly, merge the page file over__root/<locale>/data.json. - 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.
- Compare a
nuxi generateoutput or a sitemap crawl before and after:hreflang,x-default, canonical and the page list change by design. - Update imports for framework packages:
/routerfor React, Solid and Preact adapters,/nodefor Astro's loaders, andI18nTypesPlugin(o)for the types plugin. - For Astro, delete a
src/middleware.tsthat only copies the setup, and renamepages/blog-hello/style dictionaries to their pattern. - 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.