next-intl-auto-translate
v0.1.0
Published
Next.js plugin that auto-translates next-intl message files into multiple locales at dev/build time via the OpenAI chat API.
Maintainers
Readme
next-intl-auto-translate
A Next.js plugin that auto-translates your next-intl messages into every configured locale on every dev rebuild and production build. Edit en.json, save, and ru.json / fr.json / … appear next to it — translated through the OpenAI chat API and cached in a vocabulary.json so the same string is never re-translated twice.
Why?
Maintaining next-intl message files by hand is the slow part of i18n. Most tools either ship a heavyweight runtime, gate behind a vendor SDK, or break the build when the model misbehaves. next-intl-auto-translate keeps it minimal:
- One main locale file (
en.json) is the source of truth. You only edit that. - The plugin runs at dev/build time, batches every new string into one OpenAI call, and writes the translated
<locale>.jsonfiles atomically next to the source. - A local
vocabulary.jsoncaches everytext → { locale → translation }pair, so subsequent runs only translate new strings. - Works with both Webpack and Turbopack (file watcher fallback).
- Never breaks the build: when the API fails or returns garbage, missing translations fall back to the original English text.
Translation itself is delegated to json-i18n-auto-translate — this package is the Next.js integration around it.
Installation
npm install -D next-intl-auto-translateRequires Node.js >=18 and Next.js >=13. Set your API key:
export OPENAI_API_KEY=sk-...Quick start
As a Next.js plugin
next.config.ts:
import type { NextConfig } from "next"
import { createNextIntlAutoTranslatePlugin } from "next-intl-auto-translate"
const withAutoTranslate = createNextIntlAutoTranslatePlugin({
inputFile: "./messages/en.json",
locales: ["en", "ru", "fr", "de"],
})
const nextConfig: NextConfig = {
// ...your config
}
export default withAutoTranslate(nextConfig)On next dev and next build, the plugin reads messages/en.json, batch-translates anything new, and writes messages/ru.json, messages/fr.json, messages/de.json plus a sibling messages/vocabulary.json.
From a config file
Create next-intl-auto-translate.config.json next to your messages:
{
"inputFile": "./messages/en.json",
"locales": ["en", "ru", "fr", "de"],
"notes": "Friendly, informal tone. Keep product names like 'Acme' untranslated."
}Then either pass configPath:
const withAutoTranslate = createNextIntlAutoTranslatePlugin({
configPath: "./next-intl-auto-translate.config.json",
})…or run the CLI as a one-shot:
npx next-intl-auto-translate --config ./next-intl-auto-translate.config.jsonAPI
createNextIntlAutoTranslatePlugin(options)
Returns a (nextConfig) => nextConfig wrapper. Accepts either inline options or a configPath (mutually exclusive).
| Option | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| inputFile | string | yes | — | Path to the source-of-truth messages file. The basename (without extension) is the main locale. |
| locales | readonly string[] | yes | — | All locales, including the main locale. Files are written for every locale except the main one. |
| notes | string | no | — | Translator notes (tone, glossary, audience) appended to the OpenAI system prompt. |
| vocabFile | string | no | <inputFile-dir>/vocabulary.json | Where to cache text → { locale → translation } pairs. |
| debug | boolean | no | false | Verbose logs. |
| configPath | string | no | — | Use a JSON config file instead of inline options. Cannot be combined with the other fields. |
createWebpackPlugin(options)
If you wire Webpack manually (without the Next.js wrapper), this returns a plain WebpackPluginInstance:
import { createWebpackPlugin } from "next-intl-auto-translate"
const plugin = createWebpackPlugin({
inputFile: "./messages/en.json",
locales: ["en", "ru"],
})loadMessages(options)
Programmatic single-run entry point used by the CLI. Reads inputFile, translates anything missing, and writes target-locale files + the vocabulary.
import { loadMessages } from "next-intl-auto-translate"
await loadMessages({
inputFile: "/abs/path/messages/en.json",
locales: ["en", "ru", "fr"],
mainLocale: "en",
pathToVocabFile: "/abs/path/messages/vocabulary.json",
notes: "informal tone",
})| Option | Type | Required | Default | Description |
| ----------------------- | ------------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| inputFile | string | yes | — | Absolute path to the main-locale JSON file. |
| locales | readonly string[] | yes | — | All locales (target locales = locales minus mainLocale). |
| mainLocale | string | yes | — | The source-of-truth locale; its file is never overwritten. |
| pathToVocabFile | string | yes | — | Absolute path to the vocabulary cache. |
| notes | string | no | — | Translator notes. |
| pruneStaleVocabulary | boolean | no | true | Remove vocabulary entries whose source strings no longer exist in the main file. |
CLI
npx next-intl-auto-translate --config ./next-intl-auto-translate.config.json| Flag | Required | Default | Description |
| ---------------- | -------- | ---------------------------------------- | -------------------------------------- |
| -c, --config | no | ./next-intl-auto-translate.config.json | Path to the JSON config. |
| -h, --help | no | — | Show usage. |
OPENAI_API_KEY is read from the environment.
How it works
- The plugin reads
inputFileand extracts every string value (recursively, across nested objects and arrays). - Strings not present in
vocabulary.jsonare batched into a single call tojson-i18n-auto-translate, which preserves{placeholders}and returns a{ text → { locale → translation } }map. - The new translations are merged into
vocabulary.json, then every target locale file is rebuilt from the main content + the vocabulary. - Locale files are only rewritten when their content actually changes — no unnecessary file system churn for hot reloads.
- Stale vocabulary entries (whose source strings no longer exist in
inputFile) are pruned by default.
Dev mode (Webpack + Turbopack)
- Webpack: the plugin hooks
watchRunand re-translates only wheninputFileorvocabFilechange. Runs are debounced and de-duplicated across client/server compilations. - Turbopack: Webpack hooks don't fire, so the plugin sets up a
node:fs.watchon the same files (debounced by 1s) to get the same behavior.
In both cases, an initial translation is kicked off via setImmediate when the plugin is constructed, so the first next dev boot is already up to date.
Interop with next-intl-merge
If you also use next-intl-merge to split your messages into per-feature files, its locales array must contain only the main locale. If both plugins write ru.json to the same directory, they race during dev shutdown and the merge plugin will clobber the translated output silently.
next-intl-auto-translate checks for this at config load and throws a clear error if a sibling next-intl-merge.config.json lists any target locale.
Fallback behavior
json-i18n-auto-translate is designed to never throw — on network/API/JSON failures, every requested (text, locale) cell falls back to the original text. That means your build never breaks because of a flaky API:
- Missing translations land in
vocabulary.jsonas{ "<text>": { ru: "<text>" } }. - On the next successful run, those entries are retried (they're still considered untranslated only if absent; if you want to force a retry, delete the row in
vocabulary.json).
See also
json-i18n-auto-translate— the underlying translation primitive (CLI + library) this package is built on.next-intl— the runtime this plugin targets.
License
MIT
