Svelte i18n Integration
Comvi’s Svelte integration uses Svelte’s native store system for reactive translations. It targets Svelte 5 (runes) and works with SvelteKit for SSR.
Installation
Section titled “Installation”pnpm add @comvi/svelte @comvi/core @comvi/plugin-fetch-loader-
Create the i18n instance
src/lib/i18n.ts import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-distribution-id',})); -
Set up the context in your root layout
src/routes/+layout.svelte <script lang="ts">import { setI18nContext } from '@comvi/svelte';import { i18n } from '$lib/i18n';let { children } = $props();setI18nContext(i18n);</script>{@render children()} -
Use in components
src/routes/+page.svelte <script lang="ts">import { useI18n } from '@comvi/svelte';const { t, locale } = useI18n();</script><h1>{$t('hello.world')}</h1><p>{$t('welcome.message', { name: 'Alice' })}</p><p>Current locale: {$locale}</p>
Context Setup
Section titled “Context Setup”Comvi uses Svelte’s context API to make the i18n instance available to all child components. Call setI18nContext once in your root layout. All descendant components can then access translations with useI18n() or getI18nContext().
<script lang="ts"> import { setI18nContext } from '@comvi/svelte'; import { i18n } from '$lib/i18n';
let { children } = $props();
setI18nContext(i18n);</script>
{@render children()}Reading the Context Directly
Section titled “Reading the Context Directly”If you need the raw i18n instance (for example, in a utility function), use getI18nContext():
<script lang="ts"> import { getI18nContext } from '@comvi/svelte';
const i18n = getI18nContext();</script>useI18n() Store
Section titled “useI18n() Store”The primary function for accessing translations and locale state. It returns Svelte stores that you subscribe to with the $ prefix.
<script lang="ts"> import { useI18n } from '@comvi/svelte';
const { t, locale, isLoading, setLocale } = useI18n();</script>
{#if $isLoading} <p>Loading translations...</p>{:else} <h1>{$t('page.title')}</h1> <p>Current locale: {$locale}</p>{/if}Returned values:
| Property | Type | Description |
|---|---|---|
t | Readable<(key, params?) => string> | Reactive translation function (use as $t(...)) |
tRaw | Readable<(key, params?) => TranslationResult> | Structured output for advanced renderers (use as $tRaw(...)) |
locale | Readable<string> | Current language (read-only store, use setLocale() to change) |
isLoading | Readable<boolean> | Whether translations are being loaded |
setLocale | (lang: string) => Promise<void> | Switch to a different language |
Changing Language
Section titled “Changing Language”Use setLocale() to switch languages. The locale store is read-only:
<script lang="ts"> import { useI18n } from '@comvi/svelte';
const { locale, setLocale } = useI18n();</script>
<select value={$locale} onchange={(e) => setLocale(e.currentTarget.value)}> <option value="en">English</option> <option value="de">Deutsch</option> <option value="fr">Francais</option></select><script lang="ts"> import { useI18n } from '@comvi/svelte';
const { locale, setLocale } = useI18n();</script>
<button onclick={() => setLocale('de')}> Switch to German</button><p>Current: {$locale}</p>The <T> Component
Section titled “The <T> Component”For translations containing HTML, use the <T> component for safe interpolation:
<script lang="ts"> import { T } from '@comvi/svelte';</script>
<!-- Translation: "Read our <link>terms of service</link>" --><T i18nKey="legal.tos" components={{ link: { tag: 'a', props: { href: '/terms' } }, }}/>How <T> Renders
Section titled “How <T> Renders”Unlike the React and SolidJS bindings, Svelte’s <T> builds an HTML string and injects it
with {@html}. Consequences:
-
componentsmappings can only be HTML tag names or{ tag, props }objects — you can’t pass a Svelte component (Svelte components aren’t plain functions you can call to get markup). -
Output is sanitized: text is HTML-escaped, attribute names like
on*/srcdoc/formactionare stripped, and only tags in a safe allow-list are emitted (others fall back to<span>). The default allow-list covers common inline/block formatting tags (a,strong,em,br,ul/li,span, …). Override it with theallowedTagsprop:<script lang="ts">import { T } from '@comvi/svelte';const allowedTags = new Set(['strong', 'em', 'a', 'mark']);</script><T i18nKey="rich.message" {allowedTags} components={{ hl: 'mark' }} />
Namespaces
Section titled “Namespaces”Load translations from a specific namespace:
<script lang="ts"> import { useI18n } from '@comvi/svelte';
const { t } = useI18n('dashboard');</script>
<h1>{$t('page.title')}</h1>Missing Keys & Error Handling
Section titled “Missing Keys & Error Handling”useI18n() exposes onMissingKey and onLoadError (both bound directly to the core instance).
Register them once — for example in your root layout — and tear them down via the returned
cleanup. The onMissingKey callback may return a replacement (TranslationResult | void):
<script lang="ts"> import { onDestroy } from 'svelte'; import { setI18nContext, useI18n } from '@comvi/svelte'; import { i18n } from '$lib/i18n';
let { children } = $props();
setI18nContext(i18n);
const { onMissingKey, onLoadError } = useI18n();
const offMissing = onMissingKey((key, locale, namespace) => { console.warn(`Missing: ${namespace}:${key} (${locale})`); return `[${key}]`; // optional fallback string }); const offError = onLoadError((locale, namespace, error) => { console.error(`Failed to load ${namespace} for ${locale}:`, error); });
onDestroy(() => { offMissing(); offError(); });</script>
{@render children()}You can also handle missing keys per-render with the <T> component’s fallback prop or by
providing slotted fallback content (rendered when the key is missing):
<T i18nKey="optional.banner" fallback="Welcome!" />
<T i18nKey="optional.note"> <p>Default note shown when the key is missing.</p></T>Svelte 5 Runes
Section titled “Svelte 5 Runes”Comvi works with Svelte 5 runes. The stores returned by useI18n() are compatible with the $ rune syntax:
<script lang="ts"> import { useI18n } from '@comvi/svelte';
const { t, locale, setLocale } = useI18n();</script>
<h1>{$t('page.title')}</h1>
<select value={$locale} onchange={(e) => setLocale(e.target.value)}> <option value="en">English</option> <option value="de">Deutsch</option></select>Derived State with Runes
Section titled “Derived State with Runes”In Svelte 5, use $derived for computed translations:
<script lang="ts"> import { useI18n } from '@comvi/svelte';
const { t } = useI18n();
let count = $state(0); let cartMessage = $derived($t('cart.items', { count }));</script>
<p>{cartMessage}</p><button onclick={() => count++}>Add item</button>SvelteKit SSR
Section titled “SvelteKit SSR”@comvi/svelte does not ship a SvelteKit adapter. The pattern is: create a fresh i18n
instance per request in +layout.server.ts, return its loaded translations, and build the
client context from that payload.
Loading Translations Server-Side
Section titled “Loading Translations Server-Side”import { createI18n } from '@comvi/svelte';import { FetchLoader } from '@comvi/plugin-fetch-loader';import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ url, request }) => { // Detect from URL path first, then Accept-Language. Never from navigator (no DOM here). const fromPath = url.pathname.split('/')[1]; const fromHeader = request.headers.get('accept-language')?.split(',')[0]?.split('-')[0]; const locale = ['en', 'de'].includes(fromPath) ? fromPath : fromHeader ?? 'en';
// Fresh instance for this request only. const i18n = createI18n({ locale, fallbackLocale: 'en' }).use( FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id' }), ); await i18n.init();
const namespace = i18n.getDefaultNamespace(); return { locale, // Keyed by "locale:namespace" so multi-namespace apps round-trip cleanly. messages: { [`${locale}:${namespace}`]: i18n.getTranslations(locale, namespace) ?? {}, }, };};Hydrating on the Client
Section titled “Hydrating on the Client”Create the client i18n instance from the serialized messages so hydration starts from the
same cache — no duplicate fetch:
<script lang="ts"> import { createI18n, setI18nContext } from '@comvi/svelte'; import type { LayoutProps } from './$types';
let { data, children }: LayoutProps = $props();
const i18n = createI18n({ locale: data.locale, fallbackLocale: 'en', translation: data.messages, });
setI18nContext(i18n, { autoInit: false });</script>
<html lang={data.locale}> <body> {@render children()} </body></html>If you lazy-load extra namespaces, include them in the returned messages before rendering
any component that needs them.
SEO with <svelte:head>
Section titled “SEO with <svelte:head>”Add language metadata for search engines:
<script lang="ts"> import { useI18n } from '@comvi/svelte';
const { locale } = useI18n();</script>
<svelte:head> <html lang={$locale} /> <link rel="alternate" hreflang="en" href="https://yoursite.com/en" /> <link rel="alternate" hreflang="de" href="https://yoursite.com/de" /></svelte:head>TypeScript
Section titled “TypeScript”Enable type-safe translations with auto-generated types. The Comvi CLI emits a .d.ts that augments the TranslationKeys interface in @comvi/core for APIs that expose the typed core translation function.
import { createI18n } from '@comvi/core';// Side-effect import — augments @comvi/core's TranslationKeys interfaceimport './types/i18n';
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en',});The current Svelte $t store is typed as (key: string, params?: TranslationParams) => string, so it remains permissive even when generated types are imported. Use the generated types with direct core APIs or other bindings that expose typed t() overloads. See Type-Safe Translations for the full setup.