Upgrading
This page lists what changes for your app when you upgrade nuxt-i18n-micro and its @i18n-micro/* packages. Read the section for the version you come from before you bump the dependency. Most apps need few code changes.
@nuxtjs/i18n? — See Migrating from @nuxtjs/i18n.After 3.30.3: the major release
Module options, $t, $localePath, $switchLocale and the other helpers, components, composables and documented server utilities keep their names. What changed is behaviour, mostly bugs fixed. Adapter packages and the routing packages have real API breaks: see Framework packages and Other packages.
Do this first
- Check your versions. The module needs Nuxt 3.18 or newer and Node 20 or newer. Nuxt has its own Node minimum: Nuxt 4.5 declares
^22.19.0 || ^24.11.0 || >=26.0.0.nuxt generateon Nuxt 3.18.0 itself fails in plain Nuxt too ([unstorage] Invalid key: "/"); 3.21.11 works.@i18n-micro/reactneeds React 18 or newer. - Fix the installed packages.
- Remove
@i18n-micro/hmr. Translation HMR is built into the module. - If your tests import
@i18n-micro/test-utils, add it:npm install -D @i18n-micro/test-utils. It is no longer installed with the module. - Replace
nuxt-i18n-micro-cliwith@i18n-micro/cli. 28 commands became 16,checkreplaces three of them, and exit codes change: see the CLI guide.
- Remove
- Search your code for the entries in the table below.
- Check what sits in front of the app. With
baseUrlon locales behind a proxy, see Behind a proxy or CDN. With a CDN, check its cache key against theVaryheader: see Caching HTML. WithapiBaseClientHostor a strict CSP, allow the newpreloadlink: see SEO and head tags. - Update snapshots. Head snapshots gain the
preloadlink, switcher markup changed (<i18n-switcher>and<i18n-link>), and query strings in$localeRouteoutput are encoded differently. - Open your pages in each locale and look for raw keys. Layouts that used keys of the previous page's file now show them raw: move those keys to the root file.
switchLocaleRoute of @i18n-micro/path-strategy lost its first argument — switchLocaleRoute(fromLocale, toLocale, route, options) is now switchLocaleRoute(toLocale, route, options). An old JavaScript call does not fail: it returns a wrong result, because fromLocale is read as the target and toLocale as the route. Only TypeScript tells you. Nuxt's $switchLocaleRoute is not affected.| Search for | Do this |
|---|---|
switchLocaleRoute( from @i18n-micro/path-strategy | Drop the first argument, see above |
i18n-plugin-hooks or i18n-plugin-define in a dependsOn | Use i18n-plugin-loader. The old names no longer wait for anything, with no error |
localized-… names in router.push({ name }) | Pass the page's own name to $localeRoute or <i18n-link> |
.name or .params on a $localeRoute or $switchLocaleRoute result | Handle their absence, see Links |
$localeRoute(x, 'fr') in <NuxtLink :to> or router.push, with baseUrl on locales | Use $localePath, see Locale domains |
$getI18nConfig().globalLocaleRoutes, or isSSG, hmr, hooks, noPrefixRedirect, routeLocales, routeDisableMeta from it | They are gone from the runtime config. Only the build read them, and the options themselves are unchanged. ModuleOptionsExtend.isSSG stays as an optional, deprecated property that nothing sets |
Server auto-imports other than useLocaleServerMiddleware, useTranslationServerMiddleware, loadTranslationsFromServer, detectCurrentLocale | Drop them. getI18nServerConfig, detectRequestLocale, getRequestLocale, loadLiveDevTranslations, getOrCreateServerTranslationCache, DEFAULT_SERVER_CACHE_SIZE and the watcher helpers no longer auto-import |
resolveTranslationPayloadMode, resolveTranslationPayloadOptions, resolveTranslationPayloadPublicDir, resolveTranslationPayloadPublicRel, shouldCopyTranslationPayloadsToPublic, shouldRegisterNitroServerAssets from nuxt-i18n-micro | Drop them. The TranslationPayloadMode type stays |
html on <i18n-t> with markup in params | Move the markup into the translation, see <i18n-t> |
ref on <i18n-t> | It is a functional component, so a ref gives no instance |
useI18n().$t = stub and similar writes | useI18n() is one object per app, so a write reaches every caller. Stub useI18n itself |
ModuleHooks type | Renamed ModuleRuntimeHooks |
resolveServerLocale, resolvePreferredLocale from @i18n-micro/utils/resolve-locale | Call resolveLocale with url, query, state, cookie and acceptLanguage |
detectCurrentLocale is deprecated. It still works and is still a server auto-import; use useLocaleServerMiddleware(event).current, which applies the same rules.
17 pages of these docs moved. The old URLs redirect, so links to them keep working.
URLs and redirects
/enand/en/...are a 404 underprefix_except_default(enbeing the default), as documented.- A locale prefix in another case or spelling redirects to the page in its own spelling.
/DE/aboutused to render the German route in the default locale, or redirect to/en/DE/about; it is now a302to/de/about(/EN/aboutto/aboutunderprefix_except_default). With no page behind the URL in its own spelling it is a 404. A page of its own on that URL (pages/DE/about.vue) stays a page. A bare prefix beside a[slug]page goes to the locale home, so a CMS slug equal to a locale code in another case loses its page. SetprefixCaseRedirect: falseto answer 404 instead;redirects: falsedoes the same. Withssr: falsethe client makes the same redirect. - A page path in another case redirects to its own spelling.
/T/BASICwas a 200 and is now a302to/t/basic. - An unknown URL stays a 404. With
autoDetectPath: '*'or'no_prefix',/nopeno longer redirects to/de/nopefirst.prefixkeeps the redirect (nothing is served unprefixed there), as does/everywhere. - Another locale's custom path, unprefixed, finds its page under
prefix_except_default./a-proposwas a 404 and is now a302to the default locale's URL (/about). Underprefixthe same redirect now reaches a live page (/en/about, it was/en/a-propos, a 404).prefix_and_defaultstays a 404. prefixwithredirects: falseanswers 404 on an unprefixed URL (/aboutwas a302), including the original URL of a page with a custom default path.$localePath('/about')still finds that page.- Under
prefix_except_defaultthe original URL of a page with a custom default path redirects to it, dynamic pages included./dir/onewas a 404 and is now a302to/dir-one/one. autoDetectPath: '*'now applies the cookie andAccept-Languageto unprefixed paths too (/about→/de/about).- Under
prefixan unprefixed URL keeps the visitor's locale. With cookiede,/aboutand a plain<NuxtLink to="/about">go to/de/about, on every path, whateverautoDetectPathsays. - Under
prefix_and_defaultan unprefixed URL is the default locale./contactwithAccept-Language: deused to render German and then hydrate English. It now renders the default locale; visitors reach German through/de/.... - Under
no_prefixa custom path renders its own locale (/ueber-unsis German with any preference), and a restricted page renders in a locale it serves. A path several locales share renders the preference when it is one of them, and underbaseUrlis a page on each of their domains. Those locales share one record,localized-<page>-<first locale>. noPrefixRedirectgoes by the visitor's locale. With cookiede,/aboutgoes to/ueber-uns, and renders German, not to the default locale's/about-us. A locale with no custom path of its own stays on/about.- A
routeRulesredirect without a page is not localized./de/oldwas an empty 200, now it is a 404: add a rule for the prefixed path if you need one. A link from another locale to such a rule keeps the path as written and lands on the rule's target. - Route rules reach custom paths.
/blog/**also applies to/de/artikel/:slugwhenblog/[slug]has that path, and rules are copied underno_prefixtoo. A static sibling such as/de/artikel/newsmatches that pattern as well. A rule already written under a locale prefix stays as written. - A locale with
disabled: trueanswers 404 on its prefix at once, and links to it stay on the page. A locale disabled at runtime does the same: it used to render in the fallback locale. - The module's 404 reads
Page Not Foundon the prefix of a disabled locale and on a host's own prefix, where Nuxt saidPage not found: /de/about. The status is the same. customRegexMatcherno longer 404s a real page whose first segment looks like a locale.- Localized route names are consistent. A child under a shared locale record keeps the page's name,
no_prefixhonourslocalizedRouteNamePrefix, and a parent with an index child stays unnamed. - Pages a module adds without a
name(extendPages) are localized and get a name, with locale variants andhreflang. They used to stay unlocalized. - A
nuxt.configentry beats$defineI18nRoute, under any key form (about/team,/about/team,about-team). Which one won used to depend on the spelling. - New build warnings: a
localeRoutesvalue that starts with its own locale code (fr: '/fr/article'), and a child's custom path outside its parent. - Hash mode works under
prefix. The document is/, and/enor/aboutredirect into the hash. /#/de/xrenders German underprefix_except_defaultandprefix_and_default. It rendered English with a hydration mismatch.
Links and route objects
- A locale that does not serve a page links to its home.
$localeRoute,$switchLocalePathand the switcher no longer return another locale's page. - An unknown locale code goes to the default locale.
$localePath(path, 'xx')and$localeRouteused to give/xx. $localePathreturns a string with anhttp(s):,mailto:ortel:scheme as is, and thepathandfullPathof$localeRoutecarry it unchanged, so do not pass an unchecked value (a?next=parameter) to them as a link target.//hostand bare names likex.comstay paths, on this origin in every locale, and no link they turn into leaves it:/de//x.comto the default locale is/x.com. Backslashes count as slashes.$localePathencodes a path:/a bgives/a%20b.$localeRouteand$switchLocaleRoutereturn plain objects, safe inref,reactive,useState, JSON and spreads. When a param needs escaping the object is{ path, fullPath, query, hash }($localeRouteaddshref), with nonameorparams, and the URL stays the same before and after a reload. A plain URL has both.nameis new: the old objects never had it. Code reading.nameor.paramsmust handle their absence, and which form you get depends on the value.- Under
trailingSlash,$switchLocaleRoutealways returns that path form, plain slugs too, and server redirects followtrailingSlash. Results andLocationheaders can change under that option. - Query values in
$localeRouteoutput use vue-router's encoding:/stays literal and a space is+(?redirect=/v2, it was%2Fv2). The decoded value is the same, but a snapshot of such anhrefchanges. - A link with no path stays on the current page.
$localePath('#top'),'?p=2',''and{ query }keep the page they are on;'#top'used to read from the site root. A bare'about'still means/about. - Spreads.
{ ...route, query }and{ ...route, hash }keep the change, also through$switchRoute. A newpathwins in$localeRoute,$localePathand$switchRoute.{ ...route, params }works on a named route; on a path-form route the path wins and the params are ignored, with vue-router's dev warning. Use$localeRoute({ name, params }). $switchRoutestays in the app. A locale's ownbaseUrl,mailto:andtel:leave it; any other string is a path.excludePatternsmatch whole segments ('/admin'no longer matches/administrator), and globs are anchored (/docs/*no longer matches/en/docs/x).RegExppatterns now reach the runtime; they used to be lost there. A page they match is unlocalized: nohreflang, and the default locale underprefix_except_default. Check your patterns against Excluding paths.
A % or ? in a param does not survive a server-rendered reload on Nuxt 4.5.2, on any app: that is upstream, not the module.
Locale domains
locales[].baseUrlnow routes requests. One build serves every domain: see Layers and domains. WithbaseUrlset, a locale of another domain is a 404 on this host (/en/aboutonexample.de), and links to a locale on the same host are relative. Requests on any other host,localhostincluded, behave as before, except that canonical andhreflangusemetaBaseUrlif set, else the default locale'sbaseUrl, and not the request's origin.- Behind a proxy that rewrites
Host, every request falls to the build config until you settrustForwardedHost. It defaults tofalse, unlikemetaTrustForwardedHost. See Behind a proxy or CDN. nuxi generatewithbaseUrlneedsNUXT_I18N_DOMAIN, once per domain. Without it the build warns, and those locales are links with no pages. See Static site.- A link to a locale with a
baseUrlgoes through$localePathor<i18n-link>.<NuxtLink :to="$localePath(x, 'fr')">and<i18n-link :to="$localeRoute(x, 'fr')">are that absolute URL, and a click goes there. The router takes no absolute object:<NuxtLink :to="$localeRoute(x, 'fr')">androuter.push($localeRoute(x, 'fr'))stay/https://fr.example.com/..., a path of this app. Onlocalhostor a preview host every locale with abaseUrlis such a link, the current one too. In code usenavigateTo($localePath(x, 'fr'), { external: true }). - The home of a domain is written without a trailing slash everywhere (
https://example.de), in links, canonical andhreflang. - Under
prefix, a link to another domain keeps the locale prefix, with or withoutbaseDefault.https://example.de/de/about, nothttps://example.de/about, which only redirects. - Under
prefix_except_defaultwithbaseDefaulton a locale other than the build default, routes keep every prefix so other domains can link to them./enis still a 404. Only code readingrouter.getRoutes()sees the difference. - Under
prefix_except_defaulta host's own prefix is a 404 on itsbaseDefaulthost./de/xon thedehost was a 200. $getI18nConfig().localesholds only what the host serves.linkLocalesholds all of them, and$getLocales()stays complete.
Navigation and $switchLocale
- A navigation applies when it commits. The dictionary, the locale, params and head change in
router.afterEach, so a cancelled or blocked navigation changes nothing, and the old page's links stay valid while the next chunk loads. AbeforeResolve, abeforeRouteEnteror a later global guard now sees the old dictionary and locale. Route middleware is unaffected. - A failed chunk keeps the current dictionary. A 5xx, a network error or a bad body on the root file used to show raw keys. The new page now renders with the old strings, and the next navigation retries. A 404, or a static host's SPA shell in the file's place, gives the root dictionary until the next reload.
$switchLocaleresolvestrueorfalse(it resolved nothing),falsewhen the locale's dictionary did not apply. It is typedPromise<boolean>, and$switchRoutePromise<void>.$switchLocaleis an explicit choice. On arrival it stores the locale, on a 404 too. If the navigation is blocked, cancelled or throws, the stored preference and the state stay as they were, and a throw is rethrown. A move to another host stores it first. Underno_prefixand in hash mode the switch stands whatever the navigation does. On a page with one URL for every locale (excludePatterns) the choice is not undone, and the next page's URL decides: underprefix_except_defaultan unprefixed link returns to the default locale.- Under
no_prefix,$switchLocalekeeps its page when two pages share a custom path, with escaped params too. A reload of such a URL, and a$localeRoutewith an escaped param, land on the first record. - A client redirect keeps the mode of its navigation. A plain
<NuxtLink to="/about">underprefixadds a history entry instead of overwriting the current one;router.replacestill replaces. $setI18nRouteParamsanduseI18nHeadfrom the server survive hydration. After landing on/news/2the switcher links to the translated slug (/de/news/2-zwei), not/de/news/2, in the server HTML too, wherever the switcher sits, when the page sets the params during SSR. They are dropped when a navigation to another path commits, 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.useNuxtApp().$setI18nRouteParamsis an accessor now, and two reads give two functions. It has no setter:useI18n().$setI18nRouteParams = fn, as a test stub might do, throws in strict mode, so stubuseI18nitself. In a layout orapp.vueit is the plain function, as nothing there is left.- The server rewrites the links of
<i18n-switcher>when the render ends, on pages with params, because a switcher in the layout renders before the page's async setup. A hand-written$switchLocalePathlink is not rewritten: see custom locale routes. Under Nuxt's SSR streaming the first HTML keeps the URL's params. - A prop named
localeon a server component sets its render locale, so<Greeting :locale="$getLocale()" />follows the URL and a switch.$getLocale(),$localePathand$getLocaleNameinside it follow the prop too. See server-side translations. Without the prop it still uses the cookie or the default. keepalivepages get their params and head back on return.- A nested page set to
falseunderprefix_except_defaulthas no locale URL./de/parent/kidwithglobalLocaleRoutes: { 'parent-kid': false }was served and is now a 404: the page stays on/parent/kid.
Locale detection and the cookie
One precedence everywhere, described on Locale detection.
?locale=counts on pages only underno_prefix. Under the prefix strategies the URL decides;/about?locale=deno longer renders German underlang="en".?locale=de&locale=frreads the first value, it was"de,fr".Accept-Languagehonoursqvalues (fr;q=0.1, de;q=0.9→de;q=0excludes a language). Regional matching no longer depends on the order oflocales, and never crosses Chinese scripts.- A client navigation to a page stores its locale in the cookie, also on an unprefixed page of the default locale. With
localeCookieset, a visitor who clicks a link to/aboutnow hasuser-locale=en, and a later change of browser language no longer redirects/. A first load of that URL stores nothing. Underno_prefixand in hash mode a navigation stores only a change. - A 404 writes no cookie: not the response, and not a plain client navigation to it, in any mode. Under
no_prefixa URL that looks prefixed writes none either. - Under
no_prefixan explicitlocaleCookie: nullis honoured. It used to becomeuser-locale. Set it in thei18nblock ofnuxt.config: anullinmodules: [[name, opts]]or in a layer is still lost. - Hash mode reads only the
hash-localecookie. A loneuser-locale=deno longer counts. - A malformed locale cookie is ignored and stays as it is, instead of failing every SSR page with a 500. It is no longer deleted on every request, which also stopped a prefixed page from storing its locale.
- The URL's locale goes first. A plugin's
setLocale('de')on/en/...no longer loads thededictionary, and an unknown code in the state is ignored. - Server API helpers (
useLocaleServerMiddleware,useTranslationServerMiddleware) use one order on every route:?locale→[locale]param or prefix → cookie → header → default. They now respectlocaleCookie: nullandautoDetectLanguage: false, and cache a dictionary per locale. - Runtime overrides reach routing.
NUXT_I18N_DEFAULT_LOCALEand runtimedisabledLocalesnow change the generated strategy (a disabled locale stops routing), and runtimedisabledLocalesadd to the build's. Underprefix_except_defaultandprefix_and_defaulta runtimedefaultLocaleis ignored with a warning, because that locale's unprefixed routes are generated at build. A runtimefallbackLocalemust be an enabled locale: otherwise it is dropped with a warning and the build's value applies if that one is enabled. See Runtime overrides.
SEO and head tags
dir="auto"is no longer the default. A locale withoutdirused to getdir="auto", which slows parsing of large pages. Now a known RTL language getsdir="rtl", and any other locale gets no attribute. This applies to Nuxt,@i18n-micro/vue,astroandvitepress. If you setdir: 'auto'on a locale to keep the old output, it costs about 1 s of parsing at 20,000 elements: see Text direction.- Every SSR page has a new tag:
<link rel="preload" as="fetch" crossorigin="anonymous" href="/_locales/...">, 404 pages too. WithapiBaseClientHostthehrefis on that host, which needs CORS. A strict CSP must allow it. hreflanglists only the locales that serve the page;x-defaultis emitted only when the default locale serves it.- An unlocalized page has no
hreflangorog:locale:alternate. - Canonical and
hreflangincludeapp.baseURLwhen the origin comes from the request.metaBaseUrlandsite.urlstay as written. - 404 pages carry
<html lang dir>, in the locale of the URL. They have no canonical,hreflang,og:url,og:localeor alternates, also when the module itself answers the 404 on a matched record (/en/...). Tags you pass throughuseI18nHeadreplacestay, so skip them on error pages. - Canonical,
og:urlandhreflangkeep a whitelisted query (/about?page=2), on the server too: it was dropped during SSR.page,sort,filter,search,q,queryandtagare whitelisted by default; seecanonicalQueryWhitelist. - An
X-Forwarded-Hostthat does not parse is ignored instead of a 500 on every page. With no usable host, and nometaBaseUrlorsite.url, canonical andhreflangURLs on that response are relative. AHostthat does not parse stays a 500, as on a plain Nuxt app. disableMetanow works, in$defineI18nRouteandrouteDisableMetaalike, also when set innuxt.config: the page has nohreflang, canonical orog:locale.langanddirstay.useLocaleHead()also returnsbuild(), a getter foruseHead(() => build()).- On a plain build without
metaBaseUrl,X-Forwarded-Hoststill chooses the host in these tags, as before. PinmetaBaseUrlif the origin is reachable directly: see Behind a proxy or CDN.
Translations
- Merges are deep at every level. Root, page and fallback files used a 2-level merge, so
a.b.cfrom a fallback or the root was lost in source mode and in dev. All modes now give the same dictionary. - Fallback chains are followed to their end (
fr→de→en) in every mode. - Merging goes by locale, not by file kind. The active locale's root file now beats a fallback locale's page file. See Fallback locales.
- A project's
localesentry beats a layer's for the samecode; a layer only fills the gaps. - A page without its own
pages/<name>/folder reads the root dictionary, child pages too. It used to get{}and show raw keys. Output gains a file per such page, and__root/<locale>/data.jsonper locale:/_locales/<any name>/en/data.jsonis a 200 with the root keys, it was{}. That matters for monitoring, for allow-lists on static hosts and for tools that read the tree. disablePageLocales: truereads the root files only;pages/indexis no longer merged in.- The leaving page's keys stop resolving when its element leaves the document. If a layout used keys that only the previous page's file had, move them to the root file. A parent's keys stay while its child is shown, including a parent with an
indexchild, until the next navigation. Between two children of one parent, a parent's own keys need the child's dictionary or the root one. $clearCache()reloads the current page's dictionary and keeps the old one on screen until the new one lands. A failed reload keeps the old dictionary, a response requested before the clear is discarded, and a page in its leaving transition stays translated.$has(key, route)honours the route.$loadPageTranslationson a page not visited yet no longer stops its file from loading. The file loads on the first visit and your keys stay on top of it. The page used to show raw keys until a reload.- Translation HMR applies edits in place, without a page reload; removed keys disappear and root edits reach every page.
- A broken locale file warns once with its path, in every mode. Dev used to throw and premerged skipped it silently, so an app that relied on dev failing hard no longer sees that.
Plurals and formatting
$tcwith a negative, fractional or huge count takes the last form with the real count;{ count: null }no longer throws; a missing plural key reaches the missing handler.$tdrgives real future dates (in 3 days, ortomorrowwithnumeric: 'auto') andInvalid Datefor an invalid input. Under a second in the past is0 seconds ago.$tn,$tdand$tdrgo byiso, as plurals do:{ code: 'en', iso: 'en-GB' }now formats dates as31/10/2026. In a locale code_reads as-, soen_USno longer throws, and a codeIntldoes not know formats asenrather than following the machine's locale. Named formats are still looked up bycode.- A
{param}placeholder reads only the params you pass ({constructor}stays as written). {placeholder}names may hold any character but braces and spaces:{firstName},{first-name}and{user.name}are read fromparams. Without such a param they stay as written, as before.- A
defaultValueis interpolated, in$tand$tc:$t('missing', { n: 1 }, 'Got {n}')givesGot 1, it used to giveGot {n}. A default that is not a string comes back as it is. $tcon an empty translation answers'', as$tdoes. It used to answer the default value or the key.- Plural templates keep their meaning, with one exception. The table lists every shape. To get
Intl.PluralRulescategories, say so with the new=0marker:=0 no apples | one apple | {count} apples.Shape Example (in uk)Before Now Any template in a locale with one or two integer categories ( en,de,fr,es,ja...)none | one | {count} manyby index unchanged One form per category one | few | manyby index: 21 → manyunchanged. Add the marker for 21 → oneZero form first, three forms none | one | manyby index unchanged Other placeholders no items | {n} item | {n} itemsby index unchanged {count}in every form{count} many | {count} one | {count} fewby index unchanged Fewer or more forms than below a | b,none | one | two | {count} manyby index unchanged In lv,prg,blo,cv,ksh,lagnone | one | {count} manyby index unchanged Marked with =0=0 none | one | manythe marker was shown as text zero form, then categories A zero form with no placeholder, then one form per category, with {count}in each form several counts sharenone | {count} one | {count} few | {count} manyby index: 3 → 3 many, 21 →21 manyby category: 3 → 3 few, 21 →21 one. 0, 1, 2 and every count that stays inmanygive the same result
The last shape needs three or more categories (ru,uk,pl,cs,sk,hr,sr,lt,ro,sl,he,ar,cy...). Inar,oneandtwoare single counts, so those two forms need no number. - A locale code
Intldoes not know gets the index rule, with or without the marker's categories:uano longer follows the machine's locale. Setiso:{ code: 'ua', iso: 'uk-UA' }plurals as Ukrainian, in the module and in@i18n-micro/vue. - A custom
pluralrule keeps its signature and still gets the locale code. It reads the raw value, so a marked template reaches it with=0in place, and it gets the count as aNumber:1.5where it used to get1. Returningnullfalls back to the built-in rule, which honours the marker. timeZoneoption. Set it (Europe/Berlin) to render one calendar date on the server and in every browser. It has no effect onrelativeDate, which depends on the clock.
<i18n-t>
This section covers <i18n-t> in the module and I18nT in @i18n-micro/vue. For the other frameworks see Framework packages.
htmlescapesparams, in every case, and ignores all slots. Only the translation is trusted markup. A param that held markup on purpose now shows as text: put the markup in the translation and pass the data as params, or use slots withouthtml.- Slots work with
plural,number,dateandrelativeDate, and adefaultslot receives{ translation }in those cases too. Every{name}is filled, not only the first. - A slot beats a param of the same name. A param value that looks like a placeholder stays text. With a
customPluralRuleor a custompluralthe rule returns a finished string, so a param it interpolated can be taken for a placeholder there. nullmeans not set forplural,number,dateandrelativeDate.:plural="null"used to fail the render,:date="null"rendered 1970.pluralcounts like$tc, withNumber:1.5stays1.5and takes the last form, it used to be cut to1;"2 items"isNaN, it used to be2;""is0. AcustomPluralRulesees1.5too. Its other contract is unchanged: same arguments, its answer is used as is,nullrenders empty.hideIfEmptyapplies toplural,number,dateandrelativeDate. A hidden value renders a comment, not an empty text node, so a test that read''sees<!---->.- A missing key under
htmlis text. The key is escaped, because only a translation is trusted and a key can come from a request. AdefaultValueis yours and stays markup. - With
plural, adefaultValueor the key stands whole on a miss, as in$tc:a | bis no longer split. defaultValuestands in for a missing key in every branch, not the key itself, and acustomPluralRulegets it through its getter on a miss. It used to do something only afterhideIfEmpty.<i18n-t>withnumber,dateorrelativeDatekeepsparams; they are merged in.dateandrelativeDateoutput carriesdata-allow-mismatch="text": a clock or a time zone that differs between server and browser no longer warns during hydration. Next to a named slot, the text around the slot sits in a<span>of its own, because Vue checks the attribute only on an element's single text..innerHTMLandtextContentattributes are dropped along withinnerHTML.<i18n-t>and<i18n-group>read the active dictionary, as$tdoes, not the route's chunk. Evicted chunks (cacheMaxSize) no longer show raw keys. A page in its leaving transition now reads what$treads there: the new page's dictionary, falling through to its own on a miss, so a key both pages define shows the new text, and after a locale switch its own keys show raw until it unmounts.
<i18n-switcher> and <i18n-link>
- A click on a switcher link is one navigation. It is a plain
<a>calling$switchLocale, not aNuxtLinkplus a click handler, sorouter-link-activeclasses are gone. Thehrefstill carriesapp.baseURL, and the current locale's link still hasaria-current="page". Ctrl/cmd-click opens a new tab and leaves this tab alone. Links keep the query and hash. - The button is
type="button", so it no longer submits a surrounding form, and hasaria-expanded.aria-haspopupis gone, in the module,@i18n-micro/vueand@i18n-micro/astro: it promised a menu, and the list is links. Escape and a click outside close the dropdown; two switchers no longer stay open together. - The wrapper and dropdown have the classes
i18n-switcher-wrapperandi18n-switcher-dropdown; the button hasi18n-switcher-buttonas well aslanguage-switcher, and its label sits in a<span>. Default styles use logical CSS properties (inset-inline-start,margin-inline-start), and the caret is▾. The dropdown hasz-index: 1000(it was 10),min-width: 150pxand abox-shadow, the wrappervertical-align: middle. Each link haslang, anddirwhen the locale sets it. <i18n-link>:https://,mailto:andtel:are matched in any case (HTTPS://x.com,Mailto:). A barereport.pdforindex.htmlis a route, nothttps://report.pdf;example.comandexample.com:8080stay external. A file ending that is also a TLD (example.md,x.zip) counts as a file: writehttps://.activeStyleignores a trailing slash, the query and the hash. It is typedCSSPropertiesnow, so some typed values may need a change.
Hooks, plugins and types
i18n:registerruns atapp:createdon startup, after every plugin instead of between them. A plugin withorderabove 1 orenforce: 'post'now has its subscriber called on startup too, but no longer reads registered keys in its ownsetup. In a navigation the hook now runs before other plugins'router.beforeEachguards.i18n:registergets the locale of the page it fills. UnderhashModea navigation that changes the locale used to pass the old one, and the keys landed in the wrong chunk.i18n:registeris a runtime hook: register it withnuxtApp.hook('i18n:register', …)in a plugin, without@ts-expect-error. See Hooks.hooks: falseanddefine: falsestill turn their parts off.- With
plugin: false,$defineI18nRoutestays callable and does nothing at runtime. It used to crash the page. - The
hooksoption no longer documentsi18n:beforeLocaleSwitch/i18n:afterLocaleSwitch; they were never emitted. const { $defineI18nRoute } = useNuxtApp()in a page now works with configs that use variables.useI18n()has more keys:clearCache,pluralFunc,pluralLocale.clearCacheis on its type.$getRouteName(route)answers for the route it is given. It returned an empty string.useI18nLocale()dropsgetLocaleWithServerFallback(serverLocale): writegetPreferredLocale() ?? serverLocale ?? null.$getI18nConfig()is read-only on the server in dev. A write throws there, and in production it would reach every request.
Server, build and deployment
- A page's payload file holds only what differs from the locale's root. The client loads the root once and merges each page over it, so disk and Edge bundles shrink and a second page costs a fraction of the first.
premergednow writes__root/<locale>/data.json, and/_locales/<page>/<locale>/data.jsonholds only the difference: anything that reads those files directly must merge the two. A full dictionary is still a valid file, so a host orsourcemode that serves one keeps working. - Dictionary files are cached by version. A URL with the current
?v=is pinned for a year on a Node server, other URLs revalidate, and a host's static copy answersmax-age=60: see Cache headers.httpCacheControl: falsekeepsmax-age=0. A translation changed without a rebuild reaches a browser only whenvmoves: setNUXT_I18N_DATE_BUILD. - Dictionaries follow
app.cdnURL. The browser fetches public payload files from<cdnURL>/_locales/..., where it usedapp.baseURL: the CDN must hold_locales/and allow your site's origin (CORS). See App baseURL and payload hosts. - SSR no longer requests
/_localesover HTTP, so a server middleware or a log on that route stops seeing the render's reads. An externalapiBaseServerHoststill goes over HTTP. - A failed read no longer sticks, and neither does a 404. A 404 from a host means it has no such file: the render shows the root dictionary, as the browser does, nothing is kept, and the file is read once it appears. Any other failure, a 200 that is not a JSON object, or an unreadable or corrupt local file reaches no cache, and the next request reads again: a page render logs a warning, shows raw keys and answers
503withno-storeandRetry-After, so a route rule or a CDN keeps nothing of it while the browser still reads the dictionary on hydration,/_locales/...answers503withno-store, anduseTranslationServerMiddlewaregives atthat answers the key or the default. During an outage every request reaches the host, and a host that sends no response headers for 60 seconds fails the read. A slow download of a large file has no limit. See When a dictionary cannot be read./_localespasses that 404 on too, where it answered200 {}. - A prerender that cannot load its dictionary fails, and so does one whose host has neither the page's file nor the root file. The route is a 500, a warning names the locale and the page, and no file is written; with Nuxt's default
failOnErrorthe build exits 1. A page with no file of its own over a root file prerenders with the root. - A host set at build time answers SSR, also with the default public copy, and on an error page on an internal path. The public file used to answer.
NUXT_I18N_APP_BASE_SERVER_HOSTat runtime now reaches/_localesand the server loader, and the render reads either host through that loader. - A custom
nitro.output.publicDir, and any other layout wherepublic/is not beside the server, embeds the payloads. nuxi build --preset <static preset>prerenders asnuxi generatedoes, so it may write more files.nuxi generateprerenders every static page in each locale that serves it, linked or not, unless a route rule setsprerender: false.sourceorpublicAssets: falseon static output warns. There is nothing for the browser to read.- The embed warning is a size check now. The build warns only past a platform's limit: see Bundle limits.
Varyis set wherever the visitor decides: on every page underno_prefix, and on/and any other URL that can redirect by preference. It holdsCookiewhen a cookie is in use and, withautoDetectLanguage,Accept-Language. Check your CDN's cache key: see Caching HTML./_localescaching changed. There is 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. An empty dictionary is neverimmutable.- The server cache is bounded by default: at most 1000 entries when
cacheMaxSizeis unset.cacheTtlcounts from when an entry was stored; reads no longer extend it. - New optional
cacheStorage: a dictionary cache several instances share. Unset, nothing changes.
Framework packages
The 2.0.0 packages depend on each other: upgrade them together.
@i18n-micro/react, @i18n-micro/preact, @i18n-micro/solid
| Package | Change | Action |
|---|---|---|
| react | React 18 or newer; use-sync-external-store is no longer a dependency; react-dom is an optional peer; new entries /client and /server | Stay on 1.x for React 16 or 17 |
| react | Renders on a server: 1.x failed with Missing getServerSnapshot | — |
| react, solid | Router adapters moved to the /router subpath | import { createReactRouterAdapter } from '@i18n-micro/react/router' (Solid: createSolidRouterAdapter from @i18n-micro/solid/router) |
| preact | createBrowserHistoryAdapter moved to @i18n-micro/preact/router; createPreactRouterAdapter is removed. New optional third argument ssrUrl | Change the import. Under SSR pass the request's URL, or links and the switcher render as on / |
| react, preact | numberFormats and datetimeFormats are honoured, so tn(n, 'currency') output changes. They used to be ignored | Check named formats |
| react, preact | A link to / or a locale root is active on an exact match only; className on the switcher adds to i18n-switcher-wrapper | Check active-link styles |
| all three | I18nT follows <i18n-t>: html escapes params, plural keeps fractions, number, date and relativeDate beat a param of their name, hideIfEmpty and defaultValue apply in every branch, a hidden value renders bare, with no element. dangerouslySetInnerHTML, innerHTML and textContent attributes are dropped | Move markup from params into the translation or components |
| all three | New components prop on I18nT for rich text, children as Vue's default slot | See Rich text |
| all three | I18nSwitcher matches the Vue one: <a href> items with aria-current="page" on the current locale, ▾ with a space before it, logical CSS, language-switcher on the button, no aria-haspopup, clicks bubble, Escape closes it. A caller's getLocaleName wins over the list in Solid too | Check styles and tests that looked for li button or ▼ |
| all three | I18nLink: a user onClick and style are kept, a modified click or target is left to the browser, query and hash do not count for the active check. Without a router a click is the browser's own and no link is active | — |
| react, preact | I18nLocalesContext, I18nDefaultLocaleContext and I18nRouterContext are one I18nConfigContext; I18nContext still holds the instance | Use I18nProvider, or the useI18nLocales-style hooks |
| solid | One context: I18nContext holds { i18n, locales, defaultLocale, router }; the other three are gone | Use I18nProvider, or the useI18nLocales-style hooks |
| all three | getCurrentPath of the adapters has the query and hash, so switchLocale keeps them; resolvePath keeps them too. getRoute().fullPath has them as well, and a repeated key in getRoute().query is an array | Custom strategies: return the full path, links compare by page |
| react, preact | ReactI18n and PreactI18n are the runtime's I18n; t from useI18n() keeps its identity until the store changes; defaultLocale() without a value is fallbackLocale, it used to follow the locale | — |
| all three | A nested I18nProvider without i18n keeps the outer instance; a provider with no instance at all throws. In React and Preact it may instead get the createI18n options as props. locales add their iso to localeIso | — |
| solid | SolidI18n is built on the runtime, and the package now depends on @i18n-micro/runtime and publishes src/ through a solid export condition. routeMessages, loadFromUrl, hasTranslation, clear and a fallbackLocale setter are new. getCurrentPathAccessor is removed: read a signal in getCurrentPath. useI18n() no longer throws without locales, useI18nLocales and useI18nDefaultLocale answer undefined. The adapter no longer needs a reactive owner | Remove the accessor from custom strategies |
@i18n-micro/vue
| Change | Action |
|---|---|
numberFormats and datetimeFormats are honoured, so tn(n, 'currency') output changes | Check named formats |
I18nT is a functional component and follows <i18n-t>; I18nSwitcher and I18nLink follow the section above | A ref on <I18nT> no longer gives a component instance |
I18nLink and createVueRouterAdapter resolve name, query and hash; #top stays on this page; switchLocale and the switcher keep the query and hash | Check tests that pinned /de for a named target. An unknown name now throws, as RouterLink does |
useLocaleHead no longer builds //de/ with the default baseUrl: '/'; canonical and hreflang of one page agree (/de, was /de/), and whitelisted query params reach hreflang and x-default, as in Nuxt. Canonical and og:url keep canonicalQueryWhitelist params with the vue-router adapter | — |
I18nRoutingStrategy: resolvePath may get a location object and a path with a query and hash; new optional toHref | Custom strategies: keep what follows the path |
@i18n-micro/astro
| Change | Action |
|---|---|
i18nIntegration bundles translationDir and installs the middleware | Delete a src/middleware.ts that only copies the setup. To keep it, pass middleware: false, or its options are lost (a warning says so) |
i18nIntegration throws on messages, plural, missingHandler and routingStrategy, and no longer sets a global routing strategy | Pass them to createI18n / createI18nMiddleware in src/middleware.ts, with middleware: false |
I18nIntegrationOptions no longer inherits the module's options, so about 45 fields such as strategy and meta are TypeScript errors. They never did anything there | Remove them |
loadTranslationsFromDir, loadTranslationsIntoI18n moved to @i18n-micro/astro/node, which is ESM only | Change the import; virtual:i18n-micro/messages has the bundled dictionaries |
getGlobalRoutingStrategy, setGlobalRoutingStrategy, runWithRoutingStrategy removed, with their AsyncLocalStorage | Read Astro.locals.routingStrategy |
AstroI18n extends the runtime I18n: clone() is fork(), the _storage option is gone. The package depends on @i18n-micro/runtime, no longer on @i18n-micro/node | — |
Dynamic pages are one route: /blog/hello is blog-slug (was blog-hello), [...path] is path | Move pages/blog-hello/ to pages/blog-slug/. A custom getRouteName gets the pattern as a third argument |
base works: helpers return URLs with it, the cookie lives under it | A src/middleware.ts of your own passes base: config.base |
The URL decides the page locale; autoDetect is a 302 from / to the preferred locale's home; new localeCookie middleware option; redirectToDefault is implemented | See Astro |
A page view from a same-origin page of another locale writes the locale cookie and does not redirect; endpoints, images, 404s, posts and prefetches do not. Server islands and actions never redirect and carry Vary: Referer | Check your CDN if it refuses Set-Cookie; behind a proxy set security.allowedDomains |
Disabled locales: config.localeCodes leaves them out, /fr is no locale, prepareLocaleRewrite answers 404 | — |
prepareLocaleRewrite and getLocaleRewritePath take a narrower type; anything that passed before still passes | — |
getI18nProps() without keys sends all t() can answer on the page, with a dev warning above 20 KB; fallbackLocale is the instance's; translations always has the route | Pass the keys the island uses |
useLocaleHead: default baseUrl is site, else root-relative (/about, was //about); links and metas carry id; after Astro.rewrite it names the requested URL | — |
<I18nT html> escapes params; any tag works; attributes pass through; an empty translation renders empty, not the key | Move a param holding markup into the translation |
<I18nLink> leaves external URLs, #hash and ?query alone and keeps a trailing slash; localizePath the same, and with no locale it uses the current one | — |
<I18nSwitcher> keeps the query, one script for all switchers, hidden on the list, data-i18n-switcher instead of the generated ids, no aria-haspopup | Check selectors |
@i18n-micro/astro/client only has translate and hasTranslation | Import adapters from /client/vue, /client/react, /client/preact, /client/svelte |
client/vue no longer bundles its own Vue: reactivity works, and vue must be installed | Install vue |
Islands: has() is true for a subtree, t('') is '', as on the server; React and Preact get setLocale, setRoute. provideI18n takes a getter to follow new props | Use provideI18n(() => props.i18n) with transition:persist |
getLocale, getDefaultLocale throw without the middleware instead of answering 'en' | — |
@i18n-micro/vitepress
| Change | Action |
|---|---|
Links, the switcher, push and replace carry site.base; resolvePath and localizePath stay relative to it; numberFormats, datetimeFormats and timeZone reach createI18n | — |
The locale head with alternates: false also drops x-default | — |
@i18n-micro/node, @i18n-micro/runtime
| Package | Change | Action |
|---|---|---|
| node | Loads translations on Node 24 (dirent.path is gone there) | — |
| node | New run(locale, fn) and fork(locale) for requests; reload() swaps dictionaries instead of emptying them first, and logs no success message; a directory that is gone keeps the old ones, an emptied one empties them | — |
| node, astro, vitepress | One reader of a locales folder: symlinked files are read, a missing folder warns, a file that is not a JSON object is reported and skipped, a BOM is fine. Messages start with [i18n] | Adjust tests that pinned the old messages |
| runtime | numberFormats and datetimeFormats are honoured, so tn(n, 'currency') output changes; new timeZone, now and localeIso options | Check named formats |
| runtime | A write to a locale that is neither active nor fallback does not notify | A listener that wants every preload: read the store when the locale changes |
| runtime | New fork(locale); storage is a getter; loadFromUrls fetches in parallel and writes nothing if one fails | — |
| runtime, vue, node, astro | addRouteTranslations(locale, route, messages, false) replaces the chunk. It used to merge | Pass true, or the default, to merge |
Other packages
These are public on npm, but most apps reach them only through the module.
@i18n-micro/core, @i18n-micro/utils
| Package | Change | Action |
|---|---|---|
| core | useTranslationHelper(storage) is gone | new TranslationHelper(storage). A destructured method loses this |
| core | TranslationHelper drops setCache, getGeneration, lookupIn; BaseI18n drops ensurePinnedChunk, invalidatePin; route chunks hold only their own keys and lookups fall through to the root chunk | Custom adapters: use lookup(chunkKey, key) |
| core | TranslationHelper.hasPageTranslation is gone | hasCache(locale, routeName) |
| core | TranslationHelper.loadTranslations is gone | mergeTranslation(locale, routeName, data); routeName has no default, the root chunk is 'index' |
| core | TranslationHelper.loadPageTranslations is gone | mergeTranslation(locale, routeName, data) |
| core | BaseI18n.loadRouteTranslationsCore(locale, routeName, translations, merge) is gone | loadTranslationsCore(locale, translations, merge, routeName) |
| core | tc passes the count as a Number, so a custom rule sees 1.5 where it saw 1 | — |
| utils | resolveI18nConfigWithRuntimeOverrides(base, pub, warn) is gone | createI18nConfigResolver(warn)(base, pub). It memoizes, reads the environment once, and its result is frozen in dev |
| utils | Subpaths ./route, ./route-pattern, ./build, ./payload-stats, ./payload-config removed | They were module internals |
| utils | mergeRouteTranslationsWithRoot is gone | deepMergeTranslationsRecursive, which merges at every depth |
| utils | deepMergeTranslations is gone | deepMergeTranslationsRecursive. The old name stopped at two levels until this release |
| utils | applyAutoDetectLanguage is gone | hasExplicitPreference || !autoDetect ? locale : detectLocaleFromAcceptLanguage(header, validLocales) ?? locale |
| utils | LocaleHeadLink is a union by rel | Code that builds one with a free rel, or an alternate with no hreflang, no longer compiles |
@i18n-micro/path-strategy, @i18n-micro/route-strategy
| Package | Change | Action |
|---|---|---|
| path-strategy | switchLocaleRoute(fromLocale, toLocale, route, options) is switchLocaleRoute(toLocale, route, options). An old JS call returns a wrong result, with no error | Drop the first argument |
| path-strategy | localeRoute(locale, routeOrPath) lost its third parameter, getRouteBaseName(route) its second. Both were ignored | Drop them |
| path-strategy | Strategy alias removed | Use I18nPathStrategy |
| path-strategy | createPathStrategy no longer builds absolute URLs | Use new DomainPathStrategy(ctx) when locales have baseUrl |
| path-strategy | Subpaths /prefix, /no-prefix, /prefix-except-default, /prefix-and-default removed. Gone from the root: BasePathStrategy, analyzeRoute, getRouteBaseName, buildLocalizedName, isIndexRouteName, resolvePathWithParams. Gone from strategies: getRedirect, getCanonicalPath, the get* config getters. The context drops globalLocaleRoutes, routeLocales, routesLocaleLinks, noPrefixRedirect. RouterAdapter needs getRoutes | Build from createPathStrategy or the two classes |
| route-strategy | isInternalPath is no longer re-exported | Import it from @i18n-micro/utils/app-path |
| route-strategy | Helper exports gone: buildFullPath, buildFullPathNoPrefix, buildRouteName, extractLocalizedPaths, isLocalizationDisabledForPage and others. filesLocaleRoutes and fallbackRedirectComponentPath removed. New: generateRoutes, Rules, nameOfPath, routeKey | Use RouteGenerator or generateRoutes |
@i18n-micro/test-utils, @i18n-micro/types-generator, @i18n-micro/devtools-ui
| Package | Change | Action |
|---|---|---|
| test-utils | The mock is a BaseI18n: tc('a', 1.5) takes the last form, td('nope') is Invalid Date, tdr follows the active locale, t(miss, undefined, '') is the key, root keys answer on a route, a miss no longer logs | Fix tests that pinned what the app never did |
| test-utils | settRouteName and $settRouteName removed | Use setRouteName |
| test-utils | The strategy fake's switchLocaleRoute follows the new signature | Drop the first argument |
| test-utils | isolated is gone from createI18nTestContext (every context always was); ./publish-smoke removed | — |
| types-generator | I18nTypesPlugin(options) is a plain Vite and Rollup plugin: no .vite(), .webpack(), .esbuild(). flattenKeys, isPlainTranslationObject removed; getTypesString and generateTypes are sync; translationDir may be an array | I18nTypesPlugin.vite(o) → I18nTypesPlugin(o); for webpack or esbuild call generateTypes in a script |
| types-generator | Keys are what t() reads: object paths added, __proto__ and files outside the locale layout dropped, values typed by their JSON. The Nuxt module reads i18n.translationDir, inline options, layers and additionalTranslationDirs. @nuxt/kit is an optional peer, @nuxt/schema and vite are new optional peers | — |
| devtools-ui | /files and /file are replaced by /translations; a cross-site or foreign-host request is a 403, /save needs application/json; updates come over Vite's socket, no 2s poll; Astro uses i18nDevToolsIntegration. vite is an optional peer | — |
| devtools-ui | createBridge takes a base option; a failed save to a reachable endpoint throws and rolls back | Pass base if you changed the plugin's base |
| devtools-ui | /save accepts only an existing .json inside translationDir and no longer creates files | Create new locale files yourself |
From v2 to v3
Removed options
i18n: {
- includeDefaultLocaleRoute: true,
+ strategy: 'prefix',
- fallbackRedirectComponentPath: '~/components/MyRedirect.vue',
- experimental: { hmr: true, previousPageFallback: true },
+ hmr: true,
}
includeDefaultLocaleRoute: trueisstrategy: 'prefix';falseis the defaultprefix_except_default.fallbackRedirectComponentPathis gone: redirects happen on the server before render and in a client route middleware. Custom redirect logic goes into a plugin that sets the locale, see Custom locale detection.experimental.hmris the roothmroption (on by default).previousPageFallbackis gone: the leaving page's keys resolve until its transition ends, with no option.
Locale state
Use useI18nLocale() instead of useState('i18n-locale') and useCookie('user-locale'):
- useState('i18n-locale').value = 'fr'
- useCookie('user-locale').value = 'fr'
+ useI18nLocale().setLocale('fr') // state and cookie together
Config at runtime
Read the config, runtime overrides included, with $getI18nConfig():
- const cookieName = useRuntimeConfig().public.i18nConfig?.localeCookie
+ const { $getI18nConfig } = useNuxtApp()
+ const cookieName = $getI18nConfig().localeCookie
Internal type aliases
If your IDE or vue-tsc cannot resolve #i18n-internal/* imports, map them in your package.json. Nuxt resolves them at build time, so this is rarely needed:
{
"imports": {
"#i18n-internal/plural": "nuxt-i18n-micro/internals",
"#i18n-internal/strategy": "nuxt-i18n-micro/internals",
"#i18n-internal/config": "nuxt-i18n-micro/internals"
}
}