next-intl 4.0 beta
Dec 23, 2024 · by Jan AmannAfter a year of feature development, this release focuses on streamlining the API surface while maintaining the core architecture of next-intl
. With many improvements already released in previous minor versions, this update introduces several enhancements that will improve your development experience and make working with internationalization even more seamless.
Here’s what’s new in next-intl@4.0
:
- Revamped augmented types
- Strictly-typed locale
- Strictly-typed ICU arguments
- GDPR compliance
- Modernized build output
- Preparation for upcoming Next.js features
Please also have a look at the other breaking changes listed below before you upgrade.
Revamped augmented types
After type-safe Formats
was added in next-intl@3.20
, it became clear that a new API was needed that centralizes the registration of augmented types.
With next-intl@4.0
, both Messages
as well as Formats
can now be registered under a single type that is scoped to next-intl
and no longer affects the global scope:
// global.d.ts
import {formats} from '@/i18n/request';
import en from './messages/en.json';
declare module 'next-intl' {
interface AppConfig {
Messages: typeof en;
Formats: typeof formats;
}
}
See the updated TypeScript augmentation guide.
Strictly-typed locale
Building on the new type augmentation mechanism, next-intl@4.0
now allows you to strictly type locales across your app:
// global.d.ts
import {routing} from '@/i18n/routing';
declare module 'next-intl' {
interface AppConfig {
// ...
Locale: (typeof routing.locales)[number];
}
}
By doing so, APIs like useLocale()
or <Link />
that either return or receive a locale
will now pick up your app-specific Locale
type, improving type safety across your app.
To simplify narrowing of string
-based locales, a hasLocale
function has been added. This can for example be used in i18n/request.ts
to return a valid locale:
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
// Typically corresponds to the `[locale]` segment
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
Furthermore, the Locale
type can be imported into your app code in case you’re passing a locale to another function and want to ensure type safety:
import {Locale} from 'next-intl';
async function getPosts(locale: Locale) {
// ...
}
Note that strictly-typing the Locale
is optional and can be used as desired in case you wish to have additional guardrails in your app.
Strictly-typed ICU arguments
How type-safe can your app be?
The quest to bring type safety to the last corner of next-intl
has led me down a rabbit hole with the discovery of an ICU parser by Marco Schumacher—written entirely in types. Marco kindly published his implementation for usage in next-intl
, with me only adding support for rich tags on top.
Check it out:
// "Hello {name}"
t('message', {});
// ^? {name: string}
// "It's {today, date, long}"
t('message', {});
// ^? {today: Date}
// "Page {page, number} out of {total, number}"
t('message', {});
// ^? {page: number, total: number}
// "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}."
t('message', {});
// ^? {count: number}
// "Country: {country, select, US {United States} CA {Canada} other {Other}}"
t('message', {});
// ^? {country: 'US' | 'CA' | (string & {})}
// "Please refer to the <link>guidelines</link>."
t('message', {});
// ^? {link: (chunks: ReactNode) => ReactNode}
With this type inference in place, you can now use autocompletion in your IDE to get suggestions for the available arguments of a given ICU message and catch potential errors early.
This also addresses one of my favorite pet peeves:
t('followers', {count: 30000});
// ✖️ Would be: "30000 followers"
"{count} followers"
// ✅ Valid: "30,000 followers"
"{count, number} followers"
Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the strict arguments docs to learn how to enable it.
GDPR compliance
In order to comply with the current GDPR regulations, the following changes have been made and are relevant to you if you’re using the next-intl
middleware for i18n routing:
- The locale cookie has been changed to a session cookie that expires when a browser is closed.
- The locale cookie is now only set when a user switches to a locale that doesn’t match the
accept-language
header.
If you want to increase the cookie expiration, e.g. because you’re informing users about the usage of cookies or if GDPR doesn’t apply to your app, you can use the maxAge
attribute to do so:
// i18n/routing.tsx
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localeCookie: {
// Expire in one year
maxAge: 60 * 60 * 24 * 365
}
});
Since the cookie is now only available after a locale switch, make sure to not rely on it always being present. E.g. if you need access to the user’s locale in a Route Handler, a reliable option is to provide the locale as a search param (e.g. /api/posts/12?locale=en
).
As part of this change, disabling a cookie now requires you to set localeCookie: false
in your routing configuration. Previously, localeDetection: false
ambiguously also disabled the cookie from being set, but since a separate localeCookie
option was introduced recently, this should now be used instead.
Learn more in the locale cookie docs.
Modernized build output
The build output of next-intl
has been modernized and now leverages the following optimizations:
- ESM-only: To enable enhanced tree-shaking and align with the modern JavaScript ecosystem,
next-intl
is now ESM-only. The only exception isnext-intl/plugin
which is published both as CommonJS as well as ESM, due tonext.config.js
still being popular. - Modern JSX transform: The peer dependency for React has been bumped to v17 in order to use the more efficient, modern JSX transform.
- Modern syntax: Syntax is now compiled down to the Browserslist
defaults
query, which is a shortcut for “>0.5%, last 2 versions, Firefox ESR, not dead”—a baseline that is considered a reasonable target for modern apps.
With these changes, the bundle size of next-intl
has been reduced by ~7% (all details).
Preparation for upcoming Next.js features
To ensure that the sails of next-intl
are set for a steady course in the upcoming future, I’ve investigated the implications of upcoming Next.js features like ppr
, dynamicIO
and rootParams
for next-intl
.
This led to three minor changes:
- If you don’t already have a
NextIntlClientProvider
in your app that wraps all Client Components that usenext-intl
, you now have to add one (see PR #1541 for details). - If you’re using
format.relativeTime
in Client Components, you may need to provide thenow
argument explicitly now (see PR #1536 for details). - If you’re using i18n routing, make sure you’ve updated to
await requestLocale
that was introduced innext-intl@3.22
. The previously deprecatedlocale
argument will serve an edge case in the future oncerootParams
is a thing (see PR #1625 for details).
While the mentioned Next.js features are still under development and may change, these changes seem reasonable to me in any case—and ideally will be all that’s necessary to adapt for next-intl
to get the most out of these upcoming capabilities.
I’m particularly excited about the announcement of rootParams
, as it seems like this will finally fill in the missing piece that enables apps with i18n routing to support static rendering without workarounds like setRequestLocale
. I hope to have more to share on this soon!
Other breaking changes
- Return type-safe messages from
useMessages
andgetMessages
(see PR #1489) - Inherit context in case nested
NextIntlClientProvider
instances are present (see PR #1413) - Automatically inherit formats when
NextIntlClientProvider
is rendered from a Server Component (see PR #1191) - Require locale to be returned from
getRequestConfig
(see PR #1486) - Disallow passing
null
,undefined
orboolean
as an ICU argument (see PR #1561) - Bump minimum required TypeScript version to 5 for projects using TypeScript (see PR #1481)
- Remove deprecated APIs (see PR #1479)
- Remove deprecated APIs pt. 2 (see PR #1482)
Upgrade now
For a smooth upgrade, please initially upgrade to the latest v3.x version and check for deprecation warnings.
Afterwards, you can upgrade by running:
npm install next-intl@v4-beta
The beta docs are available here: v4.next-intl.dev
I’d love to hear about your experiences with next-intl@4.0
! Join the conversation in the discussions.
Thank you!
I want to sincerely thank everyone who has helped to make next-intl
what it is today.
A special thank you goes to Crowdin, the primary sponsor of next-intl
, enabling me to regularly work on this project and provide it as a free and open-source library for everyone.
—Jan
PS: Have you heard that learn.next-intl.dev is coming?
Let’s keep in touch: