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: plurals, <T> with a link and lazy catalogs, in the browser.
Install
Section titled “Install”npm install @comvi/sveltepnpm add @comvi/svelteyarn add @comvi/sveltebun add @comvi/svelte@comvi/svelte peer-depends on svelte (^5.3) and brings @comvi/core.
<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.
Use in components
Section titled “Use in components”<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
Section titled “Markup inside a translation”{ "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>."}<script lang="ts"> import { T } from '@comvi/svelte'; import FancyButton from './FancyButton.svelte';</script>
<T i18nKey="help" components={{ link: 'a', bold: 'strong', btn: FancyButton }} />
<T i18nKey="legal" components={{ policy: { tag: 'a', props: { href: '/privacy' } } }} />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
Section titled “Load from Comvi”<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. 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.
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.
First paint
Section titled “First paint”<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
Section titled “Server-side rendering”<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.
<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
Section titled “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
Section titled “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 |
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.
- Load translations on demand:
loader(map), namespaces anduseI18nLoader(). - Svelte API reference: every export, the standalone stores and
<T>props.