// 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.react react
@comvi/react
1import { createI18n } from "@comvi/core";
2 
3const i18n = createI18n({
4 language: "en",
5 translation: {
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 language: "en",
5 translation: {
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 language: "en",
5 translation: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
12 
13// in <script setup>
14const { t } = useI18n();
15t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/svelte";
2 
3export const i18n = createI18n({
4 language: "en",
5 translation: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
12 
13// in any .svelte file
14$t("hello", { name: "World" }); // "Hello, World!"
1import { createI18n } from "@comvi/solid";
2 
3const i18n = createI18n({
4 language: "en",
5 translation: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
12 
13const { 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";
4 
5const nextI18n = createNextI18n({
6 locales: ["en", "es"],
7 defaultLocale: "en",
8 translation: { en, es },
9});
10 
11export const { i18n, routing } = nextI18n;
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";
2 
3const 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});
16 
17await i18n.init();
1import { createI18n } from "@comvi/react";
2 
3const 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});
16 
17await i18n.init();
1import { createI18n } from "@comvi/vue";
2 
3const i18n = createI18n({
4 language: "en",
5 translation: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
12app.use(i18n);
1import { createI18n } from "@comvi/svelte";
2 
3export const i18n = createI18n({
4 language: "en",
5 translation: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
1import { createI18n } from "@comvi/solid";
2 
3const i18n = createI18n({
4 language: "en",
5 translation: {
6 en: { hello: "Hello, {name}!" },
7 es: { hello: "¡Hola, {name}!" },
8 },
9});
10 
11await i18n.init();
1// i18n.ts
2import { createNextI18n } from "@comvi/next";
3import en from "./locales/en.json";
4import es from "./locales/es.json";
5 
6const nextI18n = createNextI18n({
7 locales: ["en", "es"],
8 defaultLocale: "en",
9 translation: { en, es },
10});
11 
12export const { i18n, routing } = nextI18n;
1// nuxt.config.ts
2export default defineNuxtConfig({
3 modules: ["@comvi/nuxt"],
4 comvi: {
5 locales: ["en", "es"],
6 defaultLocale: "en",
7 },
8});
── 03 use ──
1i18n.t("hello", { name: "World" }); // "Hello, World!"
2i18n.t("items", { count: 5 }); // "5 items"
3 
4i18n.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"
3 
4i18n.language = "es";
5i18n.t("hello", { name: "World" }); // "¡Hola, World!"
6i18n.t("items", { count: 5 }); // "5 elementos"
1<script setup>
2const { t } = useI18n();
3</script>
4 
5<template>
6 <h1>{{ t("hello", { name: "World" }) }}</h1>
7</template>
1<script>
2 import { t } from "./i18n";
3</script>
4 
5<h1>{$t("hello", { name: "World" })}</h1>
1import { useI18n } from "@comvi/solid";
2 
3function Hello() {
4 const { t } = useI18n();
5 return <h1>{t("hello", { name: "World" })}</h1>;
6}
1// app/[locale]/page.tsx
2import { getI18n } from "@comvi/next/server";
3 
4export 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>
5 
6<template>
7 <h1>{{ t("hello", { name: "World" }) }}</h1>
8</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

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.

02 standout

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.

04 standout

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.

01 Plurals english · 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"
02 Select formal 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"
03 selectordinal 1st · 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: ({ children }) => <a href="/help">{children}</a>,
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 i18nKey="help">
3 <a slot="link" let:children href="/help">{children}</a>
4</T>
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 language.

1i18n.formatNumber(1234.5)
2// "1,234.5" (en) · "1.234,5" (de)
3 
4i18n.formatNumber(0.75, { style: "percent" })
5// "75%"
6 
7i18n.formatDate(new Date(), {
8 year: "numeric", month: "long", day: "numeric"
9})
10// "January 15, 2025" (en) · "15. Januar 2025" (de)
11 
12i18n.formatCurrency(99.99, "USD")
13// "$99.99" (en) · "99,99 $" (fr)
14 
15i18n.formatRelativeTime(-2, "hour")
16// "2 hours ago" (en) · "vor 2 Stunden" (de)
17 
18i18n.formatRelativeTime(3, "day")
19// "in 3 days"

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()],
6};
→ types regenerate on every translation file change
02
CLI
comvi platform users
1$ npm install -D @comvi/cli
2$ comvi generate-types
→ 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.ts middleware.ts app/[locale]/layout.tsx app/[locale]/page.tsx nuxt.config.ts comvi.setup.ts pages/index.vue
1import { createNextI18n } from "@comvi/next";
2import en from "./locales/en.json";
3import de from "./locales/de.json";
4 
5const nextI18n = createNextI18n({
6 locales: ["en", "de"],
7 defaultLocale: "en",
8 translation: { en, de },
9});
10 
11export const { i18n, routing } = nextI18n;
1import { createMiddleware } from "@comvi/next/middleware";
2import { routing } from "./i18n";
3 
4export default createMiddleware(routing);
1import { setRequestLocale, loadTranslations } from "@comvi/next/server";
2import { I18nProvider } from "@comvi/next/client";
3import { i18n, routing } from "@/i18n";
4 
5export default async function LocaleLayout({ children, params }) {
6 const { locale } = await params;
7 setRequestLocale(locale);
8 
9 const messages = await loadTranslations(locale);
10 
11 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";
2 
3export default async function HomePage({ params }) {
4 const { locale } = await params;
5 setRequestLocale(locale);
6 
7 const { t } = await getI18n();
8 
9 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";
2 
3export 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>
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
  • locale switcher helpers — useSwitchLocalePath()
  • optional cdn loading via FetchLoader plugin (with revalidation tags)

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 any CDN or your own API. Bundled fallback for offline. Request deduplication. SSR cache for Next.js with revalidation tags. Switches between public CDN and authenticated API based on whether you pass an apiKey.
1import { FetchLoader } from "@comvi/plugin-fetch-loader";
2 
3const 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 }));
02 @comvi/plugin-language-detector ↗ source
Auto-detect language from five sources — querystring, localStorage, sessionStorage, cookie, navigator — in configurable order. Detection result is cached for the next visit. SSR-safe.
1import { LanguageDetector } from "@comvi/plugin-language-detector";
2 
3const i18n = createI18n({
4 language: "en", // fallback if detection fails
5}).use(
6 LanguageDetector({
7 supportedLanguages: ["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/core framework-agnostic core readme ↗
@comvi/react react 16.8+ — hooks, <T> component readme ↗
@comvi/vue vue 3 — composables, <T> component readme ↗
@comvi/svelte svelte 4/5 — stores, <T> component readme ↗
@comvi/solid solidjs — reactive primitives, <T> component readme ↗
@comvi/next next.js 14+ — app router, ssr, locale routing readme ↗
@comvi/nuxt nuxt 3 — module, ssr, locale routing readme ↗
— plugins —
@comvi/plugin-fetch-loader load translations from cdn or api readme ↗
@comvi/plugin-language-detector auto-detect from browser, url, cookie, storage readme ↗
@comvi/plugin-in-context-editor visual editing with comvi platform readme ↗
— tooling —
@comvi/vite-plugin generate types from local translation files readme ↗
@comvi/cli generate types from comvi platform, sync translations readme ↗

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