Lightweight
5.27 kB core, gzipped. Plus your framework binding — about 2 kB 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 locale: "en",5 messages: {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 locale: "en",5 messages: {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 locale: "en",5 messages: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011app.use(i18n);1213// in <script setup>14const { t } = useI18n();15t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n, setI18nContext } from "@comvi/svelte";23const i18n = createI18n({4 locale: "en",5 messages: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011setI18nContext(i18n); // in the root layout1213// in any component under it14const { t } = useI18n();15$t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/solid";23const i18n = createI18n({4 locale: "en",5 messages: {6 en: { hello: "Hello, {name}!" },7 es: { hello: "¡Hola, {name}!" },8 },9});1011// inside <I18nProvider i18n={i18n}>12const { t } = useI18n();13t("hello", { name: "World" }); // "Hello, World!"
1// src/i18n/config.ts2import { createNextI18n } from "@comvi/next";3import { routing } from "./routing";4import en from "./locales/en.json";5import es from "./locales/es.json";67export const { i18n } = createNextI18n({8 ...routing, // locales: ["en", "es"], defaultLocale: "en"9 messages: { en, es },10});
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";2import { icuCompiler } from "@comvi/core/icu";34const i18n = createI18n({5 locale: "en",6 compiler: icuCompiler, // plural, select, selectordinal7 messages: {8 en: {9 hello: "Hello, {name}!",10 items: "{count, plural, one {# item} other {# items}}",11 },12 es: {13 hello: "¡Hola, {name}!",14 items: "{count, plural, one {# elemento} other {# elementos}}",15 },16 },17});1819await i18n.init();
1import { createRoot } from "react-dom/client";2import { createI18n, icuCompiler, I18nProvider } from "@comvi/react";3import App from "./App";45const i18n = createI18n({6 locale: "en",7 compiler: icuCompiler, // plural, select, selectordinal8 messages: {9 en: {10 hello: "Hello, {name}!",11 items: "{count, plural, one {# item} other {# items}}",12 },13 es: {14 hello: "¡Hola, {name}!",15 items: "{count, plural, one {# elemento} other {# elementos}}",16 },17 },18});1920createRoot(document.getElementById("root")).render(21 <I18nProvider i18n={i18n}>22 <App />23 </I18nProvider>,24);
1import { createApp } from "vue";2import { createI18n } from "@comvi/vue";3import App from "./App.vue";45const i18n = createI18n({6 locale: "en",7 messages: {8 en: { hello: "Hello, {name}!" },9 es: { hello: "¡Hola, {name}!" },10 },11});1213createApp(App).use(i18n).mount("#app");
1<!-- src/routes/+layout.svelte -->2<script>3 import { createI18n, setI18nContext } from "@comvi/svelte";45 const { children } = $props();67 const i18n = createI18n({8 locale: "en",9 messages: {10 en: { hello: "Hello, {name}!" },11 es: { hello: "¡Hola, {name}!" },12 },13 });1415 setI18nContext(i18n);16</script>1718{@render children()}
1import { render } from "solid-js/web";2import { createI18n, I18nProvider } from "@comvi/solid";3import App from "./App";45const i18n = createI18n({6 locale: "en",7 messages: {8 en: { hello: "Hello, {name}!" },9 es: { hello: "¡Hola, {name}!" },10 },11});1213render(14 () => (15 <I18nProvider i18n={i18n}>16 <App />17 </I18nProvider>18 ),19 document.getElementById("root"),20);
1// src/i18n/routing.ts2import { defineRouting } from "@comvi/next/routing";34export const routing = defineRouting({5 locales: ["en", "es"],6 defaultLocale: "en",7});89// src/i18n/config.ts10import { createNextI18n } from "@comvi/next";11import { routing } from "./routing";1213export const { i18n } = createNextI18n({14 ...routing,15 messages: {16 en: { hello: "Hello, {name}!" },17 es: { hello: "¡Hola, {name}!" },18 },19});2021// next.config.ts, the [locale] layout and the middleware22// are in the quick start: comvi.io/docs/i18n/quick-start
1// nuxt.config.ts2export default defineNuxtConfig({3 modules: ["@comvi/nuxt"],4 comvi: {5 locales: ["en", "es"],6 defaultLocale: "en",7 messages: {8 en: { hello: "Hello, {name}!" },9 es: { hello: "¡Hola, {name}!" },10 },11 },12});
1i18n.t("hello", { name: "World" }); // "Hello, World!"2i18n.t("items", { count: 5 }); // "5 items"34await i18n.setLocale("es");5i18n.t("hello", { name: "World" }); // "¡Hola, World!"6i18n.t("items", { count: 5 }); // "5 elementos"
1import { useI18n } from "@comvi/react";23export default function App() {4 const { t, setLocale } = useI18n();56 return (7 <>8 <h1>{t("hello", { name: "World" })}</h1>9 <p>{t("items", { count: 5 })}</p>10 <button onClick={() => setLocale("es")}>Español</button>11 </>12 );13}
1<script setup>2import { useI18n } from "@comvi/vue";34const { t, setLocale } = useI18n();5</script>67<template>8 <h1>{{ t("hello", { name: "World" }) }}</h1>9 <button @click="setLocale('es')">Español</button>10</template>
1<script>2 import { useI18n } from "@comvi/svelte";34 const { t, setLocale } = useI18n();5</script>67<h1>{$t("hello", { name: "World" })}</h1>8<button onclick={() => setLocale("es")}>Español</button>
1import { useI18n } from "@comvi/solid";23export default function App() {4 const { t, setLocale } = useI18n();56 return (7 <>8 <h1>{t("hello", { name: "World" })}</h1>9 <button onClick={() => setLocale("es")}>Español</button>10 </>11 );12}
1// src/app/[locale]/page.tsx2import { getI18n } from "@comvi/next/server";34export default async function Page({ params }) {5 const { locale } = await params;6 const { t } = await getI18n({ locale });78 return <h1>{t("hello", { name: "World" })}</h1>;9}
1<!-- app/pages/index.vue -->2<script setup>3const { t } = useI18n();4</script>56<template>7 <h1>{{ t("hello", { name: "World" }) }}</h1>8 <NuxtLinkLocale to="/" locale="es">Español</NuxtLinkLocale>9</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.
5.27 kB core, gzipped. Plus your framework binding — about 2 kB 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 locale.
1import {2 formatNumber, formatDate, formatCurrency, formatRelativeTime,3} from "@comvi/core";45formatNumber(i18n, 1234.5)6// "1,234.5" (en) · "1.234,5" (de)78formatNumber(i18n, 0.75, { style: "percent" })9// "75%" (en) · "75 %" (de)1011formatDate(i18n, new Date(2025, 0, 15), {12 year: "numeric", month: "long", day: "numeric"13})14// "January 15, 2025" (en) · "15. Januar 2025" (de)1516formatCurrency(i18n, 99.99, "EUR")17// "€99.99" (en) · "99,99 €" (de)1819formatRelativeTime(i18n, -2, "hour")20// "2 hours ago" (en) · "vor 2 Stunden" (de)2122formatRelativeTime(i18n, 3, "day")23// "in 3 days" (en) · "in 3 Tagen" (de)
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({ translations: "./src/locales" })],6};
1$ npm install -D @comvi/cli2$ npx comvi typegen
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.
1// src/i18n/routing.ts2import { defineRouting } from "@comvi/next/routing";34export const routing = defineRouting({5 locales: ["en", "de"],6 defaultLocale: "en",7});89// src/i18n/config.ts10import { createNextI18n } from "@comvi/next";11import { routing } from "./routing";12import en from "./locales/en.json";13import de from "./locales/de.json";1415export const { i18n } = createNextI18n({16 ...routing,17 messages: { en, de },18});1920// next.config.ts21import { withComvi } from "@comvi/next/plugin";2223export default withComvi({ config: "./src/i18n/config.ts" })({});
1import { createMiddleware } from "@comvi/next/middleware";2import { routing } from "./i18n/routing";34export default createMiddleware(routing);56export const config = {7 matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"],8};
1import { notFound } from "next/navigation";2import { hasLocale } from "@comvi/next/routing";3import { ComviProvider } from "@comvi/next/server";4import { routing } from "@/i18n/routing";56export default async function LocaleLayout({ children, params }) {7 const { locale } = await params;8 if (!hasLocale(routing.locales, locale)) notFound();910 return (11 <html lang={locale}>12 <body>13 <ComviProvider locale={locale}>{children}</ComviProvider>14 </body>15 </html>16 );17}
1import { getI18n } from "@comvi/next/server";23export default async function HomePage({ params }) {4 const { locale } = await params;5 const { t } = await getI18n({ locale });67 return <h1>{t("welcome")}</h1>;8}
1export default defineNuxtConfig({2 modules: ["@comvi/nuxt"],3 comvi: {4 locales: ["en", "de", "uk"],5 defaultLocale: "en",6 messages: {7 en: { greeting: "Hello, {name}!" },8 de: { greeting: "Hallo, {name}!" },9 uk: { greeting: "Привіт, {name}!" },10 },11 },12});
1<template>2 <NuxtPage />3</template>
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-unsuseLocaleHead()useSwitchLocalePath()cdnLoader plugin (with revalidation tags on Next.js)Comvi i18n has a small core and an extensible plugin architecture. Two plugins ship today; both are MIT.
/api entry reads the authenticated API on the server when the instance has an apiKey.1import { cdnLoader } from "@comvi/plugin-fetch-loader";23const i18n = createI18n({ locale: "en" })4 .use(cdnLoader({5 cdnUrl: "https://cdn.comvi.io/<project-token>",6 timeout: 5000,7 fallback: {8 en: () => import("./locales/en.json"),9 },10 }));
1import { localeDetector } from "@comvi/plugin-locale-detector";23const i18n = createI18n({4 locale: "en", // fallback if detection fails5}).use(6 localeDetector({7 supportedLocales: ["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.