@devalxui/kova-translate
v0.1.0
Published
Free, dead-simple i18n. <K> tags + JSON dictionaries + URL-based locale detection. No AI, no API keys, no paid services. Works with React, Next.js, and plain HTML.
Downloads
141
Maintainers
Readme
@devalxui/kova-translate
Free, dead-simple i18n. Wrap text in
<K>tags, run one command, ship every language. No AI, no API keys, no paid services.
npm install @devalxui/kova-translate
npx kova-translate initTable of contents
- Why kova-translate
- The two modes
- 60-second quick start
- The setup wizard
- Using it — React
- Using it — Next.js
- Using it — plain HTML / Vue / Svelte / anything
- Using it — Node / framework-agnostic core
- Variable interpolation
- Plurals
- HTML inside translated strings
- The CLI
- The runtime cache
- How translation works under the hood
- Configuration file
- Recipes
- Troubleshooting
- FAQ
- License
Why kova-translate
Most i18n libraries make you do all the work — extract strings into JSON, write translation keys, wire up a provider, then pay $20/mo to a translation service to actually fill the JSON.
kova-translate flips it. You write English. It translates the rest, automatically, for free.
- Wrap any text in
<K>...</K>(React) or<k>...</k>(HTML). - Run
npx kova-translate synconce and every locale's JSON is filled in for you. - Or pass
autoTranslate: trueand skip JSON files entirely — translations happen at runtime, cached forever in localStorage. - Variable placeholders (
{name},{count}) are protected and survive every translation pass. - A built-in setup wizard (
kova-translate init) writes the config, adds npm scripts, and prints copy-pasteable code for your framework.
It works with React, Next.js, Vue, Svelte, vanilla HTML, or anything else.
The two modes
You pick one or both — the wizard asks you which during setup.
Build-time
Run npx kova-translate sync whenever you add or change <K> tags. It walks your source, extracts every string, and fills locales/de.json, locales/fr.json, etc. via the free Google Translate endpoint. You ship those JSON files with your app.
- Pros: zero runtime overhead, works offline, deterministic, you can hand-tune any translation by editing the JSON.
- Cons: re-run the CLI when text changes.
Runtime
Pass autoTranslate: true to the provider/init call and don't ship any JSON. When a <K> tag's text isn't yet known in the active locale, kova-translate quietly fetches a translation from MyMemory's free public endpoint, caches it forever in localStorage, and updates the DOM.
- Pros: zero setup, write once in English, auto-translate forever. Subsequent visits hit cache, no network.
- Cons: first paint per-string requires a network round-trip; relies on a free public service (5k chars/day per IP, 50k with email).
Hybrid (the default)
Use both. Ship JSON for your hot paths (landing page, billing copy) and let autoTranslate: true handle long-tail strings. Static dictionary takes priority; runtime fills in anything missing.
60-second quick start
# 1. install
npm install @devalxui/kova-translate
# 2. set up — interactive wizard, picks framework + mode + locales
npx kova-translate init
# 3. write code with <K> tags
# (the wizard prints the snippet for your framework)
# 4. translate everything
npm run translateThat's it. You now have locales/de.json, locales/fr.json, etc. fully populated.
The setup wizard
npx kova-translate initIt asks:
- Source / default locale — usually
en. - Target locales — comma-separated 2-letter ISO codes:
de, fr, es, ja, pt, it. - Source code directory — usually
./srcor./app. - Locale JSON output directory — usually
./localesor./public/locales. - Translation mode — build-time, runtime, or hybrid.
- Framework — React, Next.js, vanilla, Vue, Svelte, or other.
- Run initial scan + auto-translate now? — fills the JSON immediately.
Then it:
- writes
kova-translate.config.json(so future commands don't need flags) - adds
npm run translateandnpm run translate:autoto yourpackage.json - runs the initial scan + translation for you
- prints a tailor-made setup snippet for your framework
Re-run anytime to change settings; it offers your existing config as defaults.
Using it — React
// app/providers.tsx
import { TranslateProvider, K, useTranslate } from "@devalxui/kova-translate/react";
import en from "./locales/en.json";
import de from "./locales/de.json";
import fr from "./locales/fr.json";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<TranslateProvider
locales={["en", "de", "fr"]}
defaultLocale="en"
translations={{ en, de, fr }}
autoTranslate // optional — fills missing keys at runtime
>
{children}
</TranslateProvider>
);
}// any component
import { K, useTranslate } from "@devalxui/kova-translate/react";
export function Hero() {
const { t, locale, setLocale } = useTranslate();
return (
<section>
<h1><K>Welcome to Kova</K></h1>
<p><K vars={{ name: "Alex" }}>{`Hello {name}`}</K></p>
{/* same thing without the JSX wrapper: */}
<h2>{t("Earn $500 per signup")}</h2>
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
</select>
</section>
);
}<TranslateProvider> props
| Prop | Type | Default | Notes |
|------------------|--------------------------------------------|--------------------------|-------|
| locales | string[] | required | All supported locales. |
| defaultLocale | string | required | Source language code. |
| translations | Record<string, Record<string, string>> | {} | Static dictionaries. Optional with autoTranslate. |
| locale | string | auto-detected | Force a specific locale. |
| autoDetect | boolean | true | Read locale from URL path's first segment. |
| autoTranslate | boolean | false | Fetch missing keys from MyMemory at runtime, cache in localStorage. |
| providers | TranslateProvider[] | BROWSER_PROVIDERS | Override the engine chain. |
| cacheKey | string | "kova-translate-cache" | localStorage key. |
<K> props
| Prop | Type | Default | Notes |
|-------------|-------------------------------------|------------|-------|
| children | string | required | Source text. Use {name} for placeholders. |
| vars | Record<string, string \| number> | undefined| Values for placeholders. |
| as | keyof JSX.IntrinsicElements | Fragment | Wrap output in this tag. |
| className | string | undefined| Applied when as is set. |
useTranslate()
const { t, plural, locale, setLocale } = useTranslate();
t("Welcome to Kova"); // "Willkommen bei Kova"
t("Hello {name}", { name: "Alex" }); // "Hallo Alex"
plural(5, { one: "1 cat", other: "{count} cats" }); // "5 Katzen"
setLocale("fr"); // switch languageUsing it — Next.js
Use the React adapter inside app/layout.tsx, then optionally wire up middleware to auto-prefix the URL with the active locale.
// app/layout.tsx
"use client";
import { TranslateProvider } from "@devalxui/kova-translate/react";
import en from "../locales/en.json";
import de from "../locales/de.json";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<TranslateProvider
locales={["en", "de"]}
defaultLocale="en"
translations={{ en, de }}
autoTranslate
>
{children}
</TranslateProvider>
</body>
</html>
);
}// middleware.ts
import { NextResponse } from "next/server";
import { createLocaleMiddleware } from "@devalxui/kova-translate/next";
export const middleware = createLocaleMiddleware(NextResponse, {
locales: ["en", "de", "fr"],
defaultLocale: "en",
strategy: "redirect", // "redirect" | "rewrite" | "passthrough"
detectFromHeader: true, // honor Accept-Language on first hit
});
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)"],
};The three middleware strategies:
passthrough— leave the URL alone (default). The provider still detects the active locale from the URL when present.rewrite— internally rewrite/aboutto/{detected}/about. URL stays clean.redirect— 302 to/{detected}/about. URL changes visibly.
Using it — plain HTML / Vue / Svelte / anything
The vanilla adapter scans the DOM for <k> tags and translates them in place. It works in any environment with a DOM — Vue components, Svelte components, raw HTML, anything that ends up rendering elements.
<h1><k>Welcome to Kova</k></h1>
<p><k>Hello {name}</k></p>
<button><k>Sign up</k></button>
<input data-k-placeholder="Search the docs">
<script type="module">
import { init, setLocale, setVars } from "https://esm.sh/@devalxui/kova-translate/vanilla";
init({
locales: ["en", "de", "fr", "es", "ja"],
defaultLocale: "en",
autoDetect: true,
autoTranslate: true, // optional — fetches missing strings
vars: { name: "Alex" },
// translations: { en, de, ... } // optional with autoTranslate
});
</script>Drop-in <script> tag (no bundler)
If you don't want a build step or ES modules at all, use the global build from unpkg/jsdelivr:
<script src="https://unpkg.com/@devalxui/kova-translate/dist/kova-translate.global.js"></script>
<script>
KovaTranslate.init({
locales: ["en", "de", "fr"],
defaultLocale: "en",
autoTranslate: true,
});
// KovaTranslate.setLocale("de"), .setVars(...), .plural(...), etc.
</script>Bundle is ~7.5 kB minified.
Vanilla API
import {
init, // initialize and start scanning
setLocale, // change active locale, re-translate all <k> tags
setVars, // merge new variable values, re-translate
t, // translate a single string outside the DOM
getLocale, // current active locale or null
clearCache, // wipe the localStorage cache
destroy, // stop the MutationObserver
} from "@devalxui/kova-translate/vanilla";init() accepts:
| Option | Type | Default | Notes |
|------------------|-------------------------------------------|--------------------------|-------|
| locales | string[] | required | |
| defaultLocale | string | required | |
| translations | Record<string, Record<string, string>> | {} | Optional with autoTranslate. |
| locale | string | auto-detected | |
| autoDetect | boolean | true | First URL path segment. |
| autoTranslate | boolean | false | Runtime auto-translate via MyMemory. |
| tag | string | "k" | Custom tag name. |
| vars | Record<string, string \| number> | {} | |
| providers | TranslateProvider[] | BROWSER_PROVIDERS | |
| cacheKey | string | "kova-translate-cache" | |
Vue
<script setup>
import { onMounted } from "vue";
import { init } from "@devalxui/kova-translate/vanilla";
onMounted(() => {
init({
locales: ["en", "de"],
defaultLocale: "en",
autoTranslate: true,
});
});
</script>
<template>
<h1><k>Welcome to Kova</k></h1>
</template>Svelte
<script>
import { onMount } from "svelte";
import { init } from "@devalxui/kova-translate/vanilla";
onMount(() => {
init({
locales: ["en", "de"],
defaultLocale: "en",
autoTranslate: true,
});
});
</script>
<h1><k>Welcome to Kova</k></h1>Using it — Node / framework-agnostic core
For server-side rendering, scripts, or any non-DOM context, use the core directly.
import {
translate,
interpolate,
detectLocaleFromPath,
stripLocaleFromPath,
} from "@devalxui/kova-translate";
const locale = detectLocaleFromPath("/de/about", ["en", "de"], "en"); // "de"
const text = translate("Hello {name}", locale, dictionaries, "en", { name: "Alex" });For programmatic auto-translation (CI scripts, custom tooling):
import { autoTranslate, NODE_PROVIDERS } from "@devalxui/kova-translate/engine";
const value = await autoTranslate("Welcome to Kova", "en", "ja", NODE_PROVIDERS);
// -> "Kovaへようこそ"Variable interpolation
Use curly braces in your source text:
<K vars={{ name: "Alex", count: 3 }}>
{`Hi {name}, you have {count} new referrals.`}
</K>Why the backtick template literal? React interprets {name} inside JSX as an expression. The template literal makes it a literal string with curly braces.
The CLI extracts these placeholders, sends the masked text (__KT0__, you have __KT1__ new referrals.) to the translator, and restores them after. The translation engine never sees your placeholder names — they're protected end-to-end.
In vanilla HTML (no JSX), you just write the curlies directly:
<k>Hi {name}, you have {count} new referrals.</k>Plurals
Per-locale plural rules are handled with the browser's built-in Intl.PluralRules. Every CLDR category is supported: zero, one, two, few, many, other. Each form is its own translation key, so the scanner picks them all up.
React
import { Plural, useTranslate } from "@devalxui/kova-translate/react";
// component form
<Plural
count={count}
one="1 referral"
other="{count} referrals"
/>
// hook form
const { plural } = useTranslate();
plural(count, {
one: "1 unread message",
other: "{count} unread messages",
});Vanilla / anywhere
import { plural } from "@devalxui/kova-translate/vanilla";
plural(count, {
one: "1 cat",
other: "{count} cats",
});Languages with multiple plural forms
Russian, Arabic, Polish, etc. — just pass every form your translation needs:
<Plural
count={count}
one="1 элемент"
few="{count} элемента"
many="{count} элементов"
other="{count} элементов"
/>{count} is automatically merged into vars; pass extra vars if you need them.
The CLI scanner extracts every form from <Plural one="..." other="..." /> props and from plural(count, { one: "...", other: "..." }) calls, treating each as a regular translation key.
HTML inside translated strings
By default <K> is text-only — safer and stops surprises. Opt in with dangerouslyAllowHtml when you want inline tags inside the translated string:
<K dangerouslyAllowHtml>
{`Welcome to <strong>Kova</strong>, the last referral toolkit you'll need.`}
</K>The string is translated as a whole (Google/MyMemory preserve HTML tags), then rendered via dangerouslySetInnerHTML. Only use this with trusted source text — never user input.
In vanilla, set data-html on the tag:
<k data-html>Welcome to <strong>Kova</strong>!</k>The runtime swaps innerHTML instead of textContent. Source text is still captured before init, so re-translations apply correctly.
For richer mixed content (a <strong> from a component, not literal HTML), prefer composition with multiple <K> tags:
<>
<K>Welcome to</K> <strong><K>Kova</K></strong>{". "}
<K>The last referral toolkit.</K>
</>The CLI
npx kova-translate <command> [options]Commands
| Command | What it does |
|--------------------------|--------------|
| init | Interactive setup wizard. Run this first. |
| sync (default) | Scans source code, then auto-translates every locale. |
| scan | Scans only — extracts keys into JSON files (no translation). |
| auto | Auto-translates only — assumes JSON files already exist. |
| help | Prints help. |
Options
| Option | Notes |
|---------------------|-------|
| <src> (positional)| Source directory to scan. Default: ./src (or config). |
| --out <dir> | Output directory for locale JSON. Default: ./locales. |
| --locales <list> | Comma-separated target locales. |
| --default <code> | Source/default locale. |
| --force | Re-translate even non-empty entries. |
| --delay <ms> | Delay between API calls. Default: 80. |
What the scanner extracts
| Pattern | Example |
|----------------------|--------------------------------------|
| React <K> element | <K>Welcome to Kova</K> |
| <K> with vars | <K vars={{ name }}>{Hi {name}}</K> |
| HTML <k> element | <k>Welcome to Kova</k> |
| Function call t() | t("Sign in") or t('Sign up') |
It walks .ts, .tsx, .js, .jsx, .mjs, .cjs, .html, .vue, .svelte files, skipping node_modules, .next, dist, etc.
The runtime cache
When autoTranslate: true is on, every translated string is cached in localStorage under the key kova-translate-cache:
{
"de": {
"Welcome to Kova": "Willkommen bei Kova",
"Sign up": "Registrieren"
},
"ja": {
"Welcome to Kova": "Kovaへようこそ"
}
}- Cache survives page reloads — no re-fetches.
- Hand-edit it in DevTools to override any auto-translation.
- Clear it with
clearCache()(vanilla) or by deleting the localStorage entry. - Customize the storage key with
cacheKey: "my-app-i18n".
The static translations prop always wins over the cache, so you can override any auto-translation by adding it to your JSON file.
How translation works under the hood
Two free, no-API-key engines:
- Google Translate (unofficial endpoint) — used in Node CLI. Same endpoint the Google Translate widget hits. No auth, but rate-limits at high volume. Best quality.
- MyMemory (
api.mymemory.translated.net) — used in browsers. CORS-enabled. Free up to 5k chars/day per IP, 50k/day with email registered.
The engine tries them in order (autoTranslate() falls back if the first fails). You can supply your own provider chain — anything implementing { name, translate(text, source, target) } works:
import { autoTranslate, googleProvider, createMyMemoryProvider } from "@devalxui/kova-translate/engine";
const myMemory = createMyMemoryProvider({ email: "[email protected]" }); // 50k/day quota
const value = await autoTranslate("Welcome", "en", "de", [googleProvider, myMemory]);Placeholder protection
Before sending to a provider, every {name} is replaced with __KT0__, __KT1__, etc. After the translation comes back, those tokens are restored verbatim. Translators never see your variable names.
Configuration file
kova-translate.config.json lives in your project root. The wizard creates it; the CLI reads it.
{
"src": "./src",
"out": "./public/locales",
"defaultLocale": "en",
"locales": ["en", "de", "fr", "es", "ja"],
"framework": "next",
"mode": "hybrid",
"delayMs": 80
}CLI flags always override config values. The framework and mode fields are reference for the wizard only — the CLI doesn't act on them.
Recipes
Add a new language to an existing project
# edit kova-translate.config.json — add "ja" to "locales"
npm run translate
# only the missing entries get translatedRe-translate everything (e.g. you fixed your source copy)
npx kova-translate sync --forceTranslate only specific locales
npx kova-translate auto --locales de,fr --default enHand-edit a translation
Open locales/de.json and edit the value. The CLI never overwrites non-empty entries unless you pass --force.
Override a single translation at runtime
The static translations prop wins over the runtime cache. Add an entry to de.json and it takes precedence over whatever MyMemory returned.
Translate a string outside JSX
const { t } = useTranslate();
const error = t("Could not save changes");
toast.error(error);Translate plain string in a Node script
import { autoTranslate, NODE_PROVIDERS } from "@devalxui/kova-translate/engine";
const ja = await autoTranslate("Welcome", "en", "ja", NODE_PROVIDERS);Custom tag name (vanilla)
init({
locales: ["en", "de"],
defaultLocale: "en",
tag: "trans", // <trans>Hello</trans>
});Detect locale from a custom URL pattern
import { detectLocaleFromPath, stripLocaleFromPath } from "@devalxui/kova-translate";
const locale = detectLocaleFromPath("/de/about", ["en", "de"], "en"); // "de"
const path = stripLocaleFromPath("/de/about", ["en", "de"]); // "/about"Troubleshooting
<K> text shows up untranslated
- Build mode: did you run
npm run translateafter adding the tag? - Runtime mode: open DevTools → Network. You should see a request to
api.mymemory.translated.net. If not,autoTranslate: trueisn't set. - Either: check that the active locale is correctly detected (
useTranslate().locale).
Translations look weird / over-formal
Free providers (especially MyMemory) sometimes pick odd phrasings. Two fixes:
- Hand-edit the JSON — kova-translate respects manual edits forever.
- Use Google's endpoint via the CLI — it generally produces nicer output than MyMemory.
429 Too Many Requests
You hit MyMemory's daily quota. Options:
- Add
emailto your provider:createMyMemoryProvider({ email: "[email protected]" })raises the cap to 50k chars/day. - Switch to build-time mode and run the CLI once — Google's endpoint via Node has higher tolerance.
{name} shows literally instead of the value
- Did you pass
vars?<K vars={{ name: "Alex" }}>{Hello {name}}</K> - Are placeholder names spelled identically?
Next.js <K> warning about children type
<K> requires a string child. If you're rendering a string from a variable, use t() instead:
const { t } = useTranslate();
return <span>{t(message)}</span>;CLI isn't finding my files
The scanner skips node_modules, .next, dist, build, .git, coverage, out. It walks .ts/.tsx/.js/.jsx/.mjs/.cjs/.html/.htm/.vue/.svelte. Pass an explicit src dir as the first argument: npx kova-translate sync ./app.
FAQ
Is this really free? No catch?
Yes. The engine uses two public endpoints — Google's unofficial widget endpoint and MyMemory — both of which work without API keys. There are rate limits, not paywalls. For small-to-medium apps, you'll never hit them.
What if Google changes the endpoint?
The engine has a fallback chain — if Google fails, MyMemory takes over. You can also plug in your own provider (LibreTranslate, DeepL, your own Argos instance) by implementing TranslateProvider.
Is the translation quality good?
For short UI strings: yes, generally on par with paid services. For long-form copy (paragraphs of marketing text): acceptable, but you may want to hand-edit the worst offenders. The JSON is just a file — fix anything you don't like.
Will the runtime mode slow down my page?
The first paint of any untranslated string takes one network round-trip (~100-300ms). After that, every subsequent load reads from localStorage with zero network. So: slow once, fast forever.
Does this work with SSR?
The static translations prop works in SSR fine. autoTranslate: true only kicks in client-side (it needs localStorage), so SSR will render source text and the client hydration will translate after mount. For best SSR results, use build-time mode.
Can I use this with a database / CMS?
Yes — you don't have to use the JSON files. Pass any object shaped as { [locale]: { [key]: string } } to the translations prop. Load from your DB, your CMS, your CDN — anywhere.
What about plurals?
Supported via <Plural> (React), useTranslate().plural(count, forms) (React), and the plural(count, forms) function exported by the vanilla adapter. Uses Intl.PluralRules so every locale's CLDR categories work — one, few, many, etc. See the Plurals section.
Can I translate HTML inside <K>?
Yes — pass dangerouslyAllowHtml on <K> (React) or data-html on <k> (HTML). The string is translated whole and rendered via innerHTML / dangerouslySetInnerHTML. Only use with trusted source text.
License
MIT © KOVA
