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: {
// Enables usage of `useExtracted`
extract: true,
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',
// Defines which locale holds the canonical source strings
sourceLocale: 'en'
},
// Relative path(s) to source code files
srcPath: './src'
}
});
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',
messages: {
path: './messages',
format: 'po',
locales: 'infer',
sourceLocale: 'en'
}
});
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.
See also: Monorepos and external packages
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—see messages.format for configuration details.
Recommendation: As keys are auto-generated with useExtracted, it’s recommended to use PO files as they support providing more context about a message like file references and descriptions. This can be helpful for (AI) translators.
AI-based translation can be automated with a translation management system like Crowdin.
Static analysis
Message extraction relies on static analysis.
In practice, this means:
tmust receive a literal string as its message argumenttmust be called in the same function body where it was retrieved fromuseExtractedorgetExtracted
Valid usage:
import {useExtracted} from 'next-intl';
function Example({name}) {
// ✅ Makes `t` available in this component
const t = useExtracted();
// ✅ String literals are supported
t('Hello there!');
// ✅ Arguments can be used for dynamic values
t('Hello {name}!', {name});
function onClick() {
// ✅ Usage in event handlers is supported
t('You clicked the button!');
}
// ✅ Usage inside JSX is supported
return <button onClick={onClick}>{t('Click me')}</button>;
}In contrast, these are examples of patterns that are not supported:
import {useExtracted} from 'next-intl';
function Example({key}) {
const t = useExtracted();
// ❌ `key` is only known at runtime
t(key);
// ❌ Passing `t` to another function
getName(t);
}
// ❌ Re-exporting the hook
export const useExtractedExport = useExtracted;
async function AsyncExample() {
// ❌ Passing `t` to `Promise.all`
const [t] = await Promise.all([getExtracted()]);
// Note: `getExtracted` is cached internally and even
// the first invocation typically takes less than 1ms.
// Therefore, there's no need to parallelize this.
}Monorepos and external packages
Whenever your app pulls in external modules that call useExtracted, whether it’s a sibling package in your monorepo or a reusable library installed in node_modules, there are two typical setups you can choose from.
1. Don’t ship messages with the external package
Some teams use monorepos and external packages purely for organizational benefits, but there’s only one “entry point” in the sense that there’s a single Next.js app that will eventually be deployed and makes use of all packages.
Here, configure srcPath to include the external package’s source directory:
const withNextIntl = createNextIntlPlugin({
experimental: {
extract: true,
messages: {
path: './messages',
format: 'json',
locales: 'infer',
sourceLocale: 'en'
},
srcPath: [
// First-party messages
'./src',
// Sibling packages in a monorepo
'../ui/src',
// Installed packages in `node_modules`
'./node_modules/@acme/components'
]
}
});This will extract both first-party and external package messages to your main app directory.
2. Ship messages with the external package
If your shared package is used by multiple apps, you might want to reuse messages among all consumers.
For this, you can use one-time extraction via unstable_extractMessages as part of your build process to extract the shared messages:
import {unstable_extractMessages} from 'next-intl/extractor';
await unstable_extractMessages({
srcPath: './src',
messages: {
path: './messages',
format: 'po',
locales: 'infer',
sourceLocale: 'en'
}
});In the consuming app, you can configure extract.path to only extract your first-party messages, while using messages.path to include the external package’s messages:
const withNextIntl = createNextIntlPlugin({
experimental: {
// Only extract first-party messages
extract: {
path: './messages'
},
// Transform both first-party messages and external
// ones when loaded (e.g. for .po files)
messages: {
path: [
// First-party messages
'./messages',
// Sibling packages in a monorepo
'../ui/messages',
// Installed packages in `node_modules`
'./node_modules/@acme/components/messages'
],
// Depends on your preference
format: 'po',
locales: 'infer',
sourceLocale: 'en'
},
srcPath: './src'
}
});Additionally, you should use transpilePackages so that useExtracted is compiled to useTranslations in the external packages:
const nextConfig = {
// ...
transpilePackages: ['@acme/ui', '@acme/components']
};
// ...Afterwards, you can merge first-party messages with external ones in getRequestConfig:
const messages = {
...(await import(`@acme/ui/messages/${locale}.po`)).default,
...(await import(`@acme/components/messages/${locale}.po`)).default,
...(await import(`../../messages/${locale}.po`)).default
};
// ...To avoid key collisions between packages, you can consider using namespaces.