@nabulingo/translate
v0.1.0
Published
AI-powered build-time translation for Next.js, React, and any static site. Eat-your-own-dogfood translation — same engine as the NabuLingo WordPress & Shopify plugin.
Maintainers
Readme
@nabulingo/translate
AI-powered build-time translation for Next.js, React, and any static site. The same engine that powers the NabuLingo WordPress & Shopify plugin — now as a dev-friendly npm package.
- ⚡ Build-time, not runtime. Translations are baked into static HTML. Zero latency, zero API cost per page-view, perfect SEO.
- 🧠 Pluggable providers. Anthropic Claude, DeepL, OpenAI — use your own keys. Later, swap to
api.nabulingo.comwith one config change. - 🔒 Human-override aware. Mark translations
reviewed: trueand the translator never touches them again. - 🪶 Tiny API.
<T>Hello</T>oruseT(). That's it. - 📦 Framework-first for Next.js, but the core is framework-agnostic — use it with Astro, SvelteKit, or a
package.jsonscript.
Quickstart
npm install @nabulingo/translate
npx nabulingo initConfig file format: v0.1 only loads
nabulingo.config.mjs/.js/.cjsreliably.nabulingo.config.tsonly works if you run the CLI viatsx(npx tsx node_modules/@nabulingo/translate/dist/cli.js ...). Stick with.mjsunless you have a reason.
Edit the generated nabulingo.config.mjs:
import { defineConfig } from "@nabulingo/translate";
export default defineConfig({
sourceLocale: "en",
locales: ["de", "fr", "es"],
provider: {
type: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
model: "claude-haiku-4-5-20251001",
},
glossary: { NabuLingo: "NabuLingo" },
tone: { de: "formal", fr: "formal", es: "informal" },
routing: "subpath",
});Use it in your components:
import { T, useT } from "@nabulingo/translate/react";
export function Hero({ name }: { name: string }) {
const t = useT();
return (
<section>
<h1><T>Your personal AI language tutor.</T></h1>
<p>{t("Welcome back, {name}!", { name })}</p>
</section>
);
}Extract and translate:
export ANTHROPIC_API_KEY=sk-ant-...
npx nabulingo extract # scan code → messages/_source.json
npx nabulingo translate # call provider → messages/de.json, messages/fr.json, ...Commit messages/ to git. You're done.
How it works
┌────────────────┐
│ your *.tsx │
└────────┬───────┘
│ extract (AST walk)
▼
┌────────────────┐
│ _source.json │ ← hash → {source, context, seenIn}
└────────┬───────┘
│ translate (provider API)
▼
┌──────────────────────────┐
│ de.json fr.json ... │ ← hash → {value, reviewed, updatedAt}
└────────────┬─────────────┘
│ loadDictionary() at build time
▼
┌────────────────┐
│ static HTML │
└────────────────┘Every source string gets a content hash. When the source changes, the hash changes, and stale translations are dropped automatically. Human-reviewed translations (reviewed: true) are never overwritten, even on --force.
Next.js integration
Important —
<html lang>caveat: Next.js renders<html>exactly once from the root layout. A[locale]/page.tsxcannot change it. For correctlangattributes, either put your locale route inside a route group with its own root-layout (e.g.app/(i18n)/[locale]/layout.tsx), or make your real root layout a server component that reads the locale from the URL viaheaders()/params.
1. Layout with [locale] segment
// app/[locale]/layout.tsx
import { NabuLingoProvider, loadDictionary, isRTL } from "@nabulingo/translate/next";
import config from "../../nabulingo.config.mjs";
export function generateStaticParams() {
return config.locales.map((locale) => ({ locale }));
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const dictionary = await loadDictionary("messages", params.locale);
return (
<html lang={params.locale} dir={isRTL(params.locale) ? "rtl" : "ltr"}>
<body>
<NabuLingoProvider
locale={params.locale}
sourceLocale={config.sourceLocale}
dictionary={dictionary}
>
{children}
</NabuLingoProvider>
</body>
</html>
);
}2. hreflang + canonical metadata
// app/[locale]/page.tsx
import { buildAlternates } from "@nabulingo/translate/next";
import config from "../../nabulingo.config.mjs";
export async function generateMetadata({ params }: { params: { locale: string } }) {
return {
alternates: buildAlternates(config, params.locale, "/", "https://example.com"),
};
}3. Locale detection at the edge
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { pickLocaleFromHeader, resolveConfig } from "@nabulingo/translate";
import raw from "./nabulingo.config.mjs";
const config = resolveConfig(raw);
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
if (config.locales.some((l) => pathname.startsWith(`/${l}`))) return;
const locale = pickLocaleFromHeader(config, req.headers.get("accept-language"));
if (locale === config.sourceLocale) return;
const url = req.nextUrl.clone();
url.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(url);
}CLI reference
| Command | Purpose |
| --- | --- |
| nabulingo init | Scaffold config + messages/ folder. |
| nabulingo extract | Parse src/**/*.{ts,tsx} for <T> and useT() usage. |
| nabulingo translate | Fill missing translations via the configured provider. |
| nabulingo translate --force | Retranslate cached entries (reviewed: true still skipped). |
| nabulingo translate --locale de | Target a single locale. |
| nabulingo check | Exit non-zero if any translation is missing. CI-friendly. |
Providers
| Provider | Best for | Tone/Formality | Glossary |
| --- | --- | --- | --- |
| anthropic (Claude) | Marketing copy, nuance, tone matching | ✅ via system prompt | ✅ via prompt |
| deepl | Fast bulk translation, well-known quality | ✅ native formality | ⚠️ server-side only |
| openai | Fallback option | ✅ via system prompt | ✅ via prompt |
| custom | Bring your own | your call | your call |
You can mix: configure two projects with different providers, or wrap your own provider via type: "custom".
Human override workflow
- Run
nabulingo translateto generate a first draft. - Edit
messages/de.jsondirectly — rewrite whatever needs a human touch. - Flip
"reviewed": trueon those entries. - From now on,
nabulingo translateleaves those entries alone forever.
This is how you keep hero-copy and conversion-critical strings pristine while letting FAQ, legal, and long-tail copy run on autopilot.
Roadmap
@nabulingo/translatev0.2 — Astro + SvelteKit adapters, pluralization (ICU MessageFormat).- v0.3 —
api.nabulingo.comprovider: centralized translation memory, team review UI, usage-based billing. - v1.0 — Stable API, semver commitments.
License
MIT © OBHOLZ SOLUTIONS
