Lightweight
7.11 kB core, gzipped. Plus your framework binding — 2 kB or less
for any of them. Zero runtime dependencies, sub-microsecond lookups for
the t() call you'll run thousands of times
per page.
≤10 kB with bindings. Zero dependencies. CSP-safe.
Built on the Intl APIs your runtime
already ships.
Pairs with Comvi when you outgrow
JSON files.
1import { createI18n } from "@comvi/core";23const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();1213i18n.t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/react";23const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();1213i18n.t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/vue";23const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();1213// in <script setup>14const { t } = useI18n();15t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/svelte";23export const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();1213// in any .svelte file14$t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/solid";23const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();1213const { t } = useI18n();14t("hello", { name: "World" }); // "Hello, World!"
1import { createNextI18n } from "@comvi/next";2import en from "./locales/en.json";3import es from "./locales/es.json";45const nextI18n = createNextI18n({6 locales: ["en", "es"],7 defaultLocale: "en",8 translation: { en, es },9});1011export const { i18n, routing } = nextI18n;
1// nuxt.config.ts2export default defineNuxtConfig({3 modules: ["@comvi/nuxt"],4 comvi: {5 locales: ["en", "es"],6 defaultLocale: "en",7 },8});
npm install.
Same shape across every binding — pick a framework, ship in five minutes. Your selection here syncs with the hero and other code blocks on the page.
1$ npm install @comvi/core
1$ npm install @comvi/react
1$ npm install @comvi/vue
1$ npm install @comvi/svelte
1$ npm install @comvi/solid
1$ npm install @comvi/next
1$ npm install @comvi/nuxt
1import { createI18n } from "@comvi/core";23const i18n = createI18n({4 language: "en",5 translation: {6 en: {7 hello: "Hello, {name}!",8 items: "{count, plural, one {# item} other {# items}}",9 },10 es: {11 hello: "¡Hola, {name}!",12 items: "{count, plural, one {# elemento} other {# elementos}}",13 },14 },15});1617await i18n.init();
1import { createI18n } from "@comvi/react";23const i18n = createI18n({4 language: "en",5 translation: {6 en: {7 hello: "Hello, {name}!",8 items: "{count, plural, one {# item} other {# items}}",9 },10 es: {11 hello: "¡Hola, {name}!",12 items: "{count, plural, one {# elemento} other {# elementos}}",13 },14 },15});1617await i18n.init();
1import { createI18n } from "@comvi/vue";23const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();12app.use(i18n);
1import { createI18n } from "@comvi/svelte";23export const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();
1import { createI18n } from "@comvi/solid";23const i18n = createI18n({4 language: "en",5 translation: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011await i18n.init();
1// i18n.ts2import { createNextI18n } from "@comvi/next";3import en from "./locales/en.json";4import es from "./locales/es.json";56const nextI18n = createNextI18n({7 locales: ["en", "es"],8 defaultLocale: "en",9 translation: { en, es },10});1112export const { i18n, routing } = nextI18n;
1// nuxt.config.ts2export default defineNuxtConfig({3 modules: ["@comvi/nuxt"],4 comvi: {5 locales: ["en", "es"],6 defaultLocale: "en",7 },8});
1i18n.t("hello", { name: "World" }); // "Hello, World!"2i18n.t("items", { count: 5 }); // "5 items"34i18n.language = "es";5i18n.t("hello", { name: "World" }); // "¡Hola, World!"6i18n.t("items", { count: 5 }); // "5 elementos"
1i18n.t("hello", { name: "World" }); // "Hello, World!"2i18n.t("items", { count: 5 }); // "5 items"34i18n.language = "es";5i18n.t("hello", { name: "World" }); // "¡Hola, World!"6i18n.t("items", { count: 5 }); // "5 elementos"
1<script setup>2const { t } = useI18n();3</script>45<template>6 <h1>{{ t("hello", { name: "World" }) }}</h1>7</template>
1<script>2 import { t } from "./i18n";3</script>45<h1>{$t("hello", { name: "World" })}</h1>
1import { useI18n } from "@comvi/solid";23function Hello() {4 const { t } = useI18n();5 return <h1>{t("hello", { name: "World" })}</h1>;6}
1// app/[locale]/page.tsx2import { getI18n } from "@comvi/next/server";34export default async function Page() {5 const { t } = await getI18n();6 return <h1>{t("hello", { name: "World" })}</h1>;7}
1<!-- pages/index.vue -->2<script setup>3const { t } = useI18n();4</script>56<template>7 <h1>{{ t("hello", { name: "World" }) }}</h1>8</template>
Six things that aren't standard in this category. Two of them — CSP-safe runtime and full ICU MessageFormat — are differentiators on their own.
7.11 kB core, gzipped. Plus your framework binding — 2 kB or less
for any of them. Zero runtime dependencies, sub-microsecond lookups for
the t() call you'll run thousands of times
per page.
No eval, no new Function. Strict Content-Security-Policy works out of the box — no 'unsafe-eval' required.
Number, date, currency, relative time formatting via the native Intl APIs your runtime already ships. No 200 kB polyfills.
Plurals, select, selectordinal, interpolation — full ICU, not a subset. Polish gets four plural forms, not one.
<link>here</link> in your
translations becomes a real component. You control the rendering. No dangerouslySetInnerHTML, no XSS.
Server-side translation loading for Next.js and Nuxt. Locale routing, SEO link tags, and cookie persistence in the framework module — not a separate library to glue in.
Pluralization that handles Polish. Select for grammatical variation. Ordinals that know German from Japanese. All in one expression, all in your translation file.
1// en2"messages": "You have {count, plural,3 one {1 message}4 other {# messages}5}"67// pl ── four forms8"messages": "Masz {count, plural,9 one {# wiadomość}10 few {# wiadomości}11 many {# wiadomości}12 other {# wiadomości}13}"1415t("messages", { count: 1 }) // "You have 1 message" | "Masz 1 wiadomość"16t("messages", { count: 5 }) // "You have 5 messages" | "Masz 5 wiadomości"
1// de ── formal / informal2"your_name": "{formality, select,3 formal {Ihr Name}4 informal {Dein Name}5 other {Ihr Name}6}"78t("your_name", { formality: "formal" }) // "Ihr Name"9t("your_name", { formality: "informal" }) // "Dein Name"
1// en ── selectordinal2"rank": "You are {place, selectordinal,3 one {#st}4 two {#nd}5 few {#rd}6 other {#th}7} place"89t("rank", { place: 1 }) // "You are 1st place"10t("rank", { place: 3 }) // "You are 3rd place"11t("rank", { place: 22 }) // "You are 22nd place"
Built on the native Intl APIs your runtime already ships. No extra packages. No polyfills. Auto-follows the active language.
1i18n.formatNumber(1234.5)2// "1,234.5" (en) · "1.234,5" (de)34i18n.formatNumber(0.75, { style: "percent" })5// "75%"67i18n.formatDate(new Date(), {8 year: "numeric", month: "long", day: "numeric"9})10// "January 15, 2025" (en) · "15. Januar 2025" (de)1112i18n.formatCurrency(99.99, "USD")13// "$99.99" (en) · "99,99 $" (fr)1415i18n.formatRelativeTime(-2, "hour")16// "2 hours ago" (en) · "vor 2 Stunden" (de)1718i18n.formatRelativeTime(3, "day")19// "in 3 days"
Get autocomplete on every translation key. Wrong parameters fail to compile. Missing tag handlers fail to compile. Three ways to generate types — pick the one that fits your workflow.
1// vite.config.ts2import { comviTypes } from "@comvi/vite-plugin";34export default {5 plugins: [comviTypes()],6};
1$ npm install -D @comvi/cli2$ comvi generate-types
1import type { InferKeys } from "@comvi/core";2import en from "./locales/en.json";34declare module "@comvi/core" {5 interface TranslationKeys6 extends InferKeys<typeof en> {}7}
Comvi i18n for Next.js and Nuxt isn't just SSR-ready — it's a full integration. Server-side translation loading, locale-prefixed routes, SEO link tags, cookie persistence. All in the framework module.
1import { createNextI18n } from "@comvi/next";2import en from "./locales/en.json";3import de from "./locales/de.json";45const nextI18n = createNextI18n({6 locales: ["en", "de"],7 defaultLocale: "en",8 translation: { en, de },9});1011export const { i18n, routing } = nextI18n;
1import { createMiddleware } from "@comvi/next/middleware";2import { routing } from "./i18n";34export default createMiddleware(routing);
1import { setRequestLocale, loadTranslations } from "@comvi/next/server";2import { I18nProvider } from "@comvi/next/client";3import { i18n, routing } from "@/i18n";45export default async function LocaleLayout({ children, params }) {6 const { locale } = await params;7 setRequestLocale(locale);89 const messages = await loadTranslations(locale);1011 return (12 <I18nProvider i18n={i18n} locale={locale}13 messages={messages} routing={routing}>14 {children}15 </I18nProvider>16 );17}
1import { getI18n, setRequestLocale } from "@comvi/next/server";23export default async function HomePage({ params }) {4 const { locale } = await params;5 setRequestLocale(locale);67 const { t } = await getI18n();89 return <h1>{t("welcome")}</h1>;10}
1export default defineNuxtConfig({2 modules: ["@comvi/nuxt"],3 comvi: {4 locales: ["en", "de", "uk"],5 defaultLocale: "en",6 },7});
1import { defineComviSetup } from "@comvi/nuxt/setup";23export default defineComviSetup(({ i18n }) => {4 i18n.registerLoader({5 en: () => import("./locales/en.json"),6 de: () => import("./locales/de.json"),7 });8});
1<script setup>2const { t } = useI18n();3const switchLocalePath = useSwitchLocalePath();4</script>56<template>7 <h1>{{ t("greeting", { name: "Alice" }) }}</h1>8 <NuxtLink :to="switchLocalePath('de')">Deutsch</NuxtLink>9</template>
/en/about, /de/ueber-unsuseSwitchLocalePath()Comvi i18n has a small core and an extensible plugin architecture. Two plugins ship today; both are MIT.
apiKey.1import { FetchLoader } from "@comvi/plugin-fetch-loader";23const i18n = createI18n({ language: "en" })4 .use(FetchLoader({5 cdnUrl: "https://cdn.example.com/locales",6 timeout: 5000,7 fallback: {8 en: () => import("./locales/en.json"),9 },10 }));
1import { LanguageDetector } from "@comvi/plugin-language-detector";23const i18n = createI18n({4 language: "en", // fallback if detection fails5}).use(6 LanguageDetector({7 supportedLanguages: ["en", "de", "fr", "pt-BR", "pt-PT"],8 }),9);
Pick the binding for your stack, add plugins as needed. Every package links to its README on GitHub — verify the source before installing.
@comvi/core with JSON files forever. When your team gets big enough that JSON files in a git repo stop working — that's what Comvi platform is for. The library API doesn't change. You just plug in the loader and the editor.