Skip to content

Vue 3

Comvi i18n integrates seamlessly with Vue 3 via the @comvi/vue plugin. This guide covers installation, setup, and common patterns for Vue applications.

Terminal window
npm install @comvi/vue
  1. Create an i18n instance in a separate module:

    src/i18n.ts
    import { createI18n } from "@comvi/vue";
    export const i18n = createI18n({
    locale: "en",
    fallbackLocale: "en",
    });
    // Register translations (from API, local files, or both)
    i18n.registerLoader({
    en: () => import("./locales/en.json"),
    de: () => import("./locales/de.json"),
    "en:admin": () => import("./locales/admin/en.json"),
    });
  2. Install the plugin in your Vue app:

    src/main.ts
    import { createApp } from "vue";
    import App from "./App.vue";
    import { i18n } from "./i18n";
    const app = createApp(App);
    app.use(i18n);
    app.mount("#app");

    The .use(i18n) call auto-initializes the library if it has not already been initialized.

  3. Use the composable in components:

    src/App.vue
    <script setup>
    import { useI18n } from "@comvi/vue";
    const { t, locale, setLocale } = useI18n();
    </script>
    <template>
    <div>
    <h1>{{ t("home.title") }}</h1>
    <p>{{ locale }}</p>
    </div>
    </template>

useI18n() returns a reactive object with translation methods and state. Commonly used values:

// Partial — see the API reference for the complete shape
interface UseI18nReturn {
t: TypedTranslationFunction; // returns string
tRaw: RawTranslationFunction; // returns TranslationResult
locale: Ref<string>;
setLocale(locale: string): Promise<void>;
isLoading: Readonly<Ref<boolean>>;
isInitializing: Readonly<Ref<boolean>>;
dir: ComputedRef<"ltr" | "rtl">;
onMissingKey(
cb: (key: string, locale: string, namespace: string) => TranslationResult | void,
): () => void;
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
formatDate(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
formatCurrency(value: number, currency: string, options?: Intl.NumberFormatOptions): string;
}

See the Vue API reference for the full return type.

Access translations by passing a namespace:

<script setup>
import { useI18n } from "@comvi/vue";
const { t: tAdmin } = useI18n("admin");
const { t: tDefault } = useI18n(); // uses default namespace
</script>
<template>
<p>{{ tDefault("common.welcome") }}</p>
<p>{{ tAdmin("dashboard.title") }}</p>
</template>

The t() function always returns a plain string. Use <T> for rich text rendering, or tRaw() if you need the structured TranslationResult directly:

<script setup>
import { useI18n } from "@comvi/vue";
const { t, tRaw } = useI18n();
const plain = t("rich.message"); // string
const structured = tRaw("rich.message"); // string | Array<string | VirtualNode>
</script>
<template>
<!-- Simple key -->
<h1>{{ t("home.title") }}</h1>
<!-- With parameters -->
<p>{{ t("home.greeting", { name: "Alice" }) }}</p>
<!-- Fallback if key missing -->
<p>{{ t("admin.section", { fallback: "Not found" }) }}</p>
<!-- Specific locale (override current) -->
<p>{{ t("home.title", { locale: "de" }) }}</p>
<!-- Specific namespace -->
<p>{{ t("dashboard.title", { ns: "admin" }) }}</p>
</template>

Reactivity: Calling t() inside a Vue template, render function, or computed value automatically tracks the current locale and translation cache, so the UI re-renders when either changes.

The <T> component renders rich text (tags, components) from translations:

<!-- With slot for custom component -->
<T i18n-key="rich_text.link_text">
<template #link="{ children }">
<a href="https://example.com" class="text-blue-600">
{{ children }}
</a>
</template>
</T>
<!-- With parameters -->
<T
i18n-key="welcome.message"
:params="{ name: 'Alice', count: 5 }"
ns="admin"
/>
interface TProps {
i18nKey: string; // Translation key (required)
params?: Record<string, unknown>; // Parameters for interpolation
ns?: string; // Namespace override
locale?: string; // Locale override
fallback?: string; // Fallback if key missing
components?: {
[tag: string]: string | Component | {
component: Component;
props?: object;
};
};
}

The components prop takes precedence over slots when both define the same tag. Slots are convenient for inline Vue template syntax. The components prop is useful when you want to reuse the same handlers across many <T> calls or configure handlers at runtime. String handlers, such as bold: "strong", render as plain HTML tags without instantiating a Vue component.

<!-- Using slots for simple text children -->
<T i18n-key="legal.tos">
<template #tos="{ children }">
<a href="/terms">{{ children }}</a>
</template>
</T>
<!-- Using components prop for reusable or runtime-configured handlers -->
<T
i18n-key="legal.tos"
:components="{
tos: {
component: 'a',
props: { href: '/terms', class: 'text-blue-600' }
}
}"
/>

Change the locale reactively. setLocale() is asynchronous and resolves after translations are loaded:

<script setup>
import { useI18n } from "@comvi/vue";
const { locale, setLocale } = useI18n();
const switchLocale = async (newLocale) => {
await setLocale(newLocale);
// Translations are now loaded
};
// or fire-and-forget (not recommended for UX)
const quickSwitch = (newLocale) => {
locale.value = newLocale;
};
</script>
<template>
<div>
<p>Current: {{ locale }}</p>
<button @click="switchLocale('de')">Deutsch</button>
<button @click="switchLocale('en')">English</button>
</div>
</template>

Vue’s Composition API makes translations reactive:

<script setup>
import { useI18n } from "@comvi/vue";
import { watch, computed, ref } from "vue";
const { locale, t } = useI18n();
// Watch locale changes
watch(locale, (newLocale) => {
document.documentElement.lang = newLocale;
document.documentElement.dir = newLocale === "ar" ? "rtl" : "ltr";
});
// Computed keys based on state
const count = ref(5);
const itemsText = computed(() => {
return t("items.count", { count: count.value });
});
</script>
<template>
<p>{{ itemsText }}</p>
</template>

Comvi Vue integration supports SSR hydration. For most Vue SSR apps, prefer the Nuxt integration because it handles per-request i18n instances, hydration locale, and routing conventions for you.

See the Nuxt guide and the SSR guide for server-rendered applications.

When the plugin is installed, two global properties are available in all components:

  • $t(key, params?) — translate a key to a string
  • $tRaw(key, params?) — translate a key to a raw TranslationResult
  • $i18n — the i18n instance
<template>
<h1>{{ $t("hello.world") }}</h1>
<p>Language: {{ $i18n.locale }}</p>
</template>
<script setup>
import { useI18n } from "@comvi/vue";
const { t, isLoading, isInitializing } = useI18n();
</script>
<template>
<div v-if="isInitializing" class="spinner">Loading translations...</div>
<div v-else-if="isLoading" class="spinner">{{ t("common.loading") }}</div>
<div v-else>Content loaded</div>
</template>
const { onMissingKey } = useI18n();
onMissingKey((key, locale, namespace) => {
console.warn(`Missing translation: ${namespace}:${key} (${locale})`);
return `[${key}]`; // Return fallback
});
import { createI18n } from "@comvi/vue";
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.example.com/translations/",
})
)
.use(
LocaleDetector({
supportedLocales: ["en", "de", "fr"],
order: ["localStorage", "navigator"],
caches: ["localStorage"],
})
);
i18n.registerLoader({
en: () => import("./locales/en.json"),
de: () => import("./locales/de.json"),
});