Next.js plugin (createNextIntlPlugin)
When setting up next-intl for the App Router, you’ll add next-intl/plugin to your Next.js config.
At the minimum, it will look like this:
import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);For customization, you can provide options to the plugin.
requestConfig
By default, next-intl will look for a file called i18n/request.ts which returns request-specific configuration. This file is searched for both in the src folder as well as in the project root with the extensions .ts, .tsx, .js and .jsx.
If you prefer to move this file somewhere else, you can provide a path to the plugin:
const withNextIntl = createNextIntlPlugin(
// Specify a custom path here
'./somewhere/else/request.ts'
);Or if you’re combining this with other options, you can use the requestConfig option:
const withNextIntl = createNextIntlPlugin({
requestConfig: './somewhere/else/request.ts'
});experimental
For early adopters, the Next.js plugin provides various experimental options that let you try out new features before they’re released as stable.
createMessagesDeclaration
To enable type-safe message arguments, you can point the createMessagesDeclaration option to a sample messages file in order to create a strict declaration file for it.
const withNextIntl = createNextIntlPlugin({
experimental: {
// Provide the path to the messages that you're using in `AppConfig`
createMessagesDeclaration: './messages/en.json'
}
// ...
});See TypeScript augmentation to learn more about this.
Note: This is not necessary when using useExtracted.
srcPath
Relative path(s) to your app source code that uses next-intl
// Using a `src` folder
srcPath: './src',// Not using a `src` folder
srcPath: './',// Monorepo with multiple packages
srcPath: ['./src', '../ui'],// External dependency on a package
srcPath: ['./src', './node_modules/@acme/components'],This is required when extract is enabled.
extract
This enables the usage of useExtracted to automatically extract messages from source files.
const withNextIntl = createNextIntlPlugin({
experimental: {
extract: true
// ...
}
});Note: The extract option should be used together with messages and srcPath.
extract.path
Defines the directory where extracted messages should be written to (defaults to messages.path):
extract: {
path: './messages'
},It’s only required to configure this property when messages.path is an array.
See also: Monorepos and external packages.
messages
This defines where messages for locales are stored and how they’re loaded.
const withNextIntl = createNextIntlPlugin({
experimental: {
messages: {
path: './messages',
format: 'json',
// Optional
precompile: true
}
}
});Configuring experimental.messages sets up a Turbo- or Webpack loader that allows messages to be imported as plain JavaScript objects (see messages.format).
messages.path
Relative path to the directory containing messages:
// Load and transform messages from here
path: './messages',If you’re loading messages from external packages, you can include multiple paths:
// Multiple directories with messages
path: ['./messages', '../packages/ui/messages'],See also: Monorepos and external packages
messages.locales
Defines which locales you translate between in your messages.path.
When using useExtracted, this defines which messages are kept in sync when changes to your messages.sourceLocale are detected (e.g. en.json, de.json, …).
You can either automatically detect all locales within path:
// Detect available locales in `path`
locales: 'infer',… specify them explicitly (e.g. to use a subset):
// Extract to these locales
locales: ['en', 'de', 'fr'],… or pass an empty array to not extract any messages:
// Don't extract any messages
locales: [],In the latter case, you can run extraction manually via unstable_extractMessages.
messages.sourceLocale
Your primary locale, which might be used to define messages in source code when using useExtracted.
// Source messages will be extracted here
sourceLocale: 'en',messages.format
Defines how your catalogs are stored (e.g. 'json', 'po', or a custom format).
JSON format
When using this option, your messages might look like this:
{
"greeting": "Hello"
}… or in case of useExtracted, with an auto-generated key:
{
"NhX4DJ": "Hello"
}For local editing of JSON messages, you can use e.g. a VSCode integration like i18n Ally.
Note that JSON files can only hold pairs of keys and values. To provide more context about a message like file references and descriptions, you can use PO files or create a custom format to store additional metadata.
PO format
When using this option, your messages will look like this:
#. Advance to the next slide
#: src/components/Carousel.tsx
msgid "carousel.next"
msgstr "Right"… or in case of useExtracted, with an auto-generated key:
#. Advance to the next slide
#: src/components/Carousel.tsx
msgid "5VpL9Z"
msgstr "Right"Besides the message key and the label itself, this format also supports optional descriptions and file references for modules that consume this message.
For local editing of .po files, you can use e.g. a tool like Poedit.
Note: The default PO format emits path-only references (no line numbers) to avoid noisy diffs. If you’d like to store line numbers as well, you can create a custom format that emits this format.
Custom format
To configure a custom format, you need to specify a codec along with an extension.
The codec can be created via defineCodec from next-intl/extractor:
import {defineCodec} from 'next-intl/extractor';
export default defineCodec(() => ({
decode(content, context) {
// ...
},
encode(messages, context) {
// ...
},
toJSONString(content, context) {
// ...
}
}));Then, reference it in your configuration along with an extension:
const withNextIntl = createNextIntlPlugin({
experimental: {
messages: {
format: {
codec: './CustomCodec.ts',
extension: '.json'
}
// ...
}
}
});See also the built-in codecs for inspiration, as well as the supplied types and JSDoc reference.
Node.js supports native TypeScript execution like it’s needed for the example above, starting with v22.18. If you’re on an older version, you should define your codec as a JavaScript file.
messages.precompile
As a performance optimization, you can achieve smaller bundles and faster message formatting at runtime by precompiling messages ahead of time during the build:
const withNextIntl = createNextIntlPlugin({
experimental: {
messages: {
path: './messages',
format: 'json',
precompile: true
}
// ...
}
});Based on the provided options, message will now be precompiled when imported into your app:
// ✅ Will be pre-processed by a Turbo- or Webpack loader
const messages = (await import(`../../messages/en.json`)).default;See also: Ahead-of-time compilation with next-intl
Note: t.raw is not supported with precompiled messages (see tradeoffs)
How can I manually precompile messages?
For cases where you don’t import messages into your app (e.g. when fetching them at runtime), you can manually precompile them using icu-minify/compile:
import compile from 'icu-minify/compile';
import {getRequestConfig} from 'next-intl/server';
type Messages = Record<string, unknown>;
function compileMessages(messages: Messages): Messages {
return Object.fromEntries(
Object.entries(messages).map(([key, value]) => {
if (value && typeof value === 'object') {
return [key, compileMessages(value as Messages)];
}
if (typeof value === 'string') {
return [key, compile(value)];
}
throw new Error(`Unexpected message: ${typeof value}`);
})
);
}
export default getRequestConfig(async () => {
const response = await fetch('https://cdn.example.com/messages/en.json');
const messages = (await response.json()) as Messages;
const compiled = compileMessages(messages);
return {
messages: compiled
// ...
};
});If you do this, be sure to use the same version of icu-minify as the one that your next-intl version depends on to ensure compatibility.