Skip to content

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.

Two ways to change the language:

import { i18n } from './i18n';
// Fire-and-forget: loads translations asynchronously
i18n.locale = 'fr';
// Awaitable: same as above, but returns a Promise you can await
await i18n.setLocaleAsync('fr');

Both approaches:

  1. Load translations for the new locale (and any active namespaces)
  2. Switch the language only after translations are ready
  3. Emit 'localeChanged' event
  4. 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).

src/components/LanguageSwitcher.vue
<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>

When switching to a language that hasn’t been loaded yet, isLoading becomes true. Use this to show feedback:

src/App.vue
<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>

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 multiple
i18n.setFallbackLocale(['de-AT', 'de', 'en']);
// Tries Austrian German → German → English

This 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'.

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:

src/i18n.ts
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:

  1. On first visit, the plugin detects the browser language via navigator.languages
  2. When the user switches languages, the choice is saved to localStorage
  3. On subsequent visits, the saved preference is used instead of the browser language

See Language Detection for all detection sources and cache options.

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.

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 function
const 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 needed
unsubscribe();

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 German
await i18n.reloadTranslations('de');
// Refresh only one namespace
await i18n.reloadTranslations('de', 'dashboard');
// Refresh the current locale + fallback chain
await 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.

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:

src/i18n.ts
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>

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>