Skip to content
Blognext-intl 3.22

next-intl 3.22: Incrementally moving forward

Oct 21, 2024 · by Jan Amann

Over 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:

  1. defineRouting: Type-safe, centralized routing configuration (introduced in v3.18)
  2. i18n/request.ts: Streamlined file organization (introduced in v3.19)
  3. await requestLocale: Preparation for Next.js 15 (introduced in v3.22)
  4. createNavigation: Streamlined navigation APIs (introduced in v3.22)
  5. setRequestLocale: Static rendering (marked as stable in v3.22)
  6. defaultTranslationValues: Deprecated in favor of a user-land pattern (introduced in v3.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:

i18n/routing.ts
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:

middleware.ts
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:

  1. Routing configuration (defined via defineRouting)
  2. 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:

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:

  1. requestLocale needs to be awaited
  2. The result can be undefined, therefore requiring a fallback
  3. The locale should now also be returned from getRequestConfig
+ 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:

  1. createSharedPathnamesNavigation
  2. 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

i18n/routing.ts
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:

  1. createNavigation is expected to receive your complete routing configuration. Ideally, you define this via the defineRouting function and pass the result to createNavigation. The one edge case is if you’re locales aren’t known at build time.
  2. If you’ve used createLocalizedPathnamesNavigation and have composed the Link with its href prop, you should no longer provide the generic Pathname type argument.
- ComponentProps<typeof Link<Pathname>>
+ ComponentProps<typeof Link>
  1. 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 the href 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})
  1. 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(/* ... */);
  1. If you’re using a combination of localePrefix: 'as-needed' & domains and you’re using getPathname, you now need to provide a domain argument (see Special case: Using domains with localePrefix: '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:

  1. We can’t serialize them automatically across the RSC boundary (see #611)
  2. 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

Stay in the loop: