|
| 1 | +--- |
| 2 | +title: next-intl 4.0 beta |
| 3 | +--- |
| 4 | + |
| 5 | +import PartnerContentLink from '@/components/PartnerContentLink'; |
| 6 | +import StayUpdated from '@/components/StayUpdated.mdx'; |
| 7 | + |
| 8 | +# next-intl 4.0 beta |
| 9 | + |
| 10 | +<small>Dec 23, 2024 · by Jan Amann</small> |
| 11 | + |
| 12 | +After 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](/blog/next-intl-3-22), this update introduces several enhancements that will improve your development experience and make working with internationalization even more seamless. |
| 13 | + |
| 14 | +Here's what's new in `next-intl@4.0`: |
| 15 | + |
| 16 | +1. [**Revamped augmented types**](#revamped-augmented-types) |
| 17 | +2. [**Strictly-typed locale**](#strictly-typed-locale) |
| 18 | +3. [**Strictly-typed ICU arguments**](#strictly-typed-icu-arguments) |
| 19 | +4. [**GDPR compliance**](#gdpr-compliance) |
| 20 | +5. [**Modernized build output**](#modernized-build-output) |
| 21 | +6. [**Improved inheritance in `NextIntlClientProvider`**](#nextintlclientprovider-inheritance) |
| 22 | +7. [**Preparation for upcoming Next.js features**](#nextjs-future) |
| 23 | + |
| 24 | +Please also have a look at the [other breaking changes](#other-breaking-changes) listed below before you upgrade. |
| 25 | + |
| 26 | +## Revamped augmented types |
| 27 | + |
| 28 | +After type-safe [`Formats`](/docs/usage/configuration#formats) was added in `next-intl@3.20`, it became clear that a new API was needed that centralizes the registration of augmented types. |
| 29 | + |
| 30 | +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: |
| 31 | + |
| 32 | +```tsx |
| 33 | +// global.d.ts |
| 34 | + |
| 35 | +import {formats} from '@/i18n/request'; |
| 36 | +import en from './messages/en.json'; |
| 37 | + |
| 38 | +declare module 'next-intl' { |
| 39 | + interface AppConfig { |
| 40 | + Messages: typeof en; |
| 41 | + Formats: typeof formats; |
| 42 | + } |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +See the updated [TypeScript augmentation](https://v4.next-intl.dev/docs/workflows/typescript) guide. |
| 47 | + |
| 48 | +## Strictly-typed locale |
| 49 | + |
| 50 | +Building on the new type augmentation mechanism, `next-intl@4.0` now allows you to strictly type locales across your app: |
| 51 | + |
| 52 | +```tsx |
| 53 | +// global.d.ts |
| 54 | + |
| 55 | +import {routing} from '@/i18n/routing'; |
| 56 | + |
| 57 | +declare module 'next-intl' { |
| 58 | + interface AppConfig { |
| 59 | + // ... |
| 60 | + Locale: (typeof routing.locales)[number]; |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +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. |
| 66 | + |
| 67 | +To simplify narrowing of `string`-based locales, a `hasLocale` function has been added. This can for example be used in [`i18n/request.ts`](https://v4.next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale: |
| 68 | + |
| 69 | +```tsx |
| 70 | +import {getRequestConfig} from 'next-intl/server'; |
| 71 | +import {hasLocale} from 'next-intl'; |
| 72 | +import {routing} from './routing'; |
| 73 | + |
| 74 | +export default getRequestConfig(async ({requestLocale}) => { |
| 75 | + // Typically corresponds to the `[locale]` segment |
| 76 | + const requested = await requestLocale; |
| 77 | + const locale = hasLocale(routing.locales, requested) |
| 78 | + ? requested |
| 79 | + : routing.defaultLocale; |
| 80 | + |
| 81 | + return { |
| 82 | + locale, |
| 83 | + messages: (await import(`../../messages/${locale}.json`)).default |
| 84 | + }; |
| 85 | +}); |
| 86 | +``` |
| 87 | + |
| 88 | +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: |
| 89 | + |
| 90 | +```tsx |
| 91 | +import {Locale} from 'next-intl'; |
| 92 | + |
| 93 | +async function getPosts(locale: Locale) { |
| 94 | + // ... |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +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. |
| 99 | + |
| 100 | +## Strictly-typed ICU arguments |
| 101 | + |
| 102 | +How type-safe can your app be? |
| 103 | + |
| 104 | +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](https://github.com/schummar)—written entirely in types. Marco kindly published his implementation for usage in `next-intl`, with me only adding support for rich tags on top. |
| 105 | + |
| 106 | +Check it out: |
| 107 | + |
| 108 | +```tsx |
| 109 | +// "Hello {name}" |
| 110 | +t('message', {}); |
| 111 | +// ^? {name: string} |
| 112 | + |
| 113 | +// "It's {today, date, long}" |
| 114 | +t('message', {}); |
| 115 | +// ^? {today: Date} |
| 116 | + |
| 117 | +// "Page {page, number} out of {total, number}" |
| 118 | +t('message', {}); |
| 119 | +// ^? {page: number, total: number} |
| 120 | + |
| 121 | +// "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}." |
| 122 | +t('message', {}); |
| 123 | +// ^? {count: number} |
| 124 | + |
| 125 | +// "Country: {country, select, US {United States} CA {Canada} other {Other}}" |
| 126 | +t('message', {}); |
| 127 | +// ^? {country: 'US' | 'CA' | (string & {})} |
| 128 | + |
| 129 | +// "Please refer to the <link>guidelines</link>." |
| 130 | +t('message', {}); |
| 131 | +// ^? {link: (chunks: ReactNode) => ReactNode} |
| 132 | +``` |
| 133 | + |
| 134 | +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. |
| 135 | + |
| 136 | +This also addresses one of my favorite pet peeves: |
| 137 | + |
| 138 | +```tsx |
| 139 | +t('followers', {count: 30000}); |
| 140 | +``` |
| 141 | + |
| 142 | +```json |
| 143 | +// ✖️ Would be: "30000 followers" |
| 144 | +"{count} followers" |
| 145 | + |
| 146 | +// ✅ Valid: "30,000 followers" |
| 147 | +"{count, number} followers" |
| 148 | +``` |
| 149 | + |
| 150 | +Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the [strict arguments](https://v4.next-intl.dev/docs/workflows/typescript#messages-arguments) docs to learn how to enable it. |
| 151 | + |
| 152 | +## GDPR compliance [#gdpr-compliance] |
| 153 | + |
| 154 | +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: |
| 155 | + |
| 156 | +1. The locale cookie has been changed to a session cookie that expires when a browser is closed. |
| 157 | +2. The locale cookie is now only set when a user switches to a locale that doesn't match the `accept-language` header. |
| 158 | + |
| 159 | +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: |
| 160 | + |
| 161 | +```tsx |
| 162 | +// i18n/routing.tsx |
| 163 | + |
| 164 | +import {defineRouting} from 'next-intl/routing'; |
| 165 | + |
| 166 | +export const routing = defineRouting({ |
| 167 | + // ... |
| 168 | + |
| 169 | + localeCookie: { |
| 170 | + // Expire in one year |
| 171 | + maxAge: 60 * 60 * 24 * 365 |
| 172 | + } |
| 173 | +}); |
| 174 | +``` |
| 175 | + |
| 176 | +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](https://v4.next-intl.dev/docs/environments/actions-metadata-route-handlers#route-handlers), a reliable option is to provide the locale as a search param (e.g. `/api/posts/12?locale=en`). |
| 177 | + |
| 178 | +As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](https://v4.next-intl.dev/docs/routing#locale-cookie) 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. |
| 179 | + |
| 180 | +Learn more in the [locale cookie](https://v4.next-intl.dev/docs/routing#locale-cookie) docs. |
| 181 | + |
| 182 | +## Modernized build output |
| 183 | + |
| 184 | +The build output of `next-intl` has been modernized and now leverages the following optimizations: |
| 185 | + |
| 186 | +1. **ESM-only:** To enable enhanced tree-shaking and align with the modern JavaScript ecosystem, `next-intl` is now ESM-only. The only exception is `next-intl/plugin` which is published both as CommonJS as well as ESM, due to `next.config.js` still being popular. |
| 187 | +2. **Modern JSX transform:** The peer dependency for React has been bumped to v17 in order to use the more efficient, modern JSX transform. |
| 188 | +3. **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. |
| 189 | + |
| 190 | +With these changes, the bundle size of `next-intl` has been reduced by ~7% ([all details](https://github.com/amannn/next-intl/pull/1470)). |
| 191 | + |
| 192 | +## Improved inheritance of `NextIntlClientProvider` [#nextintlclientprovider-inheritance] |
| 193 | + |
| 194 | +Previously, [`NextIntlClientProvider`](/docs/usage/configuration#nextintlclientprovider) would conservatively inherit only a subset from `i18n/request.ts`. |
| 195 | + |
| 196 | +To improve the getting started experience, the provider now also inherits: |
| 197 | + |
| 198 | +- `messages` ([PR #1682](https://github.com/amannn/next-intl/pull/1682)) |
| 199 | +- `formats` ([PR #1191](https://github.com/amannn/next-intl/pull/1191)) |
| 200 | + |
| 201 | +Due to this, you can now remove these props from `NextIntlClientProvider` if you've previously passed them manually: |
| 202 | + |
| 203 | +```diff |
| 204 | +<NextIntlClientProvider |
| 205 | +- messages={messages} |
| 206 | +- formats={formats} |
| 207 | +> |
| 208 | + {/* ... */} |
| 209 | +</NextIntlClientProvider> |
| 210 | +``` |
| 211 | + |
| 212 | +With this, `NextIntlClientProvider` now inherits all of your configuration, with the minor exception of [error handling functions](/docs/usage/configuration#error-handling). Since functions are not serializable, they cannot be passed across the server/client boundary. However, [an alternative](https://github.com/amannn/next-intl/issues/1285) for this is also on the horizon. |
| 213 | + |
| 214 | +To make it easier to work with error handling functions on the client side, `NextIntlClientProvider` can now also be used in a nested fashion and will inherit the configuration from a parent provider ([PR #1413](https://github.com/amannn/next-intl/pull/1413)). |
| 215 | + |
| 216 | +## Preparation for upcoming Next.js features [#nextjs-future] |
| 217 | + |
| 218 | +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`](https://nextjs.org/docs/app/api-reference/next-config-js/ppr), [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) and [`rootParams`](https://github.com/vercel/next.js/pull/72837) for `next-intl`. |
| 219 | + |
| 220 | +This led to three minor changes: |
| 221 | + |
| 222 | +1. If you don't already have a `NextIntlClientProvider` in your app that wraps all Client Components that use `next-intl`, you now have to add one (see [PR #1541](https://github.com/amannn/next-intl/pull/1541) for details). |
| 223 | +2. If you're using `format.relativeTime` in Client Components, you may need to provide the `now` argument explicitly now (see [PR #1536](https://github.com/amannn/next-intl/pull/1536) for details). |
| 224 | +3. If you're using i18n routing, make sure you've updated to [`await requestLocale`](https://next-intl.dev/blog/next-intl-3-22#await-request-locale) that was introduced in `next-intl@3.22`. The previously deprecated `locale` argument will serve an edge case in the future once `rootParams` is a thing (see [PR #1625](https://github.com/amannn/next-intl/pull/1625/) for details). |
| 225 | + |
| 226 | +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. |
| 227 | + |
| 228 | +I'm particularly excited about the announcement of `rootParams`, as it seems like this will finally fill in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that enables apps with i18n routing to support static rendering without workarounds like `setRequestLocale`. I hope to have more to share on this soon! |
| 229 | + |
| 230 | +## Other breaking changes |
| 231 | + |
| 232 | +1. Return type-safe messages from `useMessages` and `getMessages` (see [PR #1489](https://github.com/amannn/next-intl/pull/1489)) |
| 233 | +2. Require locale to be returned from `getRequestConfig` (see [PR #1486](https://github.com/amannn/next-intl/pull/1486)) |
| 234 | +3. Disallow passing `null`, `undefined` or `boolean` as an ICU argument (see [PR #1561](https://github.com/amannn/next-intl/pull/1561)) |
| 235 | +4. Bump minimum required TypeScript version to 5 for projects using TypeScript (see [PR #1481](https://github.com/amannn/next-intl/pull/1481)) |
| 236 | +5. Remove deprecated APIs (see [PR #1479](https://github.com/amannn/next-intl/pull/1479)) |
| 237 | +6. Remove deprecated APIs pt. 2 (see [PR #1482](https://github.com/amannn/next-intl/pull/1482)) |
| 238 | + |
| 239 | +## Upgrade now |
| 240 | + |
| 241 | +For a smooth upgrade, please initially upgrade to the latest v3.x version and check for deprecation warnings. |
| 242 | + |
| 243 | +Afterwards, you can upgrade by running: |
| 244 | + |
| 245 | +``` |
| 246 | +npm install next-intl@v4-beta |
| 247 | +``` |
| 248 | + |
| 249 | +The beta docs are available here: [v4.next-intl.dev](https://v4.next-intl.dev) |
| 250 | + |
| 251 | +I'd love to hear about your experiences with `next-intl@4.0`! Join the conversation in the [discussions](https://github.com/amannn/next-intl/discussions/1631). |
| 252 | + |
| 253 | +## Thank you! |
| 254 | + |
| 255 | +I want to sincerely thank everyone who has helped to make `next-intl` what it is today. |
| 256 | + |
| 257 | +A special thank you goes to <PartnerContentLink href="https://crowdin.com/">Crowdin</PartnerContentLink>, 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. |
| 258 | + |
| 259 | +—Jan |
| 260 | + |
| 261 | +PS: Have you heard that [learn.next-intl.dev](https://learn.next-intl.dev) is coming? |
| 262 | + |
| 263 | +<StayUpdated /> |
0 commit comments