next-intl-utils
v2.1.1
Published
Next-intl utilities to split translation files properly.
Maintainers
Readme
next-intl-utils
Split next-intl dictionaries into small index.json files and merge them back into locale files for development, type checking, and build-time consumption.
Why
Large translation files become hard to maintain when everything lives in a single en.json or vi.json.
This package lets you organize messages like this:
src/i18n/dictionaries/
en/
common/
index.json
home/
index.json
vi/
common/
index.json
home/
index.jsonAnd generate merged locale files like:
src/i18n/dictionaries/
en.json
vi.jsonInstallation
npm install next-intl-utils next-intlFolder structure
The package looks for files named index.json under each locale directory, or [feature].json directly.
Example:
src/i18n/dictionaries/
en/
common/
index.json
auth.json
vi/
common/
index.json
auth.jsonsrc/i18n/dictionaries/en/common/index.json
{
"title": "Hello",
"description": "Welcome back"
}src/i18n/dictionaries/en/auth.json
{
"signIn": "Sign in"
}Merged output:
src/i18n/dictionaries/en.json
{
"common": {
"title": "Hello",
"description": "Welcome back"
},
"auth": {
"signIn": "Sign in"
}
}Quick start with Next.js
1. Add the plugin in next.config.ts
import createNextIntlSplitPlugin from "next-intl-utils/plugin";
const withNextIntl = createNextIntlSplitPlugin({
dictionariesPath: "./src/i18n/dictionaries",
i18nPathOrConfig: "./src/i18n/request.ts"
});
export default withNextIntl({});What this does:
- Merges locale files once when Next starts.
- Generates a split-message loader file for edge/production use (see below).
- Watches your split
index.jsonfiles in development. - Regenerates merged locale files and the split-message loader when translations change.
2. Load messages in your next-intl request config
import {getRequestConfig} from "next-intl/server";
import {loadI18nTranslations} from "next-intl-utils";
export default getRequestConfig(async ({locale}) => ({
locale,
messages: loadI18nTranslations(
"./src/i18n/dictionaries",
locale,
process.env.NODE_ENV === "development"
)
}));enableTypeCheck is optional. When enabled, the package refreshes merged locale files during load so next-intl can keep type information aligned with your split dictionaries.
Edge/production usage (Cloudflare Workers, OpenNext, edge runtimes)
loadI18nTranslations uses Node fs, which is unavailable on edge/serverless runtimes. A common workaround is importing the generated merged locale JSON directly (await import(\./dictionaries/${locale}.json`)), but that pulls in every locale through a single dynamic import, and combined with next-intl-utils's own fs` usage, it causes both the merged JSON and the split source JSON to end up in the Worker bundle — wasting compressed size and risking Cloudflare's 3 MiB Free plan limit.
The plugin (see step 1 above) generates a split-message loader file at splitLoaderPath (default: a generated/ folder next to dictionariesPath, e.g. ./src/i18n/dictionaries → ./src/i18n/generated/split-loader.ts). This file contains one literal import() per namespace JSON file, so bundlers (webpack, Turbopack, esbuild) resolve and code-split each file individually — no fs, no runtime-templated paths, and no merged JSON in the graph.
createNextIntlSplitPlugin({
dictionariesPath: "./src/i18n/dictionaries",
i18nPathOrConfig: "./src/i18n/request.ts",
splitLoaderPath: "./src/i18n/generated/split-loader.ts" // optional, this is the default
});Use the generated file's loadSplitMessages in getRequestConfig:
import {getRequestConfig} from "next-intl/server";
import {loadSplitMessages} from "@/i18n/generated/split-loader";
export default getRequestConfig(async ({requestLocale}) => {
const locale = await requestLocale; // your existing locale resolution logic
return {
locale,
messages: await loadSplitMessages(locale)
};
});Note: the generated file imports next-intl-utils/edge (not next-intl-utils), and its import("...json") calls require resolveJsonModule: true in your tsconfig.json — already the default in most Next.js starter configs. Treat the generated file like the merged en.json/vi.json: don't hand-edit it, and only commit it if your workflow needs it in CI.
API
next-intl-utils
loadI18nTranslations(dictionariesPath, locale, enableTypeCheck?)
Loads a single locale as nested next-intl messages.
import {loadI18nTranslations} from "next-intl-utils";
const messages = loadI18nTranslations("./src/i18n/dictionaries", "en", true);Arguments:
dictionariesPath: path to the dictionaries folder.locale: locale key like"en"or"vi".enableTypeCheck: optional boolean to refresh merged locale files while loading.
loadMessages(dictionariesPath)
Loads all locales from a split dictionaries folder.
import {loadMessages} from "next-intl-utils";
const messages = loadMessages("./src/i18n/dictionaries");next-intl-utils/plugin
createNextIntlSplitPlugin({dictionariesPath, i18nPathOrConfig, splitLoaderPath?})
Creates a next-intl plugin wrapper with automatic merging, split-loader generation, and file watching.
import createNextIntlSplitPlugin from "next-intl-utils/plugin";Arguments:
dictionariesPath: relative path to the split dictionaries folder.i18nPathOrConfig: same argument you would pass tonext-intl/plugin.splitLoaderPath: optional path for the generated edge/production loader file. Defaults to agenerated/folder next todictionariesPath, e.g../src/i18n/dictionaries→./src/i18n/generated/split-loader.ts.
next-intl-utils/edge
createSplitMessagesLoader(loaders)
Fs-free factory used by the generated split-loader file. You normally don't call this directly — the plugin-generated file already wires it up (see Edge/production usage).
import {createSplitMessagesLoader} from "next-intl-utils/edge";
const loadSplitMessages = createSplitMessagesLoader({
en: {
common: () => import("./dictionaries/en/common/index.json")
}
});
const messages = await loadSplitMessages("en");loaders is { [locale]: { [namespacePath]: () => import(...) } }, where namespacePath is a /-joined key (e.g. "business/job/detail" becomes messages.business.job.detail). Calling the returned function with a locale that has no entry in loaders throws an Error listing the locales that are available.
next-intl-utils/generate-split-loader
generateSplitLoader(dictionariesPath, outputPath)
Manually (re)generates the split-loader file without going through the Next.js plugin — useful for CI scripts or non-Next.js build pipelines.
import {generateSplitLoader} from "next-intl-utils/generate-split-loader";
generateSplitLoader("./src/i18n/dictionaries", "./src/i18n/generated/split-loader.ts");next-intl-utils/merge
mergeMessages(pathToDictionaries, options?)
Manually merges split dictionaries into top-level locale JSON files.
import {mergeMessages} from "next-intl-utils/merge";
mergeMessages("./src/i18n/dictionaries");
// mergeMessages("./src/i18n/dictionaries", { muteLogs: true });Important:
- This function expects a relative path starting with
./. - Logs only when locale JSON files actually change. Pass
{ muteLogs: true }to silence output. - The Next.js plugin mutes logs automatically when
NODE_ENV === "production".
next-intl-utils/write
writeMessages(dictionariesPath, messages, options?)
Writes merged locale JSON files to disk.
import {writeMessages} from "next-intl-utils/write";next-intl-utils/load
This entrypoint exports the same loading helpers:
loadI18nTranslationsloadMessages
Development behavior
The plugin is designed to be safe in Next.js development mode:
- Watches only split source files named
index.json. - Avoids rewriting locale files when content has not changed.
- Uses a singleton watcher to avoid duplicated watchers during Next reloads.
- Cleans up the watcher on process exit.
Notes
mergeMessagesrequires a relative path such as./src/i18n/dictionaries.- The package merges based on folder names, so directory nesting becomes nested message keys.
- Generated files such as
en.jsonandvi.jsonshould usually be committed only if your workflow needs them in CI or production.
License
MIT
