# Svelte

Add Comvi i18n to a Svelte 5 or SvelteKit app with @comvi/svelte: the i18n instance in context, $t and $locale as stores, <T> for rich text and per-request instances for SSR.

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

With a Svelte 5 app, plain Vite or SvelteKit, `@comvi/svelte` puts the i18n instance on Svelte's
context, hands components `$t` and `$locale` as stores, and renders markup inside a translation
through `<T>`.

To watch `$t` re-render on a switch, open the [Svelte example in StackBlitz](https://stackblitz.com/github/comvi-io/comvi-js/tree/main/examples/svelte): plurals, `<T>` with a link and lazy catalogs, in the browser.

## Install
**npm**

```bash
npm install @comvi/svelte
```

**pnpm**

```bash
pnpm add @comvi/svelte
```

**yarn**

```bash
yarn add @comvi/svelte
```

**bun**

```bash
bun add @comvi/svelte
```

`@comvi/svelte` peer-depends on `svelte` (^5.3) and brings `@comvi/core`.

## Setup

`src/routes/+layout.svelte`

```svelte
<script lang="ts">
import { createI18n, setI18nContext } from '@comvi/svelte';
import type { Snippet } from 'svelte';

const { children }: { children: Snippet } = $props();

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

setI18nContext(i18n);
</script>

{@render children()}
```

`createI18n()` returns the instance and `setI18nContext()` stores it in Svelte context, where
`useI18n()` in any descendant finds it. It also schedules `init()` in a microtask, so there is no
`init()` to write, and an explicit `await i18n.init()` in the same script still wins without a
double init. Pass `{ autoInit: false }` to run it yourself, and `{ onError }` to see a failed
auto-init, which otherwise reaches only the instance's own `onError`. In a plain Vite app the same code goes in
`src/App.svelte`. The catalog options are on the [Quick start](/docs/i18n/quick-start).

## Use in components

`src/routes/+page.svelte`

```svelte
<script lang="ts">
import { useI18n } from '@comvi/svelte';

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

<h1>{$t('greeting', { name: 'Alice' })}</h1>
<select value={$locale} onchange={(e) => setLocale(e.currentTarget.value)}>
<option value="en">English</option>
<option value="uk">Українська</option>
</select>
```

`useI18n()` returns stores, so `$`-prefix them in markup: `$t` renders a string, `$tRaw` the parts
array. Both derive from the locale store and a cache-revision store, so `$t` in markup or inside a
`$derived` re-runs when the locale or the catalog changes and at no other time. `setLocale()` is a
plain function that resolves once the new locale's catalog has loaded.

### Markup inside a translation

`src/locales/en.json`

```json
{
"help": "For support, <link>visit our docs</link>, <bold>email us</bold> or <btn>open a ticket</btn>.",
"legal": "See our <policy>privacy policy</policy>."
}
```

`src/Help.svelte`

```svelte
<script lang="ts">
import { T } from '@comvi/svelte';
import FancyButton from './FancyButton.svelte';
</script>

```

Each tag maps to an element name, a `{ tag, props }` config, or a Svelte component that receives
the tag's content as its `children` snippet. `<T>` builds real DOM nodes, never an HTML string; a
tag with no mapping renders its inner text. Plain `$t()` does not parse tags.

## Load from Comvi

`src/routes/+layout.svelte`

```svelte
<script lang="ts">
import { createI18n, setI18nContext } from '@comvi/svelte';
import { cdnLoader } from '@comvi/plugin-fetch-loader';
import type { Snippet } from 'svelte';

const { children }: { children: Snippet } = $props();

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

setI18nContext(i18n);
</script>

{@render children()}
```

Install `@comvi/plugin-fetch-loader` and compose `.use(cdnLoader())` inside the `createI18n`
expression, before `setI18nContext()` schedules `init()`: `init()` runs the queued plugins once,
and the CDN plugin registers its loader there. The rest of the walkthrough is on
[Connect to Comvi](/docs/i18n/connect-comvi). A server render does not wait for that fetch,
so a SvelteKit app that renders on the server loads the catalog in a `load` function instead,
as on [SvelteKit SSR](/docs/i18n/ssr#sveltekit-and-solidstart).

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/routes/+page.svelte`

```svelte
<script lang="ts">
import { useI18n } from '@comvi/svelte';

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

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

With a catalog in flight during the first paint, `$isInitializing` is the gate: there is no
provider component and no `fallback` prop. The store is `true` from `setI18nContext()` on, so
the first render is already inside the gate, stays `true` while `init()` runs, and goes `false`
once the default namespace is in. `$isLoading` goes `true` again for every later
load, which is why the gate is `isInitializing` and not `isLoading`.

## Server-side rendering

`src/routes/+layout.svelte`

```svelte
<script lang="ts">
import { useI18n } from '@comvi/svelte';

const { locale, dir } = useI18n();

$effect(() => {
    document.documentElement.lang = $locale;
    document.documentElement.dir = $dir;
});
</script>
```

`@comvi/svelte` ships no SvelteKit binding of its own and needs none. Build a fresh instance per
request and never mutate a module-level one on the server, or one user's locale bleeds into
another's response. A `load` function returns plain data, `{ locale, messages }`, never the
instance; construct the instance in `+layout.svelte` with `setI18nContext(i18n, { autoInit: false })`
seeded from that payload, so the server and the client render the same text and there is no
hydration mismatch. The complete example is on
[SvelteKit SSR](/docs/i18n/ssr#sveltekit-and-solidstart).

`<svelte:head>` inserts into `<head>`, so `lang` and `dir` go on `document.documentElement` from an
`$effect`, or through `<svelte:html lang={$locale} dir={$dir} />` on Svelte 5.16 or newer, which
also applies during SSR.

## How it works

`setI18nContext()` is Svelte's `setContext()` under one fixed key, so the instance is visible to
every component below the one that called it and to nothing else. `useI18n()` is the matching
`getContext()` and therefore runs during component initialisation only; called with no ancestor
that set the context, it throws the named error in the table at the end rather than returning an
empty bag.

The stores are created once per instance and cached, so every `useI18n()` call and every `<T>` in
the tree shares the same store objects. The locale store listens to `localeChanged`; the loading
and initializing stores to `loadingStateChanged`; the initialized store to `initialized` and
`destroyed`; and a cache-revision store counts every other revision event (namespace loaded,
translations cleared, config changed). Each store subscribes to the instance only while a component
reads it and unsubscribes when the last reader leaves, so an unmounted page costs nothing.

`$t` is a `derived` of the locale store and the cache-revision store, one per instance and
namespace, whose value is a translation function bound to that namespace. That is why `$t('key')`
in markup updates on a locale switch or a namespace load: the store emits a new function and
Svelte re-runs the expression. A translation pinned to a locale with `$tRaw('key', { locale })`
still re-derives with the store, since the store is shared, but resolves against the pinned
locale.

`setLocale()` is the instance's `setLocale()`. The catalog for the new locale is loaded
before the locale is assigned, when a loader is composed, and `localeChanged` fires after both, so
the locale store moves once and no frame shows half-translated text. When several switches
overlap, the latest wins and the superseded ones apply nothing.

`init()` runs from the microtask `setI18nContext()` scheduled: it raises `isInitializing`, runs
the plugins, loads the default namespace in the current locale and in each fallback locale, then
sets `isInitialized` and emits `initialized`. A fallback catalog that does not load is reported,
and `init()` still resolves. With an inline catalog the load is a cache hit. A missing key returns the key itself and emits
`missingKey`; `strict: 'dev'` adds a console warning in development.

On the server the microtask still runs, against whatever instance the layout built. A module-level
instance would be shared by every concurrent request, and the first `setLocale()` from one request
would move the locale under another's render. Building the instance inside `+layout.svelte` from
the `load` data gives each request its own, and `autoInit: false` with the catalog already in it
leaves nothing to await.

## Errors

| what happened | message |
|---|---|
| `useI18n()` with no ancestor that called `setI18nContext()` | `[@comvi/svelte] i18n context not found. Call setI18nContext(i18n) in your root component (e.g., App.svelte).` |
| `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()`.
- [Svelte API reference](/docs/i18n/api/svelte): every export, the standalone stores and `<T>` props.
