Nuxt
With a Nuxt 3 or 4 app, @comvi/nuxt is one module entry: useI18n() and <T> arrive
auto-imported, routes get a locale prefix, and the server builds one i18n instance per request
and ships its catalog to the client in the payload.
Open the Nuxt example in StackBlitz to see server rendering, the locale in the URL (/de), plurals and <T> with a link inside the text in a running app.
Install
Section titled “Install”npm install @comvi/nuxtpnpm add @comvi/nuxtyarn add @comvi/nuxtbun add @comvi/nuxt@comvi/nuxt peer-depends on nuxt (^3 or ^4) and brings @comvi/core and @comvi/vue.
import { defineNuxtConfig } from 'nuxt/config';
export default defineNuxtConfig({ modules: ['@comvi/nuxt'], comvi: { defaultLocale: 'en', locales: ['en', 'uk'], messages: { en: { greeting: 'Hello, {name}!', legal: { tos: 'Read our <link>Terms of Service</link>' } }, uk: { greeting: 'Привіт, {name}!', legal: { tos: 'Прочитайте наші <link>Умови використання</link>' } }, }, },});comvi.messages is the inline catalog, nested or flat. It travels through runtime config, so it
must stay JSON-serializable. locales and defaultLocale drive the route prefixes and the
locales ref useI18n() returns; a locale can also be an object, { code, name?, iso?, dir? }.
<template> <NuxtPage /></template>Paths follow Nuxt 4’s app/ directory; on Nuxt 3 drop that prefix.
The module writes lang and dir on <html> from the active locale: on the server before the
first byte, and again on every locale switch in the browser. lang is the locale object’s iso
when you set one, else its code; dir is the locale object’s dir, else the direction of that
language. Set comvi.head: false to own both attributes yourself. useLocaleHead() is the call
for the rest of the head, canonical, hreflang and og:locale, and calling it as well is fine:
both write the same values.
Use in components
Section titled “Use in components”<script setup lang="ts">const { t, locale } = useI18n();</script>
<template> <h1>{{ t('greeting', { name: 'Alice' }) }}</h1> <NuxtLinkLocale to="/" :locale="locale === 'en' ? 'uk' : 'en'">Switch</NuxtLinkLocale></template>useI18n() is auto-imported and returns the @comvi/vue composable plus locales and
defaultLocale from runtime config. setLocale() loads the new catalog, applies the locale in
place and writes the cookie; it never navigates, so on its own it is a complete switcher only under
localePrefix: 'never'. When the URL carries the locale, switch by navigating: <NuxtLinkLocale :locale> as in the example, which also records the choice when the link goes to another page, or
a link to useSwitchLocalePath()(code) for the same page in another locale. Awaiting
setLocale(code) and then navigateTo(switchLocalePath(code)) is the same switch written out.
$t() works in any template without calling useI18n().
Markup inside a translation
Section titled “Markup inside a translation”<template> <T i18nKey="legal.tos"> <template #link="{ children }"> <NuxtLinkLocale to="/terms">{{ children }}</NuxtLinkLocale> </template> </T></template><T> is auto-imported. Each tag a translator wrote becomes a named slot, or an entry in the
components prop, and renders real elements. Plain t() does not parse tags. Never render a
translation with v-html.
Load from Comvi
Section titled “Load from Comvi”import { createI18n, defineComviI18n } from '@comvi/nuxt/i18n';import { cdnLoader } from '@comvi/plugin-fetch-loader';
export default defineComviI18n((options) => createI18n(options).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' })),);Install @comvi/plugin-fetch-loader and add this file next to nuxt.config.ts. The module picks
up ./comvi.i18n.* by convention and logs the path it found; comvi.i18nModule points at a file
elsewhere. defineComviI18n() types the factory and hands it back unchanged. The factory receives
the resolved comvi options and runs once for the client and once per server request, so it
returns a new instance from createI18n on every call; in development the module throws when it
gets the same instance twice. See Connect to Comvi.
The CDN endpoint URL is under Settings → Content Deploy in your Comvi project, once Content
Deploy is enabled and saved; pass it as cdnUrl. The URL format is on
CDN Deployment.
If your keys use ICU ({count, plural, …}), add compiler: icuCompiler as on
Pluralize with ICU. The platform and the compiler are independent:
either works without the other.
On Nuxt, comvi.icu: true in nuxt.config.ts adds compiler: icuCompiler to the options the
factory receives.
First paint
Section titled “First paint”The server loads the catalog, renders, and writes the instance’s cache into the Nuxt payload. The
client plugin adds that payload back before init(), so there is no gate to write and no flash of
raw keys.
Server-side rendering
Section titled “Server-side rendering”import { createI18n, defineComviI18n, loader } from '@comvi/nuxt/i18n';
export default defineComviI18n((options) => createI18n(options).use(loader({ de: () => import('./locales/de.json') })),);On every request the module calls the factory, runs comvi.setup.ts against the new instance,
awaits init(), lets the route middleware detect the locale from the URL path, the query
parameter, the cookie or the Accept-Language header, and after the render serializes the loaded
cache into the payload, so the client hydrates with no second fetch. Server-side loading is the
loader capability: compose .use(loader(map)) or .use(cdnLoader()) in comvi.i18n.ts. Without
that file the module builds core’s base instance, with no loader and no ICU compiler, and
comvi.messages is the only catalog it ever has. In a Nitro route the server auto-imports are
useTranslation(event), loadMessages(event, locale), getRequestI18n(event, locale) and
getServerRuntimeConfig(event); the app-side composables do not exist there. useTranslation(event)
resolves the locale as the route middleware does, from a locale prefix on the path and then
localeDetection.order, except that a prefixless path is not read as the default locale, since an
API route carries no prefix to omit. The rendered
<html> carries lang and dir for the request’s locale, so a right-to-left locale arrives
right-to-left on first paint. The full pattern is on Render on the server.
Locale routing
Section titled “Locale routing”localePrefix |
Default locale | Other locale |
|---|---|---|
as-needed |
/about |
/uk/about |
always |
/en/about |
/uk/about |
never |
/about |
/about, locale from cookie or header |
export default defineNuxtConfig({ modules: ['@comvi/nuxt'], comvi: { defaultLocale: 'en', locales: ['en', 'uk'], localeCookie: { name: 'locale', sameSite: 'strict' }, localeDetection: { order: ['query', 'cookie', 'accept-language'], queryParam: 'lang' }, },});The route middleware detects the locale, persists it in the locale cookie (i18n_locale unless
localeCookie names another), calls
setLocale() so the catalog is in place before the page renders, and redirects when the URL and
the rendered locale disagree. localeCookie is false, a cookie name, or
{ name, maxAge, secure, sameSite, domain }; localeDetection is false or
{ order, queryParam, headerName, resolveAcceptLanguage }, and query reads nothing until
queryParam names a parameter. The same two options, with the same values and defaults except
the cookie name, configure @comvi/next’s middleware. <NuxtLinkLocale> and useLocalePath()
leave absolute URLs, mailto: and tel: links, and query-only or fragment-only targets as
written. Routing and Accept-Language matching share one implementation with @comvi/next; the
rules are on Language detection on the server.
How it works
Section titled “How it works”The module registers a Nuxt plugin that runs before the others. On each run it reads the comvi
runtime config, calls the factory from comvi.i18n.ts (or the generated default, which is core’s
base instance) with those options, and gets back a @comvi/vue wrapper whose ssrLocale is the
current locale state, defaultLocale at first. It then runs comvi.setup.ts, awaits init(), and
only afterwards calls nuxtApp.vueApp.use(i18n) and provides $i18n, so no component renders
against an instance that is still loading. With head on, the same run registers one head entry
that computes htmlAttrs.lang and htmlAttrs.dir from the locale state, so the server HTML has
both; in the browser a watcher writes the new values in the same flush as the translated text,
and touches an attribute only while its current value is the one it wrote last, so your own
useHead() entry for <html> wins.
In development the module’s generated code remembers every instance the factory returned and throws when it sees one a second time. A factory that hands back a module-level singleton would share one locale and one catalog across every request. Production builds carry no guard.
On the server that instance belongs to one request. It is parked on the request event, and once
the app has rendered the plugin serializes the translation cache, keyed locale:namespace, into
the Nuxt payload. After the response is written, every instance the request built is destroyed,
which is why a server utility called after the response throws rather than translating from a
cleared cache.
The route middleware runs on every navigation and decides the locale in a fixed order: the path
prefix, then localeDetection.order, by default the query parameter named in queryParam when you
set one, then the cookie, then Accept-Language on the server only, then defaultLocale. Under
as-needed a prefixless path other than / is the default locale’s URL, decided after a leading
query source and before the rest; / is too when the navigation comes from the same page under
a locale prefix (/de to /, what useSwitchLocalePath() returns for the default locale), and
on every other visit / is decided by localeDetection.order and redirects to that locale’s
prefixed root when it is not the default. When the result differs from the instance’s locale it calls
$i18n.setLocale(), which loads that locale’s catalog before applying it, writes the locale state
and the cookie, and redirects when the prefix mode says the URL should look different. init()
loaded defaultLocale and every locale in the fallbackLocale chain for the active namespaces;
this is the step that loads the visitor’s locale. A namespace a page activates during the render
loads its fallback chain the same way, as long as the page awaits the activation. With
ssr: false there is no server step and no payload: the browser makes the same requests itself
on first paint, the active locale and its fallback chain for the active namespaces.
In the browser the plugin reads the payload and calls addMessages() before init(), so the
default-namespace load is a cache hit and nothing is fetched on first paint. From then on
localeChanged writes the locale state and the cookie, and a watcher on the locale state mirrors
the middleware’s decisions back onto the instance, so a route change and a setLocale() call end
in the same place.
useI18n() is the @comvi/vue composable with two refs added, so the reactivity model is Vue’s:
t() tracks the locale ref and the cache revision, and a template that calls it re-renders on a
switch or a namespace load and at no other time. The Vue page has
that model in full. A missing key returns the key and emits missingKey, on the server and in the
browser alike.
Errors
Section titled “Errors”| what happened | message |
|---|---|
useI18n() in an app without the module in modules |
[@comvi/nuxt] i18n not configured. Add "@comvi/nuxt" to `modules` in nuxt.config.ts. |
a comvi key from an older release |
at build time, naming the key to write, as in [@comvi/nuxt] `comvi.detectBrowserLanguage` is not an option: use `localeCookie` and `localeDetection`.; the full list is on Error codes |
comvi.i18nModule points at a file that does not exist |
[@comvi/nuxt] Failed to resolve comvi i18nModule path: "./comvi.i18n". |
the comvi.i18n.ts factory returned the same instance twice (development only) |
[@comvi/nuxt] comvi i18nModule returned an instance it already returned. Every call builds a separate instance (the client, and each server request), so return a new instance from createI18n on every call. |
More messages and their fixes are on Troubleshooting.
Read next
Section titled “Read next”- Connect to Comvi: the CDN URL, and a wording change that ships without a redeploy.
- Render on the server: the server utilities and the request lifecycle.
- Nuxt API reference: every module option,
defineComviI18n, the setup types, the composables.