next-intl 3.22: Incrementally moving forward
Oct 21, 2024 · by Jan AmannOver the past few months, a number of minor releases have been rolled out, each providing incremental improvements to next-intl
. And while each of these releases has been an improvement on its own, they’ve also been part of a larger progression towards a more unified and streamlined API surface in general.
Today, another minor release was published that marks the final step of this progression: next-intl@3.22
.
While this release is fully backwards-compatible, it includes some modern alternatives to existing APIs. Therefore, it’s a good time to take a moment to review the changes and consider migrating where relevant.
Recent improvements:
defineRouting
: Type-safe, centralized routing configuration (introduced inv3.18
)i18n/request.ts
: Streamlined file organization (introduced inv3.19
)await requestLocale
: Preparation for Next.js 15 (introduced inv3.22
)createNavigation
: Streamlined navigation APIs (introduced inv3.22
)setRequestLocale
: Static rendering (marked as stable inv3.22
)defaultTranslationValues
: Deprecated in favor of a user-land pattern (introduced inv3.22
)
Let’s take a look at these changes in more detail.
defineRouting
Previously, configuration that was shared among the middleware and the navigation APIs required special care to be kept in sync. With the introduction of defineRouting
, this configuration can now be centralized in a type-safe manner:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en-US', 'en-GB'],
defaultLocale: 'en-US',
localePrefix: {
mode: 'always',
prefixes: {
'en-US': '/us',
'en-GB': '/uk'
}
},
pathnames: {
'/': '/',
'/organization': {
'en-US': '/organization',
'en-GB': '/organisation'
}
}
});
This routing
config is typically defined in a central place like i18n/routing.ts
and can be used where previously individual configuration was required:
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
// ...
If you’ve used defineRouting
previously to v3.22 and have provided middleware options as a second argument to createMiddleware
, you can now pass these to defineRouting
instead.
The docs have been consistently updated to reflect these changes and also suggest the creation of navigation APIs directly in i18n/routing.ts
for simplicity. If you prefer to keep your navigation APIs separately, that’s of course fine as well.
i18n/request.ts
If you’re using i18n routing, you’ll typically end up with two configuration files for next-intl
:
- Routing configuration (defined via
defineRouting
) - Request configuration (defined via
getRequestConfig
)
While (2) was historically suggested at i18n.ts
, the default is now changing to i18n/request.ts
in order to streamline the organization of files and to communicate the purpose of this file more clearly:
└── src
└── i18n
├── routing.ts (1)
└── request.ts (2)
Due to this, i18n/request.ts
is now considered the new default and is suggested throughout the docs.
If you prefer to use a custom location instead, you can do so by providing it in next.config.mjs
:
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./somewhere/else/request.ts');
// ...
await requestLocale
in getRequestConfig
Next.js 15 is on the horizon and introduces a change for request APIs to turn async. In preparation for this change, the locale
that is passed to getRequestConfig
has been replaced by requestLocale
with some minor differences:
requestLocale
needs to be awaited- The result can be
undefined
, therefore requiring a fallback - The
locale
should now also be returned fromgetRequestConfig
+ import {routing} from './i18n/routing';
export default getRequestConfig(async ({
- locale
+ requestLocale
}) => {
+ // This typically corresponds to the `[locale]` segment
+ let locale = await requestLocale;
- // Validate that the incoming `locale` parameter is valid
- if (!routing.locales.includes(locale as any)) notFound();
+ // Ensure that the incoming locale is valid
+ if (!locale || !routing.locales.includes(locale as any)) {
+ locale = routing.defaultLocale;
+ }
return {
+ locale,
// ...
};
});
While slightly more verbose, this change allows you to return a fallback locale for edge cases where the middleware can’t provide a locale (e.g. a global country selection page at /
or a global 404 page).
Also note that it’s considered good practice to return a valid locale here in case any unknown values are encountered in the [locale]
segment. However, you may wish to add validation in a central place like your root layout to conditionally call notFound()
in this case.
createNavigation
The newly added createNavigation
function supersedes these previously provided APIs:
createSharedPathnamesNavigation
createLocalizedPathnamesNavigation
This new function is a reimplementation of the existing navigation functionality and unifies the API for both use cases, while also fixing a few quirks of the previous APIs. Additionally, this implementation has been updated in anticipation of Next.js 15 to run without warnings.
Usage
import {createNavigation} from 'next-intl/navigation';
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting(/* ... */);
export const {Link, redirect, usePathname, useRouter} =
createNavigation(routing);
Migrating to createNavigation
createNavigation
is generally considered a drop-in replacement, but a few changes might be necessary:
createNavigation
is expected to receive your complete routing configuration. Ideally, you define this via thedefineRouting
function and pass the result tocreateNavigation
. The one edge case is if you’re locales aren’t known at build time.- If you’ve used
createLocalizedPathnamesNavigation
and have composed theLink
with itshref
prop, you should no longer provide the genericPathname
type argument.
- ComponentProps<typeof Link<Pathname>>
+ ComponentProps<typeof Link>
- If you’ve used
redirect
, you now have to provide an explicit locale (even if it’s just the current locale). The previously passed href (whether it was a string or an object) now needs to be wrapped in an object and assigned to thehref
prop. This change was necessary in preparation for Next.js 15.
// Retrieving the current locale
// ... in regular components:
const locale = useLocale();
// ... in async components:
const locale = await getLocale();
- redirect('/about')
+ redirect({href: '/about', locale})
- redirect({pathname: '/users/[id]', params: {id: 2}})
+ redirect({href: {pathname: '/users/[id]', params: {id: 2}}, locale})
- If you’ve used
getPathname
and have previously manually prepended a locale prefix, you should no longer do so—getPathname
now takes care of this depending on your routing strategy.
- '/'+ locale + getPathname(/* ... */)
+ getPathname(/* ... */);
- If you’re using a combination of
localePrefix: 'as-needed'
&domains
and you’re usinggetPathname
, you now need to provide adomain
argument (see Special case: Usingdomains
withlocalePrefix: 'as-needed'
)
setRequestLocale
marked as stable
In case you rely on static rendering, you might have used the unstable_setRequestLocale
API before. This function has now been marked as stable since it will likely remain required for the foreseeable future.
- import {unstable_setRequestLocale} from 'next-intl/server';
+ import {setRequestLocale} from 'next-intl/server';
Close to a year ago, I opened discussion #58862 in the Next.js repository. This was an attempt at starting a conversation about how Next.js could provide a way to access a user locale in Server Components without a tradeoff in ergonomics or rendering implications. While the issue has gained in popularity and currently ranks as #2 of the top upvoted discussions of the past year, I’ve unfortunately not been able to get a response from the Next.js team on this topic so far. Based on my understanding, it’s certainly not an easy problem to solve, but I’d be more than happy to collaborate on this if I can.
While I’m still optimistic that we can make the setRequestLocale
API obsolete at some point in the future, the “unstable” prefix doesn’t seem to be justified anymore—especially since the API has been known to work reliably since its introduction.
defaultTranslationValues
(deprecated)
defaultTranslationValues
allow you to share global values to be used in messages across your app. The most common case are shared rich text elements (e.g. b: (chunks) => <b>{chunks}</b>
).
However, over time this feature has shown drawbacks:
- We can’t serialize them automatically across the RSC boundary (see #611)
- They get in the way of type-safe arguments (see #410)
Due to this, the feature will be deprecated and the docs now suggest a better alternative:
import {useTranslations} from 'next-intl';
import RichText from '@/components/RichText';
function AboutPage() {
const t = useTranslations('AboutPage');
return <RichText>{(tags) => t.rich('description', tags)}</RichText>;
}
What’s next?
These release notes were originally planned to be posted as part of the next major version. However, I’m really happy that it was possible to roll out all of these changes incrementally and without breaking changes.
All of these changes were inspired by many conversations with users and contributors of next-intl
. It’s quite a privileged position to be in, being able to receive feedback from so many of you, and ensuring the spectrum of i18n use cases can be served by a lean, cohesive package. A big thank you also goes out to everyone who helped to test pre-releases of this version.
With these changes out of the way, users can upgrade to modern APIs at their own pace in the v3 range. That being said, the 4.0 release is already in the works, but mostly aims to clean up deprecated functionality to ensure next-intl
remains minimalistic.
—Jan
Let’s keep in touch: