useExtracted (experimental)
As an alternative to managing namespaces and keys manually, next-intl provides an additional API that works similar to useTranslations but automatically extracts messages from your source files.
import {useExtracted} from 'next-intl';
function InlineMessages() {
const t = useExtracted();
return <h1>{t('Look ma, no keys!')}</h1>;
}Extraction integrates automatically with next dev and next build via a Turbo- or Webpack loader, you don’t need to manually trigger it.
When the above file is compiled, this will:
- Extract the inline message with an automatically assigned key to your source locale:
{
"VgH3tb": "Look ma, no keys!"
}- Keeps target locales in sync by either adding empty entries or removing outdated ones:
{
"VgH3tb": ""
}- Compiles the file to replace
useExtractedwithuseTranslations
import {useTranslations} from 'next-intl';
function InlineMessages() {
const t = useTranslations();
return <h1>{t('VgH3tb')}</h1>;
}Links:
Getting started
This API is currently experimental, and needs to be enabled in next.config.ts:
import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin({
experimental: {
// Relative path(s) to source files
srcPath: './src',
extract: {
// Defines which locale to extract to
sourceLocale: 'en'
},
messages: {
// Relative path to the directory
path: './messages',
// Either 'json', 'po', or a custom format (see below)
format: 'json',
// Either 'infer' to automatically detect locales based on
// matching files in `path` or an explicit array of locales
locales: 'infer'
}
}
});
const config: NextConfig = {};
export default withNextIntl(config);With this, every time you call next dev or next build, messages will be extracted as they are discovered and your messages will be kept in sync.
See createNextIntlPlugin for details.
Can I extract messages manually?
While message extraction is designed to seamlessly integrate with your development workflow based on running next dev and next build, you can also extract messages manually:
import {unstable_extractMessages} from 'next-intl/extractor';
await unstable_extractMessages({
srcPath: './src',
sourceLocale: 'en',
messages: {
path: './messages',
format: 'json',
locales: 'infer'
}
});
console.log('✔ Messages extracted');This can be useful when you’re developing a package like a component library, where you don’t have a Next.js dev server running and you want to provide messages along with the package.
Inline messages
ICU messages
All ICU features you are familiar with from useTranslations are supported and can be used as usual:
// Interpolation of arguments
t('Hello {name}!', {name: 'Jane'});// Cardinal pluralization
t(
'You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}.',
{count: 3580}
);// Ordinal pluralization
t(
"It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!",
{year: 22}
);// Select values
t('{gender, select, female {She is} male {He is} other {They are}} online.', {
gender: 'female'
});// Rich text
t.rich('Please refer to the <link>guidelines</link>.', {
link: (chunks) => <Link href="/guidelines">{chunks}</Link>
});The one exception is t.raw, this feature is not intended to be used with message extraction.
Descriptions
In order to provide more context about a message for (AI) translators, you can provide descriptions:
<button onClick={onSlideRight}>
{t({
message: 'Right',
description: 'Advance to the next slide'
})}
</button>Explicit IDs
If you want to use an explicit ID instead of the auto-generated one, you can optionally provide one:
<button onClick={onSlideRight}>
{t({
id: 'carousel.next',
message: 'Right'
})}
</button>This can be useful when you have a label that is used in multiple places, but should have different translations in other languages. This is an escape hatch that should rarely be necessary.
Namespaces
If you want to organize your messages under a specific namespace, you can pass it to useExtracted:
function Modal() {
const t = useExtracted('design-system');
return (
<>
<button>{t('Close')}</button>
...
</>
);
}This will extract messages associated with a call to t to the given namespace:
{
"design-system": {
"5VpL9Z": "Close"
}
}Namespaces are useful in situations like:
- Libraries: If you have multiple packages in a monorepo, you can merge messages from different packages into a single catalog and avoid key collisions between packages.
- Splitting: If you want to pass only certain messages to the client side, this can help to group them accordingly (e.g.
<NextIntlClientProvider messages={messages.client}>).
It’s a good idea to not overuse namespaces, as they can make moving messages between components more difficult if this involves refactoring the namespace.
await getExtracted()
For usage in async functions like Server Components, Metadata and Server Actions, you use an asynchronous variant from next-intl/server:
import {getExtracted} from 'next-intl/server';
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getExtracted();
return (
<PageLayout title={t('Hello {name}!', {name: user.name})}>
<UserDetails user={user} />
</PageLayout>
);
}Optional compilation
While message extraction is primarily designed to be used with a running Next.js app, useExtracted works perfectly fine without being compiled into useTranslations. In this case, the inline message will be used directly instead of being replaced with a translation key.
This can for example be useful for tests:
import {expect, it} from 'vitest';
import {NextIntlClientProvider} from 'next-intl';
import {renderToString} from 'react-dom/server';
function Component() {
const t = useExtracted();
return t('Hello {name}!', {name: 'Jane'});
}
it('renders', () => {
const html = renderToString(
// No need to pass any messages
<NextIntlClientProvider locale="en" timeZone="UTC">
<Component />
</NextIntlClientProvider>
);
// ✅ The inline message will be used
expect(html).toContain('Hello Jane!');
});Formats
Messages can be extracted as JSON, PO, or with custom file formats.
When messages is configured, this will also set up a Turbo- or Webpack loader that will ensure imported messages can be used as plain JavaScript objects.
For example, when using format: 'po', messages can be imported as:
import {cookies} from 'next/headers';
import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async () => {
const locale = 'en';
// E.g. `[{"NhX4DJ": "Hello"}]`
const messages = (await import(`../../messages/${locale}.po`)).default;
// ...
});JSON format
When using this option, your messages will look like this:
{
"NhX4DJ": "Hello"
}Note that JSON files can only hold pairs of keys and values. To provide more context about a message like file references and descriptions, it’s therefore recommended to use PO files instead. Alternatively, you can create a custom format to store additional metadata.
For local editing of JSON messages, you can use e.g. a VSCode integration like i18n Ally:
Tip: AI-based translation can be automated with a translation management system like Crowdin.
PO format
When using this option, your messages will look like this:
#. Advance to the next slide
#: src/components/Carousel.tsx:13
msgid "5VpL9Z"
msgstr "Right"Besides the message key and the label itself, this format also supports optional descriptions and file references to all modules that consume this message.
For local editing of .po files, you can use e.g. a tool like Poedit (note however that replacing keys with source text requires a pro license).
Tip: AI-based translation can be automated with a translation management system like Crowdin.
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.