Language Switching
Comvi supports runtime language switching with automatic reactivity. When you change the language, every translated string in your app updates immediately — no page reload needed.
Basic Switching
Section titled “Basic Switching”Two ways to change the language:
import { i18n } from './i18n';
// Fire-and-forget: loads translations asynchronouslyi18n.locale = 'fr';
// Awaitable: same as above, but returns a Promise you can awaitawait i18n.setLocaleAsync('fr');Both approaches:
- Load translations for the new locale (and any active namespaces)
- Switch the language only after translations are ready
- Emit
'localeChanged'event - Update all components reactively
The key difference: setLocaleAsync() returns a Promise, so you can await it to run code after the switch completes. locale = 'fr' is fire-and-forget — the switch happens asynchronously in the background.
Use setLocaleAsync() when you need to guarantee translations are loaded before proceeding (e.g., fetching data that depends on the language).
Building a Language Selector
Section titled “Building a Language Selector”<script setup lang="ts">import { useI18n } from '@comvi/vue';
const languages = ['en', 'de', 'fr', 'ja'];const { locale, setLocale, isLoading } = useI18n();
// locale is a writable ref — you can also assign directly:// locale.value = 'de';</script>
<template> <select :value="locale" @change="setLocale(($event.target as HTMLSelectElement).value)" :disabled="isLoading"> <option v-for="lang in languages" :key="lang" :value="lang">{{ lang }}</option> </select></template>import { useI18n } from '@comvi/react';
const languages = ['en', 'de', 'fr', 'ja'];
function LanguageSwitcher() { const { locale, setLocale, isLoading } = useI18n(); return ( <select value={locale} onChange={(e) => setLocale(e.currentTarget.value)} disabled={isLoading}> {languages.map((lang) => ( <option key={lang} value={lang}>{lang}</option> ))} </select> );}'use client';import { useI18n } from '@comvi/next/client';import { useLocalizedRouter, usePathname } from '@comvi/next/navigation';
const languages = ['en', 'de', 'fr', 'ja'];
function LanguageSwitcher() { const { locale } = useI18n(); const router = useLocalizedRouter(); const pathname = usePathname();
return ( <select value={locale} onChange={(e) => router.push(pathname || '/', e.currentTarget.value)}> {languages.map((lang) => ( <option key={lang} value={lang}>{lang}</option> ))} </select> );}<script setup lang="ts">const languages = ['en', 'de', 'fr', 'ja'];const { locale, setLocale, isLoading } = useI18n();</script>
<template> <select :value="locale" @change="setLocale(($event.target as HTMLSelectElement).value)" :disabled="isLoading"> <option v-for="lang in languages" :key="lang" :value="lang">{{ lang }}</option> </select></template>import { useI18n } from '@comvi/solid';
const languages = ['en', 'de', 'fr', 'ja'];
function LanguageSwitcher() { const { locale, setLocale, isLoading } = useI18n(); return ( <select value={locale()} onChange={(e) => setLocale(e.currentTarget.value)} disabled={isLoading()}> {languages.map((lang) => ( <option value={lang}>{lang}</option> ))} </select> );}<script lang="ts">import { useI18n } from '@comvi/svelte';
const languages = ['en', 'de', 'fr', 'ja'];const { locale, setLocale, isLoading } = useI18n();
function handleChange(event: Event) { const target = event.currentTarget as HTMLSelectElement; setLocale(target.value);}</script>
<select value={$locale} on:change={handleChange} disabled={$isLoading}> {#each languages as lang} <option value={lang}>{lang}</option> {/each}</select>import { i18n } from './i18n';
const languages = ['en', 'de', 'fr', 'ja'];
const select = document.querySelector<HTMLSelectElement>('#language-select')!;languages.forEach((lang) => { const option = document.createElement('option'); option.value = lang; option.textContent = lang; select.appendChild(option);});select.value = i18n.locale;select.addEventListener('change', () => { i18n.locale = select.value; });Showing a Loading State
Section titled “Showing a Loading State”When switching to a language that hasn’t been loaded yet, isLoading becomes true. Use this to show feedback:
<script setup lang="ts">import { useI18n } from '@comvi/vue';
const { t, isLoading } = useI18n();</script>
<template> <div v-if="isLoading" class="loading-overlay"> Loading translations... </div> <main v-else> <h1>{{ t('page.title') }}</h1> </main></template>import { useI18n } from '@comvi/react';
function App() { const { t, isLoading } = useI18n();
if (isLoading) { return <div className="loading-overlay">Loading translations...</div>; }
return ( <main> <h1>{t('page.title')}</h1> </main> );}'use client';import { useI18n } from '@comvi/next/client';
function Page() { const { t, isLoading } = useI18n();
if (isLoading) { return <div className="loading-overlay">Loading translations...</div>; }
return ( <main> <h1>{t('page.title')}</h1> </main> );}<script setup lang="ts">const { t, isLoading } = useI18n();</script>
<template> <div v-if="isLoading" class="loading-overlay"> Loading translations... </div> <main v-else> <h1>{{ t('page.title') }}</h1> </main></template>import { useI18n } from '@comvi/solid';import { Show } from 'solid-js';
function App() { const { t, isLoading } = useI18n();
return ( <Show when={!isLoading()} fallback={<div class="loading-overlay">Loading translations...</div>}> <main> <h1>{t('page.title')}</h1> </main> </Show> );}<script lang="ts">import { useI18n } from '@comvi/svelte';const { t, isLoading } = useI18n();</script>
{#if $isLoading} <div class="loading-overlay">Loading translations...</div>{:else} <main> <h1>{$t('page.title')}</h1> </main>{/if}import { i18n } from './i18n';
const overlay = document.querySelector<HTMLElement>('.loading-overlay')!;const main = document.querySelector<HTMLElement>('main')!;
i18n.on('loadingStateChanged', ({ isLoading }) => { overlay.style.display = isLoading ? 'block' : 'none'; main.style.display = isLoading ? 'none' : 'block';});Fallback Locale Chain
Section titled “Fallback Locale Chain”Set a fallback chain so missing translations in one language fall back to another:
const i18n = createI18n({ locale: 'de', fallbackLocale: 'en', // If key missing in German, try English});
// Or chain multiplei18n.setFallbackLocale(['de-AT', 'de', 'en']);// Tries Austrian German → German → EnglishThis is checked during key lookup, so if a key doesn’t exist in the current locale, the library walks the fallback chain before emitting 'missingKey'.
Persisting Language Choice
Section titled “Persisting Language Choice”By default, language choice is not persisted. When the user refreshes the page, the app falls back to the configured default language. To remember the user’s choice, use the Language Detector plugin with caching enabled:
import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';import { LocaleDetector } from '@comvi/plugin-locale-detector';
const i18n = createI18n({ locale: 'en', fallbackLocale: 'en',}) .use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', })) .use(LocaleDetector({ order: ['localStorage', 'navigator'], caches: ['localStorage'], }));With this setup:
- On first visit, the plugin detects the browser language via
navigator.languages - When the user switches languages, the choice is saved to
localStorage - On subsequent visits, the saved preference is used instead of the browser language
See Language Detection for all detection sources and cache options.
URL-Based Language Routing
Section titled “URL-Based Language Routing”For SEO and shareable links, you may want the language reflected in the URL. This approach varies by framework.
Vue does not have built-in locale routing. Use the programmatic setLocale() approach from Building a Language Selector above, combined with the Language Detector plugin to persist the choice.
React does not have built-in locale routing. Use the programmatic setLocale() approach from Building a Language Selector above, combined with the Language Detector plugin to persist the choice.
Next.js uses middleware to detect the locale from the URL path and redirect:
import { createMiddleware } from '@comvi/next/middleware';import { routing } from './i18n';
export default createMiddleware(routing);
export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'],};Routes are automatically prefixed with the locale:
/en/about → English/de/about → German/about → Redirects to detected localeUse the locale-aware Link component for navigation:
import { Link } from '@comvi/next/navigation';
<Link href="/about">About</Link>// Renders as /en/about or /de/about based on current localeSee the Next.js guide for the full setup.
Nuxt uses the @comvi/nuxt module with a locale routing strategy:
export default defineNuxtConfig({ modules: ['@comvi/nuxt'], comvi: { cdnUrl: 'https://cdn.comvi.io/your-distribution-id', defaultLocale: 'en', locales: ['en', 'de', 'fr'], localePrefix: 'as-needed', },});Locale prefix options:
localePrefix | URL pattern | Description |
|---|---|---|
'always' | /en/about, /de/about | All locales have a prefix |
'as-needed' | /about, /de/about | Default locale has no prefix |
'never' | /about | No locale prefixes in URLs |
See the Nuxt guide for the full setup.
SolidJS does not have built-in locale routing. Use the programmatic setLocale() approach from Building a Language Selector above, combined with the Language Detector plugin to persist the choice.
Svelte does not have built-in locale routing. Use the programmatic setLocale() approach from Building a Language Selector above, combined with the Language Detector plugin to persist the choice.
Vanilla JS does not have built-in locale routing. Use the programmatic approach from Building a Language Selector above, combined with the Language Detector plugin to persist the choice.
Reacting to Language Changes
Section titled “Reacting to Language Changes”Listen for language changes with the event system to run side effects — update the document title, refetch data, or track analytics:
import { i18n } from './i18n';
// Subscribe to language changes — on() returns an unsubscribe functionconst unsubscribe = i18n.on('localeChanged', ({ from, to }) => { // Update the HTML lang and direction (i18n.dir handles script // subtags and CLDR-defined RTL locales — see RTL Language Support) document.documentElement.lang = to; document.documentElement.dir = i18n.dir;
// Track in analytics analytics.track('language_changed', { from, to });});
// Unsubscribe when no longer neededunsubscribe();Caching and Refresh
Section titled “Caching and Refresh”Comvi caches each loaded (locale, namespace) pair. Once de:default has loaded, switching back to German does not fetch that namespace again unless the cache is cleared or you explicitly reload it.
If you need to force a refresh from the server (after a content update, or in development when translation files change), use reloadTranslations(). It deletes the cached translations and reloads them via the configured loader:
// Refresh all active namespaces for Germanawait i18n.reloadTranslations('de');
// Refresh only one namespaceawait i18n.reloadTranslations('de', 'dashboard');
// Refresh the current locale + fallback chainawait i18n.reloadTranslations();reloadTranslations() invalidates the targeted cache entries before fetching, so calling it twice triggers two loader calls for the same entries. Use it for invalidation, not as a generic preload.
RTL Language Support
Section titled “RTL Language Support”When switching to a right-to-left language like Arabic or Hebrew, update the document direction. Use i18n.dir instead of a hardcoded list — it covers script subtags (ku-Arab, uz-Arab), region tags (he-IL), and every CLDR-defined RTL locale, which a manual array would miss:
i18n.on('localeChanged', ({ to }) => { document.documentElement.dir = i18n.dir; document.documentElement.lang = to;});In framework integrations, dir is exposed reactively from useI18n(), so you can bind it directly without wiring up the event yourself:
<script setup lang="ts">import { useI18n } from '@comvi/vue';import { watchEffect } from 'vue';
const { dir, locale } = useI18n();
watchEffect(() => { document.documentElement.dir = dir.value; document.documentElement.lang = locale.value;});</script>import { useEffect } from 'react';import { useI18n } from '@comvi/react';
function HtmlAttrsSync() { const { dir, locale } = useI18n();
useEffect(() => { document.documentElement.dir = dir; document.documentElement.lang = locale; }, [dir, locale]);
return null;}<script lang="ts">import { useI18n } from '@comvi/svelte';const { dir, locale } = useI18n();
$: if (typeof document !== 'undefined') { document.documentElement.dir = $dir; document.documentElement.lang = $locale;}</script>import { createEffect } from 'solid-js';import { useI18n } from '@comvi/solid';
function HtmlAttrsSync() { const { dir, locale } = useI18n();
createEffect(() => { document.documentElement.dir = dir(); document.documentElement.lang = locale(); });
return null;}With Tailwind CSS, use the rtl: variant for directional styles:
<div class="ml-4 rtl:mr-4 rtl:ml-0 text-left rtl:text-right"> <!-- Adapts padding and text alignment for RTL --></div>