Skip to content

Svelte i18n Integration

Comvi’s Svelte integration uses Svelte’s native store system for reactive translations. It supports both Svelte 4 (reactive $: declarations) and Svelte 5 (runes), and works with SvelteKit for SSR.

Terminal window
pnpm add @comvi/svelte @comvi/core @comvi/plugin-fetch-loader
  1. 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',
    }));
  2. 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';
    setI18nContext(i18n);
    </script>
    <slot />
  3. 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>

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().

src/routes/+layout.svelte
<script lang="ts">
import { setI18nContext } from '@comvi/svelte';
import { i18n } from '$lib/i18n';
setI18nContext(i18n);
</script>
<slot />

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>

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:

PropertyTypeDescription
tReadable<(key, params?) => string>Reactive translation function (use as $t(...))
tRawReadable<(key, params?) => TranslationResult>Structured output for advanced renderers (use as $tRaw(...))
localeReadable<string>Current language (read-only store, use setLocale() to change)
isLoadingReadable<boolean>Whether translations are being loaded
setLocale(lang: string) => Promise<void>Switch to a different 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} on:change={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Francais</option>
</select>

For translations containing HTML or child components, 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' } },
}}
/>

Load translations from a specific namespace:

<script lang="ts">
import { useI18n } from '@comvi/svelte';
const { t } = useI18n('dashboard');
</script>
<h1>{$t('page.title')}</h1>

Comvi works with Svelte 5 runes. The stores returned by useI18n() are compatible with the $ rune syntax:

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

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>

In Svelte 4, use $: reactive declarations for derived translations:

<script lang="ts">
import { useI18n } from '@comvi/svelte';
const { t } = useI18n();
let count = 0;
$: cartMessage = $t('cart.items', { count });
</script>
<p>{cartMessage}</p>
<button on:click={() => count++}>Add item</button>

Initialize translations on the server so they are included in the initial HTML response:

src/routes/+layout.server.ts
import { i18n } from '$lib/i18n';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ request }) => {
const acceptLanguage = request.headers.get('accept-language');
// Parse the Accept-Language header to find a supported language
const locale = acceptLanguage?.split(',')[0]?.split('-')[0] || 'en';
i18n.locale = locale;
await i18n.init();
return {
locale,
translations: i18n.getTranslations(locale),
};
};

Pass server-loaded translations to the client layout to avoid a duplicate fetch:

src/routes/+layout.svelte
<script lang="ts">
import { setI18nContext } from '@comvi/svelte';
import { i18n } from '$lib/i18n';
import type { LayoutData } from './$types';
export let data: LayoutData;
// Hydrate with server-loaded translations
i18n.addTranslations({ [data.locale]: data.translations });
i18n.locale = data.locale;
setI18nContext(i18n);
</script>
<html lang={data.locale}>
<body>
<slot />
</body>
</html>

Add language metadata for search engines:

src/routes/+layout.svelte
<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>

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.

src/lib/i18n.ts
import { createI18n } from '@comvi/core';
// Side-effect import — augments @comvi/core's TranslationKeys interface
import './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.