zintljs
v0.1.0-alpha.20
Published
Compile-time internationalization for Vite — no keys, no wrappers, and only the translations each screen actually needs.
Maintainers
Readme
Zintl is a compile-time internationalization engine.
Most i18n libraries ask you to change how you write code — wrap every string in t(), invent a key for it, keep a dictionary in sync by hand. Zintl doesn't. You write normal strings; the compiler finds them, works out which ones each part of your app actually needs, and ships exactly those.
Install
npm install -D zintljsor pnpm add -D zintljs · yarn add -D zintljs
Use
1. Add the plugin.
// vite.config.ts
import { defineConfig } from "vite";
import zintl from "zintljs/vite";
export default defineConfig({
plugins: [zintl({ locales: ["en", "ar", "fr"] })],
});// rsbuild.config.ts
import { defineConfig } from "@rsbuild/core";
import zintl from "zintljs/rsbuild";
export default defineConfig({
plugins: [...zintl({ locales: ["en", "ar", "fr"] })],
});Same plugin, same options — note the spread, since the Rsbuild entry point returns an array. Everything below applies unchanged. What's covered there.
2. Set a locale.
// src/main.ts
import { zintl } from "zintljs/macro";
const locale = new URLSearchParams(location.search).get("lang") ?? "en";
await zintl(locale);
document.querySelector("#app").innerHTML = `<h1>Welcome back!</h1>`;That's the whole API. No keys, no wrappers, no dictionary to maintain.
What you pass matters. A variable ships every locale and lets users switch at runtime. A literal —
zintl("fr")— tells the compiler this page is French: it bakes that locale in, emits no catalog chunk, and leaves every other locale out of the bundle, source language included. Use a literal for per-locale static builds, a variable whenever language is a choice.
zintljs/vite(orzintljs/rsbuild) is the plugin;zintljs/macrois what you call in app code. Importing the wrong one into your config gives you an async no-op rather than a plugin.
3. Run your dev server. Zintl extracts your strings and writes one file per locale, ready to fill in:
// zintl/src/main.fr.json
{
"Welcome back!": ""
}Fill it in and the page updates without a reload. Leave it empty and the production build refuses to ship — a blank string is a bug, not a fallback.
What you get
Only what each screen needs. Zintl works out which strings are reachable from which entry point, and splits catalogs along the same lines your bundler splits code. Opening the settings page downloads the settings translations, not all of them.
Nothing extra in the bundle. Plurals and grammar rules compile to plain JavaScript at build time — no ICU parser reaches the browser. Your source locale is never written to disk; the compiler already has those strings.
Translations that survive refactors. Identity is content-based, not path-based — so translations aren't attached to a file, a line, or a key you have to keep stable. Rename components, move files, restructure whole directories: the translations follow. Even a source string edited into a near-identical one keeps what it had, instead of being orphaned and sent back to translators.
Restructuring an app usually means a day of reconciling catalogs afterwards. Here it means zero.
Grammar where it belongs. Source files keep simple template literals; plural and gender rules live in the catalog, with a generated JSON schema so translators get autocomplete.
const msg = `You have ${count} items in your cart`;{
"You have {count} items in your cart": "{count, plural, =0 {سلتك فارغة} one {لديك عنصر واحد} other {لديك {count} عناصر}}"
}Where it runs
Works with React, Vue, Svelte, and vanilla apps, on Vite 6, 7 or 8 or on Rsbuild 2 — install whichever you build with; both are optional peer dependencies. Node ^22.18.0 || >=24.11.0.
On Vite that covers client-rendered, server-rendered and multi-page apps, with all four frameworks. On Rsbuild it covers single-page and multi-page apps, also with all four, in production builds and in dev. Two exclusions there are worth knowing before you start: per-locale HTML fan-out (multiplex) and SSR are Vite-only, and combining them with Rsbuild fails your build with a clear error rather than doing nothing quietly. Vue components work with either <script setup> or the Options API, on both hosts. The full comparison.
That list is a starting point, not the design. The extractor carries no framework knowledge and the compiler is bundler-agnostic, so another framework or another build tool is something you add rather than something the core is rewritten around. More of both are coming.
Docs
- Configuration — every option
- Comment directives —
@zintl-ignore,@zintl-note,@zintl-pass - Plurals & grammar
- Glossary
Status
Alpha. The ideas are settled and the test suite is thorough, but the API can still move between releases. Pin your version, and please open an issue when something surprises you — early reports shape this more than anything else right now.
License
MIT © Khalid F. Shuhail
