// the i18n library

A modern i18n library for JavaScript.

≤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.

$npm install @comvi/react
GitHub ↗
// pick your stack
// hero.reactreact
@comvi/react
1import { createI18n } from "@comvi/core";
2 
3const i18n = createI18n({
4 locale: "en",
5 messages: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
12 
13i18n.t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/react";
2 
3const i18n = createI18n({
4 locale: "en",
5 messages: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
12 
13i18n.t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/vue";
2 
3const i18n = createI18n({
4 locale: "en",
5 messages: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11app.use(i18n);
12 
13// in <script setup>
14const { t } = useI18n();
15t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n, setI18nContext } from "@comvi/svelte";
2 
3const i18n = createI18n({
4 locale: "en",
5 messages: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11setI18nContext(i18n); // in the root layout
12 
13// in any component under it
14const { t } = useI18n();
15$t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/solid";
2 
3const i18n = createI18n({
4 locale: "en",
5 messages: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11// inside <I18nProvider i18n={i18n}>
12const { t } = useI18n();
13t("hello", { name: "World" }); // "Hello, World!"
1// src/i18n/config.ts
2import { createNextI18n } from "@comvi/next";
3import { routing } from "./routing";
4import en from "./locales/en.json";
5import es from "./locales/es.json";
6 
7export const { i18n } = createNextI18n({
8 ...routing, // locales: ["en", "es"], defaultLocale: "en"
9 messages: { en, es },
10});
1// nuxt.config.ts
2export default defineNuxtConfig({
3 modules: ["@comvi/nuxt"],
4 comvi: {
5 locales: ["en", "es"],
6 defaultLocale: "en",
7 },
8});
≤10 kB gzipped with bindings·zero dependencies·sub-microsecond lookups·MIT

Three steps. Maybe four if you count 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.

── 01 install ──
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
── 02 setup ──
1import { createI18n } from "@comvi/core";
2import { icuCompiler } from "@comvi/core/icu";
3 
4const i18n = createI18n({
5 locale: "en",
6 compiler: icuCompiler, // plural, select, selectordinal
7 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});
18 
19await i18n.init();
1import { createRoot } from "react-dom/client";
2import { createI18n, icuCompiler, I18nProvider } from "@comvi/react";
3import App from "./App";
4 
5const i18n = createI18n({
6 locale: "en",
7 compiler: icuCompiler, // plural, select, selectordinal
8 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});
19 
20createRoot(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";
4 
5const i18n = createI18n({
6 locale: "en",
7 messages: {
8 en: { hello: "Hello, {name}!" },
9 es: { hello: "¡Hola, {name}!" },
10 },
11});
12 
13createApp(App).use(i18n).mount("#app");
1<!-- src/routes/+layout.svelte -->
2<script>
3 import { createI18n, setI18nContext } from "@comvi/svelte";
4 
5 const { children } = $props();
6 
7 const i18n = createI18n({
8 locale: "en",
9 messages: {
10 en: { hello: "Hello, {name}!" },
11 es: { hello: "¡Hola, {name}!" },
12 },
13 });
14 
15 setI18nContext(i18n);
16</script>
17 
18{@render children()}
1import { render } from "solid-js/web";
2import { createI18n, I18nProvider } from "@comvi/solid";
3import App from "./App";
4 
5const i18n = createI18n({
6 locale: "en",
7 messages: {
8 en: { hello: "Hello, {name}!" },
9 es: { hello: "¡Hola, {name}!" },
10 },
11});
12 
13render(
14 () => (
15 <I18nProvider i18n={i18n}>
16 <App />
17 </I18nProvider>
18 ),
19 document.getElementById("root"),
20);
1// src/i18n/routing.ts
2import { defineRouting } from "@comvi/next/routing";
3 
4export const routing = defineRouting({
5 locales: ["en", "es"],
6 defaultLocale: "en",
7});
8 
9// src/i18n/config.ts
10import { createNextI18n } from "@comvi/next";
11import { routing } from "./routing";
12 
13export const { i18n } = createNextI18n({
14 ...routing,
15 messages: {
16 en: { hello: "Hello, {name}!" },
17 es: { hello: "¡Hola, {name}!" },
18 },
19});
20 
21// next.config.ts, the [locale] layout and the middleware
22// are in the quick start: comvi.io/docs/i18n/quick-start
1// nuxt.config.ts
2export 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});
── 03 use ──
1i18n.t("hello", { name: "World" }); // "Hello, World!"
2i18n.t("items", { count: 5 }); // "5 items"
3 
4await i18n.setLocale("es");
5i18n.t("hello", { name: "World" }); // "¡Hola, World!"
6i18n.t("items", { count: 5 }); // "5 elementos"
1import { useI18n } from "@comvi/react";
2 
3export default function App() {
4 const { t, setLocale } = useI18n();
5 
6 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";
3 
4const { t, setLocale } = useI18n();
5</script>
6 
7<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";
3 
4 const { t, setLocale } = useI18n();
5</script>
6 
7<h1>{$t("hello", { name: "World" })}</h1>
8<button onclick={() => setLocale("es")}>Español</button>
1import { useI18n } from "@comvi/solid";
2 
3export default function App() {
4 const { t, setLocale } = useI18n();
5 
6 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.tsx
2import { getI18n } from "@comvi/next/server";
3 
4export default async function Page({ params }) {
5 const { locale } = await params;
6 const { t } = await getI18n({ locale });
7 
8 return <h1>{t("hello", { name: "World" })}</h1>;
9}
1<!-- app/pages/index.vue -->
2<script setup>
3const { t } = useI18n();
4</script>
5 
6<template>
7 <h1>{{ t("hello", { name: "World" }) }}</h1>
8 <NuxtLinkLocale to="/" locale="es">Español</NuxtLinkLocale>
9</template>

What you actually get.

Six things that aren't standard in this category. Two of them — CSP-safe runtime and full ICU MessageFormat — are differentiators on their own.

01

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.

02standout

CSP-safe runtime

No eval, no new Function. Strict Content-Security-Policy works out of the box — no 'unsafe-eval' required.

03

Built on Intl

Number, date, currency, relative time formatting via the native Intl APIs your runtime already ships. No 200 kB polyfills.

04standout

Real ICU MessageFormat

Plurals, select, selectordinal, interpolation — full ICU, not a subset. Polish gets four plural forms, not one.

05

Safe tag interpolation

<link>here</link> in your translations becomes a real component. You control the rendering. No dangerouslySetInnerHTML, no XSS.

06

SSR-ready

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.

The standard, in full.

Pluralization that handles Polish. Select for grammatical variation. Ordinals that know German from Japanese. All in one expression, all in your translation file.

01Pluralsenglish · polish
1// en
2"messages": "You have {count, plural,
3 one {1 message}
4 other {# messages}
5}"
6 
7// pl ── four forms
8"messages": "Masz {count, plural,
9 one {# wiadomość}
10 few {# wiadomości}
11 many {# wiadomości}
12 other {# wiadomości}
13}"
14 
15t("messages", { count: 1 }) // "You have 1 message" | "Masz 1 wiadomość"
16t("messages", { count: 5 }) // "You have 5 messages" | "Masz 5 wiadomości"
02Selectformal vs informal
1// de ── formal / informal
2"your_name": "{formality, select,
3 formal {Ihr Name}
4 informal {Dein Name}
5 other {Ihr Name}
6}"
7 
8t("your_name", { formality: "formal" }) // "Ihr Name"
9t("your_name", { formality: "informal" }) // "Dein Name"
03selectordinal1st · 2nd · 3rd · 22nd
1// en ── selectordinal
2"rank": "You are {place, selectordinal,
3 one {#st}
4 two {#nd}
5 few {#rd}
6 other {#th}
7} place"
8 
9t("rank", { place: 1 }) // "You are 1st place"
10t("rank", { place: 3 }) // "You are 3rd place"
11t("rank", { place: 22 }) // "You are 22nd place"

Rich text. No dangerouslySetInnerHTML.

Put tags in your translation. Pass real components. Comvi calls them with the inner text — you decide what to render. No HTML injection. No XSS.

// translation
1{ help: "Click <link>here</link> for help" }
// rendered output
Click here for help
the inner text stays translatable. the surrounding markup is yours.
1// React
2<T
3 i18nKey="help"
4 components={{
5 link: <a href="/help" />,
6 }}
7/>
1<!-- Vue -->
2<T i18nKey="help">
3 <template #link="{ children }">
4 <a href="/help">{{ children }}</a>
5 </template>
6</T>
1<!-- Svelte -->
2<T
3 i18nKey="help"
4 components={{
5 link: { tag: "a", props: { href: "/help" } },
6 }}
7/>
1// SolidJS
2<T
3 i18nKey="help"
4 components={{
5 link: (props) => <a href="/help">{props.children}</a>,
6 }}
7/>

Numbers, dates, currency, relative time — locale-aware out of the box.

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";
4 
5formatNumber(i18n, 1234.5)
6// "1,234.5" (en) · "1.234,5" (de)
7 
8formatNumber(i18n, 0.75, { style: "percent" })
9// "75%" (en) · "75 %" (de)
10 
11formatDate(i18n, new Date(2025, 0, 15), {
12 year: "numeric", month: "long", day: "numeric"
13})
14// "January 15, 2025" (en) · "15. Januar 2025" (de)
15 
16formatCurrency(i18n, 99.99, "EUR")
17// "€99.99" (en) · "99,99 €" (de)
18 
19formatRelativeTime(i18n, -2, "hour")
20// "2 hours ago" (en) · "vor 2 Stunden" (de)
21 
22formatRelativeTime(i18n, 3, "day")
23// "in 3 days" (en) · "in 3 Tagen" (de)

Translations type themselves.

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.

01
Vite plugin
local JSON files
1// vite.config.ts
2import { comviTypes } from "@comvi/vite-plugin";
3 
4export default {
5 plugins: [comviTypes({ translations: "./src/locales" })],
6};
→ types regenerate on every translation file change
02
CLI
comvi platform users
1$ npm install -D @comvi/cli
2$ npx comvi typegen
→ pulls types from your Comvi project
03
InferKeys
zero tooling
1import type { InferKeys } from "@comvi/core";
2import en from "./locales/en.json";
3 
4declare module "@comvi/core" {
5 interface TranslationKeys
6 extends InferKeys<typeof en> {}
7}
→ autocomplete works without any build step

Translations on the server. Locale routing. SEO out of the box.

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.

i18n/config.tsmiddleware.tsapp/[locale]/layout.tsxapp/[locale]/page.tsxnuxt.config.tsapp/app.vueapp/pages/index.vue
1// src/i18n/routing.ts
2import { defineRouting } from "@comvi/next/routing";
3 
4export const routing = defineRouting({
5 locales: ["en", "de"],
6 defaultLocale: "en",
7});
8 
9// src/i18n/config.ts
10import { createNextI18n } from "@comvi/next";
11import { routing } from "./routing";
12import en from "./locales/en.json";
13import de from "./locales/de.json";
14 
15export const { i18n } = createNextI18n({
16 ...routing,
17 messages: { en, de },
18});
19 
20// next.config.ts
21import { withComvi } from "@comvi/next/plugin";
22 
23export default withComvi({ config: "./src/i18n/config.ts" })({});
1import { createMiddleware } from "@comvi/next/middleware";
2import { routing } from "./i18n/routing";
3 
4export default createMiddleware(routing);
5 
6export 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";
5 
6export default async function LocaleLayout({ children, params }) {
7 const { locale } = await params;
8 if (!hasLocale(routing.locales, locale)) notFound();
9 
10 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";
2 
3export default async function HomePage({ params }) {
4 const { locale } = await params;
5 const { t } = await getI18n({ locale });
6 
7 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>
5 
6<template>
7 <h1>{{ t("greeting", { name: "Alice" }) }}</h1>
8 <NuxtLink :to="switchLocalePath('de')">Deutsch</NuxtLink>
9</template>
— what's included —
  • server-side translation loading via the framework's data fetching
  • browser language detection with cookie persistence
  • locale-prefixed routes — /en/about, /de/ueber-uns
  • seo link tags for hreflang alternates on Nuxt — useLocaleHead()
  • locale switcher helpers — useSwitchLocalePath()
  • optional cdn loading via the cdnLoader plugin (with revalidation tags on Next.js)

Two official plugins. .use() them in.

Comvi i18n has a small core and an extensible plugin architecture. Two plugins ship today; both are MIT.

01@comvi/plugin-fetch-loader↗ source
Load translations from a CDN. Bundled fallback when the network fails. Request deduplication. Fetch cache for Next.js with revalidation tags. A separate /api entry reads the authenticated API on the server when the instance has an apiKey.
1import { cdnLoader } from "@comvi/plugin-fetch-loader";
2 
3const 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 }));
02@comvi/plugin-locale-detector↗ source
Auto-detect the locale from five sources — querystring, localStorage, sessionStorage, cookie, navigator — in configurable order. Detection result is cached for the next visit. SSR-safe.
1import { localeDetector } from "@comvi/plugin-locale-detector";
2 
3const i18n = createI18n({
4 locale: "en", // fallback if detection fails
5}).use(
6 localeDetector({
7 supportedLocales: ["en", "de", "fr", "pt-BR", "pt-PT"],
8 }),
9);

One library. A handful of entry points.

Pick the binding for your stack, add plugins as needed. Every package links to its README on GitHub — verify the source before installing.

— framework bindings —
@comvi/coreframework-agnostic corereadme ↗
@comvi/reactreact 18+ — hooks, <T> componentreadme ↗
@comvi/vuevue 3 — composables, <T> componentreadme ↗
@comvi/sveltesvelte 5.3+ — stores, <T> componentreadme ↗
@comvi/solidsolidjs — reactive primitives, <T> componentreadme ↗
@comvi/nextnext.js 15+ — app router, ssr, locale routingreadme ↗
@comvi/nuxtnuxt 3/4 — module, ssr, locale routingreadme ↗
— plugins —
@comvi/plugin-fetch-loaderload translations from cdn or apireadme ↗
@comvi/plugin-locale-detectorauto-detect from browser, url, cookie, storagereadme ↗
@comvi/plugin-in-context-editorvisual editing with comvi platformreadme ↗
— tooling —
@comvi/vite-plugingenerate types from local translation filesreadme ↗
@comvi/cligenerate types from comvi platform, sync translationsreadme ↗

You can run @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.

01
translations live in a collaborative editor
→ translators don't touch your code
02
auto-deployed to a global cdn via @comvi/plugin-fetch-loader
→ no rebuild on copy changes
03
typescript types auto-generated from the platform via @comvi/cli
→ autocomplete every key in your ide
04
in-context editing in your live app via @comvi/plugin-in-context-editor
→ translators hover any string, click to edit, save — no deploy
See the platform →— the library API doesn't change
// ready

Pick your stack and ship.

$npm install @comvi/react
MIT · zero dependencies · ≤10 kB with bindings