# Vue

Add Comvi i18n to a Vue 3 app: install @comvi/vue, install the plugin with app.use(), translate with useI18n() and <T>, load from the CDN and render on the server.

Canonical: https://comvi.io/docs/i18n/vue

With a Vue 3 app, `@comvi/vue` is a plugin: `app.use(i18n)` at the root, `useI18n()` in
components, and `<T>` for translations that carry markup. `t()` is reactive the way a `computed`
is, so a locale switch re-renders exactly the templates that call it.

The [Vue example in StackBlitz](https://stackblitz.com/github/comvi-io/comvi-js/tree/main/examples/vue) runs the switcher, plurals and `<T>` with a link inside the text, and fetches each catalog the first time its locale is selected.

## Install
**npm**

```bash
npm install @comvi/vue
```

**pnpm**

```bash
pnpm add @comvi/vue
```

**yarn**

```bash
yarn add @comvi/vue
```

**bun**

```bash
bun add @comvi/vue
```

`@comvi/vue` peer-depends on `vue` (^3) and brings `@comvi/core`.

## Setup

`src/main.ts`

```ts

const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: { en: { greeting: 'Hello, {name}!' }, uk: { greeting: 'Привіт, {name}!' } },
});

createApp(App).use(i18n).mount('#app');
```

`createI18n()` returns the i18n instance wrapped for Vue: `locale` is a ref, `isInitializing` a
readonly ref, and `t()` tracks both. `app.use(i18n)` provides the instance to every component,
registers `$t`, `$tRaw` and `$i18n`, and starts `init()` fire-and-forget. The two `use`s are
different verbs: `app.use(i18n)` is Vue's plugin install, and Comvi's `.use(installer)` composes a
capability onto the instance. The options are on the
[Quick start](/docs/i18n/quick-start#create-the-i18n-instance).

## Use in components

`src/App.vue`

```vue
<script setup lang="ts">

const { t, locale, setLocale } = useI18n();
</script>

<template>
<h1>{{ t('greeting', { name: 'Alice' }) }} ({{ locale }})</h1>
<button @click="setLocale('uk')">Українська</button>
</template>
```

`t()` inside a template or a `computed` reads the locale ref and the cache revision, so it
re-evaluates when either changes and at no other time. A `t()` called once in `setup()` and stored
in a plain `const` is computed once. `locale` is a `Ref<string>`: `setLocale()` resolves once the
new locale's catalog is loaded, and assigning `locale.value = 'uk'` triggers the same switch.
`useI18n('admin')` binds a namespace, and `dir` is a `ComputedRef<"ltr" | "rtl">`.

### Markup inside a translation

`src/locales/en.json`

```json
{
"help": "Read <link>our docs</link> or <bold>contact us</bold>.",
"legal.tos": "Read our <tos>Terms of Service</tos>"
}
```

`src/Help.vue`

```vue
<script setup lang="ts">
</script>

<template>

    <template #link="{ children }">
      <a href="/docs">{{ children }}</a>
    </template>
    <template #bold="{ children }">
      <strong>{{ children }}</strong>
    </template>

</template>
```

A named slot per tag receives the tag's content as `children`. The `components` prop takes an HTML
tag name, a Vue component or a `{ component, props }` config, and wins over a slot of the same
name. Every attribute other than `i18nKey`, `params`, `ns`, `locale`, `fallback`,
`markers` and `components` is an interpolation param in camelCase, so `class` and `style`
go on an element around `<T>`. Default slot content renders when the key is missing. Plain `t()`
does not parse tags. See [Tag interpolation](/docs/i18n/tag-interpolation).

## Load from Comvi

`src/main.ts`

```ts

const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
}).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));

createApp(App).use(i18n).mount('#app');
```

Install `@comvi/plugin-fetch-loader` and compose `.use(cdnLoader())` before `app.use(i18n)`.
The install starts `init()`, and `init()` is the one thing that runs the queued plugins: an
installer composed after it leaves its plugin waiting for a second `init()` that never comes, and
development warns, opening with `[@comvi/vue] .use(installer) ran after app.use(i18n) started
init()`. See [Connect to Comvi](/docs/i18n/connect-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](/docs/platform/cdn#url-format).
If your keys use ICU (`{count, plural, …}`), add `compiler: icuCompiler` as on
[Pluralize with ICU](/docs/i18n/pluralization). The platform and the compiler are independent:
either works without the other.
## First paint

`src/App.vue`

```vue
<script setup lang="ts">

const { t, isInitializing } = useI18n();
</script>

<template>
<div v-if="isInitializing" class="spinner" />
<h1 v-else>{{ t('greeting', { name: 'Alice' }) }}</h1>
</template>
```

Vue has no `fallback` prop, so `isInitializing` gates the first paint in the component that owns
the children, and raw keys never render. By the time the root component renders, `init()` has
started and the ref is `true`; it drops to `false` on the `initialized` event. `isLoading` goes
`true` again for every later load, which is why the gate is `isInitializing` and not `isLoading`.

## Server-side rendering

`server.ts`

```ts

export async function render(localeFromRequest: string) {
const app = createSSRApp(App);
const i18n = createI18n({
    locale: 'en',
    ssrLocale: localeFromRequest,
    messages: { en: { greeting: 'Hello' }, uk: { greeting: 'Привіт' } },
});
app.use(i18n);
await i18n.init();
try {
    return await renderToString(app);
} finally {
    await i18n.destroy(); // runs plugin cleanups; a collected instance never does
}
}
```

Build one instance per request. A module-scope instance is shared by every concurrent request, and
one visitor's `setLocale()` moves the locale under everyone else's render. `ssrLocale` seeds the
reactive ref and the core locale before the first render, so the client hydrates against the
locale it was served; its catalogs have to be on the instance by then, inline or loaded. Await `i18n.init()` before `renderToString()`: `app.use(i18n)` starts it
fire-and-forget, and a render that outruns the load serializes an empty cache. On Nuxt,
[`@comvi/nuxt`](/docs/i18n/nuxt) does all three per request. See
[Render on the server](/docs/i18n/ssr).

## Options API

Installing the plugin adds `$t(key, params?)`, `$tRaw(key, params?)` and `$i18n` to every
component: `{{ $t('greeting', { name: 'Alice' }) }}` works in any template without `useI18n()`,
and so does `$i18n.setLocale('uk')`. `$i18n.locale` is a ref.

## How it works

`createI18n()` builds a core instance and wraps it in a `VueI18n` object. The wrapper keeps a
handful of shallow refs: the locale, `isLoading`, `isInitializing`, `isInitialized`, a cache
revision and a config revision. It subscribes once to the core's revision events and mirrors each
event into the matching ref: `localeChanged` writes the locale, `namespaceLoaded` and
`messagesCleared` bump the cache revision, `loadingStateChanged` writes the two flags,
`initialized` syncs everything. Components never touch the core's events; they read refs.

`t()` reads the locale ref, the cache revision and the config revision, then calls the core. That
is the whole reactivity model: any template, `computed` or `watchEffect` that calls `t()` depends
on those three refs, so a locale switch, a namespace arriving, a `clearMessages()` or a change
of default namespace re-evaluates it, and nothing else does. The formatters default to the same
locale ref, so `formatDate()` in a template follows a switch too.

`setLocale()` goes through a queue on the wrapper, so two quick calls run in order rather than
racing, and `locale.value = 'uk'` is the same call through a writable computed. Underneath, the
core's loader loads every active namespace for the new locale before it assigns the locale and
emits `localeChanged`, so the ref changes once, after the catalog is complete, and the templates
re-render with finished text rather than a half-translated frame.

`app.use(i18n)` runs `install()`: it provides the wrapper under the injection key `useI18n()`
reads, registers the `$t`, `$tRaw` and `$i18n` globals, and starts `init()` if nothing has.
`init()` raises `isInitializing`, runs the queued plugins, fetches the default namespace in the
current locale and in every `fallbackLocale` entry, then sets `isInitialized` and emits
`initialized`. A fallback catalog that fails to load is only reported. With an inline catalog the
load is a cache hit; with a loader this is where the first fetch happens.

A missing key is not an error. The core looks in the current locale, then in each fallback locale
in order, and when nothing matches it emits `missingKey` and returns the key itself, so a raw key
in a template means a missing translation, not a broken plugin. `strict: 'dev'` adds a console
warning in development.

On the server, `init()` is fire-and-forget from `install()` and nothing awaits it, so a render
that starts before the load finishes serializes whatever is in the cache. Awaiting `i18n.init()`
yourself, on an instance built for that request, is what makes the server HTML and the client's
first render agree.

## Errors

| what happened | message |
|---|---|
| `useI18n()` in an app without `app.use(i18n)` | `[@comvi/vue] useI18n must be used within a Vue app with i18n plugin installed. Make sure you called app.use(i18n) before using this composable.` |
| `<T>` in an app without `app.use(i18n)` | `[@comvi/vue] <T> component must be used within a Vue app with i18n plugin installed` |
| `useI18nLoader()` on an instance with no loader composed | development: `[@comvi/core] This i18n instance has no loader capability. Compose it: .use(attachLoader) from "@comvi/core/loader", or loader(map).`; a production build throws the short form quoted on [Error codes](/docs/i18n/errors#a-capability-was-never-composed) |

More messages and their fixes are on [Troubleshooting](/docs/i18n/troubleshooting).

## Read next

- [Connect to Comvi](/docs/i18n/connect-comvi): the CDN URL, and a wording change that ships without a redeploy.
- [Load translations on demand](/docs/i18n/loading-translations): `loader(map)`, namespaces and `useI18nLoader()`.
- [Vue API reference](/docs/i18n/api/vue): every export, composable, `<T>` prop and the injection key.
