useExtracted
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' or 'po'
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} male {He} other {They}} is 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!');
});Formatters
Currently, messages can be extracted as either JSON or PO files. Support for custom formatters is planned for a future release.
When messages is configured, this will also set up a Turbo- or Webpack loader that will ensure loaded messages can be imported 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 formatter
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. Another alternative will be to use a custom JSON formatter in the future.
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 formatter
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 messages, you can use e.g. a tool like Poedit (replacing keys with source text requires a pro license).
Tip: AI-based translation can be automated with a translation management system like Crowdin.