From cbf313da26f42f3aff65c9ab107384267c798b4f Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 09:22:57 +0100 Subject: [PATCH 01/16] docs: Add v4 beta blog post --- docs/src/components/StayUpdated.mdx | 1 - docs/src/pages/blog/_meta.tsx | 4 + docs/src/pages/blog/index.mdx | 6 + docs/src/pages/blog/next-intl-4-0.mdx | 253 ++++++++++++++++++++++++++ 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 docs/src/pages/blog/next-intl-4-0.mdx diff --git a/docs/src/components/StayUpdated.mdx b/docs/src/components/StayUpdated.mdx index 436d0c69d..5fc1a2720 100644 --- a/docs/src/components/StayUpdated.mdx +++ b/docs/src/components/StayUpdated.mdx @@ -2,6 +2,5 @@ **Let's keep in touch:** -- [GitHub releases](https://github.com/amannn/next-intl/releases) - [Bluesky (Jan Amann)](https://bsky.app/profile/amann.work) - [X (Jan Amann)](https://x.com/jamannnnnn) diff --git a/docs/src/pages/blog/_meta.tsx b/docs/src/pages/blog/_meta.tsx index bd0c17456..d2289e251 100644 --- a/docs/src/pages/blog/_meta.tsx +++ b/docs/src/pages/blog/_meta.tsx @@ -2,6 +2,10 @@ export default { index: { title: 'Overview' }, + 'next-intl-4-0': { + title: 'next-intl 4.0 beta', + display: 'hidden' + }, 'next-intl-3-22': { title: 'next-intl 3.22', display: 'hidden' diff --git a/docs/src/pages/blog/index.mdx b/docs/src/pages/blog/index.mdx index 9640e7360..150c6ee85 100644 --- a/docs/src/pages/blog/index.mdx +++ b/docs/src/pages/blog/index.mdx @@ -4,6 +4,12 @@ import StayUpdated from '@/components/StayUpdated.mdx'; # next-intl blog
+ Dec XX, 2024 · by Jan Amann + +After a year of feature development, this release focuses on streamlining the API surface while maintaining the lean core architecture of `next-intl`. While many major improvements were already released in [previous minor versions](/blog/next-intl-3-22), this update introduces several valuable 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`: + +1. [**Revamped augmented types**](#revamped-augmented-types) +2. [**Strictly-typed locale**](#strictly-typed-locale) +3. [**Strictly-typed ICU arguments**](#strictly-typed-icu-arguments) +4. [**GDPR compliance**](#gdpr-compliance) +5. [**Modernized build output**](#modernized-build-output) +6. [**Preparation for upcoming Next.js features**](#nextjs-future) + +Please also have a look at the [other breaking changes](#other-breaking-changes) before you [upgrade](#upgrade-now). + +## Revamped augmented types + +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. + +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: + +```tsx +// 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](/docs/workflows/typescript) 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: + +```tsx +// 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 `` 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`](/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale: + +```tsx +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: + +```tsx +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](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. + +Check it out: + +```tsx +// "Hello {name}" +t('message'); +// ^? Expected 2 arguments + +// "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 guidelines." +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: + +```tsx +t('followers', {count: 30000}); +``` + +```json +// ✖️ 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/workflows/typescript#messages-arguments) docs to learn how to enable it. + +## GDPR compliance [#gdpr-compliance] + +In order to comply with the current GDPR regulations, the following changes have been made if you're using the `next-intl` middleware for i18n routing: + +1. The locale cookie expiration has been decreased to 5 hours. +2. 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: + +```tsx +// 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](/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`). + +As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](/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. + +Learn more in the [locale cookie](/docs/routing#locale-cookie) docs. + +## Modernized build output + +The build output of `next-intl` has been modernized and now leverages the following optimizations: + +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. +2. **Modern JSX transform:** The peer dependency for React has been bumped to v17 in order to use the more efficient, modern JSX transform. +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. + +With these changes, the bundle size of `next-intl` has been reduced by ~7% ([all details](https://github.com/amannn/next-intl/pull/1470)). + +## Preparation for upcoming Next.js features [#nextjs-future] + +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 [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) for `next-intl`. + +This led to two minor changes: + +1. If you don't already have a `NextIntlClientProvider` in your app that wraps all Client Components, you now have to add one (see [PR #1541](https://github.com/amannn/next-intl/pull/1541) for details). +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). + +While the mentioned Next.js features are still under development and may change, these two 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. + +As a closing note for this section, it seems like another feature is on its way to Next.js: [`rootParams`](https://github.com/vercel/next.js/pull/72837). + +```tsx +import {unstable_rootParams as rootParams} from 'next/server'; + +async function Component() { + // The ability to read params deeply in + // Server Components ... finally! + const {locale} = await rootParams(); +} +``` + +If things go well, I think 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! + +## Other breaking changes + +1. Return type-safe messages from `useMessages` and `getMessages` (see [PR #1489](https://github.com/amannn/next-intl/pull/1489)) +2. Inherit context in case nested `NextIntlClientProvider` instances are present (see [PR #1413](https://github.com/amannn/next-intl/pull/1413)) +3. Automatically inherit formats when `NextIntlClientProvider` is rendered from a Server Component (see [PR #1191](https://github.com/amannn/next-intl/pull/1191)) +4. Require locale to be returned from `getRequestConfig` (see [PR #1486](https://github.com/amannn/next-intl/pull/1486)) +5. Disallow passing `null`, `undefined` or `boolean` as an ICU argument (see [PR #1561](https://github.com/amannn/next-intl/pull/1561)) +6. Bump minimum required typescript version to 5 for projects using TypeScript (see [PR #1481](https://github.com/amannn/next-intl/pull/1481)) +7. Remove deprecated APIs (see [PR #1479](https://github.com/amannn/next-intl/pull/1479)) +8. Remove deprecated APIs pt. 2 (see [PR #1482](https://github.com/amannn/next-intl/pull/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 +``` + +If you need help, you can refer to the [examples](/examples) which have all been updated. + +## 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](https://learn.next-intl.dev) is coming? + + From 523c0aad34e8adc622f2d544e8776cfe65cf61d2 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 09:33:46 +0100 Subject: [PATCH 02/16] reference v4 domain --- docs/src/pages/blog/next-intl-4-0.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/pages/blog/next-intl-4-0.mdx b/docs/src/pages/blog/next-intl-4-0.mdx index cd6d3d8d9..2adefd95b 100644 --- a/docs/src/pages/blog/next-intl-4-0.mdx +++ b/docs/src/pages/blog/next-intl-4-0.mdx @@ -42,7 +42,7 @@ declare module 'next-intl' { } ``` -See the updated [TypeScript augmentation](/docs/workflows/typescript) guide. +See the updated [TypeScript augmentation](https://v4.next-intl.dev/docs/workflows/typescript) guide. ## Strictly-typed locale @@ -63,7 +63,7 @@ declare module 'next-intl' { By doing so, APIs like `useLocale()` or `` 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`](/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale: +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: ```tsx import {getRequestConfig} from 'next-intl/server'; @@ -150,7 +150,7 @@ t('followers', {count: 30000}); "{count, number} followers" ``` -Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the [strict arguments](/docs/workflows/typescript#messages-arguments) docs to learn how to enable it. +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. ## GDPR compliance [#gdpr-compliance] @@ -176,11 +176,11 @@ export const routing = defineRouting({ }); ``` -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](/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`). +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`). -As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](/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. +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. -Learn more in the [locale cookie](/docs/routing#locale-cookie) docs. +Learn more in the [locale cookie](https://v4.next-intl.dev/docs/routing#locale-cookie) docs. ## Modernized build output From cd9100a0f60708eef54a1ce8771aea3827d02c1a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 09:53:29 +0100 Subject: [PATCH 03/16] mention discussion --- docs/src/pages/blog/next-intl-4-0.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/pages/blog/next-intl-4-0.mdx b/docs/src/pages/blog/next-intl-4-0.mdx index 2adefd95b..2a709b6e4 100644 --- a/docs/src/pages/blog/next-intl-4-0.mdx +++ b/docs/src/pages/blog/next-intl-4-0.mdx @@ -198,7 +198,7 @@ To ensure that the sails of `next-intl` are set for a steady course in the upcom This led to two minor changes: -1. If you don't already have a `NextIntlClientProvider` in your app that wraps all Client Components, you now have to add one (see [PR #1541](https://github.com/amannn/next-intl/pull/1541) for details). +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). 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). While the mentioned Next.js features are still under development and may change, these two 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. @@ -240,6 +240,8 @@ npm install next-intl@v4-beta If you need help, you can refer to the [examples](/examples) which have all been updated. +I'd also invite you to share your experiences with `next-intl@4.0` in the [discussions](https://github.com/amannn/next-intl/discussions/1631). + ## Thank you! I want to sincerely thank everyone who has helped to make `next-intl` what it is today. From c7c2f6604a32dc967bfd8c377fe87bb9860f0d80 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 10:32:29 +0100 Subject: [PATCH 04/16] docs: Add blog post for `rootParams` --- docs/src/pages/blog/_meta.tsx | 4 + docs/src/pages/blog/index.mdx | 6 + docs/src/pages/blog/next-intl-4-0.mdx | 6 +- docs/src/pages/blog/nextjs-root-params.mdx | 318 +++++++++++++++++++++ 4 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 docs/src/pages/blog/nextjs-root-params.mdx diff --git a/docs/src/pages/blog/_meta.tsx b/docs/src/pages/blog/_meta.tsx index d2289e251..74bed960d 100644 --- a/docs/src/pages/blog/_meta.tsx +++ b/docs/src/pages/blog/_meta.tsx @@ -2,6 +2,10 @@ export default { index: { title: 'Overview' }, + 'nextjs-root-params': { + title: 'New in Next.js 15.X: rootParams', + display: 'hidden' + }, 'next-intl-4-0': { title: 'next-intl 4.0 beta', display: 'hidden' diff --git a/docs/src/pages/blog/index.mdx b/docs/src/pages/blog/index.mdx index 150c6ee85..4f3682930 100644 --- a/docs/src/pages/blog/index.mdx +++ b/docs/src/pages/blog/index.mdx @@ -4,6 +4,12 @@ import StayUpdated from '@/components/StayUpdated.mdx'; # next-intl blog
+ Dec XX, 2024 · by Jan Amann + +(this post is still a draft) + +Next.js v15.X was just released and with it comes a new feature: [`rootParams`](https://github.com/vercel/next.js/pull/72837). + +This new API fills in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that allows apps that use top-level dynamic segments like `[locale]` to read segment values deeply in Server Components: + +```tsx +import {unstable_rootParams as rootParams} from 'next/server'; + +async function Component() { + // The ability to read params deeply in + // Server Components ... finally! + const {locale} = await rootParams(); +} +``` + +This addition is a game-changer for `next-intl`. + +While the library previously relied on workarounds to provide a locale to all Server Components, this API now provides native support in Next.js for this use case, allowing the library to integrate much tighter with Next.js. + +Practically, for users of `next-intl` this means: + +1. Being able to support static rendering of apps with i18n routing without `setRequestLocale` +2. Improved integration with the Next.js cache +3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) (although there's more work necessary here on the Next.js side) + +But first, let's have a look at how this API works in practice. + +## Root layouts + +Previously, Next.js required a [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) to be present at `app/layout.tsx`. + +Now, you can move this to a nested folder, e.g.: + +``` +src +└── app + └── [locale] + ├── layout.tsx + ├── page.tsx + └── news + ├── layout.tsx + └── page.tsx +``` + +A root layout is defined as the layout that has no pages located above it. In our example, `src/app/[locale]/layout.tsx` is the root layout. In contrast, layouts that do have pages above them are regular layouts (e.g. `src/app/[locale]/news/layout.tsx`). + +That being said, with the addition of `rootParams`, you can now read the params of a root layout in all Server Components that are located below it: + +```tsx filename=src/components/LocaleSwitcher.tsx +import {unstable_rootParams as rootParams} from 'next/server'; + +export async function LocaleSwitcher() { + // Read the value of `[locale]` + const {locale} = await rootParams(); + + // ... +} +``` + +## Multiple root layouts + +Here's where it gets interesting: Since Next.js supports [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups), you can have multiple root layouts: + +``` +src +└── app + ├── [locale] + │ ├── layout.tsx + │ └── page.tsx + └── (unlocalized) + ├── layout.tsx + └── page.tsx +``` + +The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` are on the same level, therefore both qualify as root layouts. Due to this, in this case the returned value of `rootParams` will depend on where the component that calls the function is being rendered from. + +If you call `rootParams` in shared code that is used by both root layouts, this allows for a pattern like this: + +```tsx filename="src/utils/getLocale.tsx" +import {unstable_rootParams as rootParams} from 'next/server'; + +export async function getLocale() { + // Try to read the locale in case we're in `[locale]/layout.tsx` + let {locale} = await rootParams(); + + // If we're in `(unlocalized)/layout.tsx`, let's use a fallback + if (!locale) { + locale = 'en'; + } + + return locale; +} +``` + +With this, we can use the `getLocale` function across our codebase to read the current locale without having to worry about where it's being called from. + +In an internationalized app, this can for example be useful to implement a country selection page at the root where you have to rely on a default locale. + +## Static rendering + +In case we know all possible values for the `[locale]` segment ahead of time, we can provide them to Next.js using the [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) function to enable static rendering: + +```tsx filename="src/app/[locale]/layout.tsx" +const locales = ['en', 'de']; + +// Pre-render all available locales at build time +export async function generateStaticParams() { + return locales.map((locale) => ({locale})); +} + +// ... +``` + +In combination with [`dynamicParams`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams), we can furthermore instruct Next.js to disallow values that are encountered at runtime and do not match the values we provided to `generateStaticParams`: + +```tsx filename="src/app/[locale]/layout.tsx" +// Return a 404 for any unknown locales +export const dynamicParams = false; + +// ... +``` + +## Leveraging `rootParams` in `next-intl` + +So, how can you use this feature in `next-intl`? + +Similarly to how we've defined the `getLocale` function above, we do in fact already have a shared place that is called by all server-side functions that require the current locale of the user: [`src/i18n/request.ts`](/docs/usage/configuration#server-client-components). + +Check it out: + +```tsx filename="src/i18n/request.ts" +import {unstable_rootParams as rootParams} from 'next/server'; +import {hasLocale} from 'next-intl'; +import {getRequestConfig} from 'next-intl/server'; +import {routing} from './routing'; + +export default getRequestConfig(async () => { + const params = await rootParams(); + const locale = hasLocale(routing.locales, params.locale) + ? params.locale + : routing.defaultLocale; + + return { + locale, + messages: (await import(`../../messages/${locale}.json`)).default + }; +}); +``` + +`hasLocale` is a new addition scheduled for [`next-intl@4.0`](/blog/next-intl-4-0), but practically simply checks if the provided `locales` array contains a given `locale`. If it doesn't, typically because we're not in the `[locale]` segment, a default locale is used instead. + +That's it—a single change to `src/i18n/request.ts` is all you need to do to start using `rootParams`! + +## Cleaning up + +With this change, you can now simplify your codebase in various ways: + +### Removing a pass-through root layout + +For certain patterns like global 404 pages, you might have used a pass-through root layout so far: + +```tsx filename="src/app/layout.tsx" +type Props = { + children: React.ReactNode; +}; + +export default function RootLayout({children}: Props) { + return children; +} +``` + +This needs to be removed now as otherwise this will qualify as a root layout instead of the one defined at `src/app/[locale]/layout.tsx`. + +### Avoid reading the `[locale]` segment + +Since `next-intl` provides the current locale via [`useLocale` and `getLocale`](/docs/usage/configuration#locale), you can seamlessly read the locale from these APIs instead of `params`: + +```diff filename="src/app/[locale]/layout.tsx" ++ import {getLocale} from 'next-intl/server'; + +type Props = { + children: React.ReactNode; +- params: {locale: string}; +}; + +export default async function RootLayout({ + children, +- params +}: Props) { +- const {locale} = await params; ++ const locale = await getLocale(); + + return ( + + {children} + + ); +} +``` + +Behind the scenes, if you call `useLocale` or `getLocale` in a Server Component, your `i18n/request.ts` config will be consulted, potentially using a fallback that you've defined. + +### Remove manual locale overrides [#locale-override] + +If you're using async APIs like `getTranslations`, you might have previously passed the locale manually, e.g. to enable static rendering in the Metadata API. + +Now, you can remove this and rely on the locale that is returned from `i18n/request.ts`: + +```diff filename="src/app/[locale]/page.tsx" +- type Props = { +- params: Promise<{locale: string}>; +- }; + +export async function generateMetadata( +- {params}: Props +) { +- const {locale} = await params; +- const t = await getTranslations({locale, namespace: 'HomePage'}); ++ const t = await getTranslations('HomePage'); + + // ... +} +``` + +The only case where you might still want to pass a locale to `getTranslations` is if your UI renders multiple locales in parallel. If this is the case in your app, you should make sure to accept an override in your `i18n/request.ts` config: + +```tsx filename="src/i18n/request.ts" +import {hasLocale} from 'next-intl'; +import {routing} from './routing'; + +export default getRequestConfig(async ({locale}) => { + // Use a locale based on these priorities: + // 1. An override passed to the function + // 2. A locale from the `[locale]` segment + // 3. A default locale + if (!locale) { + const params = await rootParams(); + locale = hasLocale(routing.locales, params.locale) + ? params.locale + : routing.defaultLocale; + } + + // ... +}); +``` + +### Static rendering + +If you've previously used `setRequestLocale` to enable static rendering, you can now remove it: + +```diff filename="src/[locale]/page.tsx" +- import {setRequestLocale} from 'next-intl/server'; + +- type Props = { +- params: Promise<{locale: string}>; +- } + +- export default function Page({params}: Props) { +- setRequestLocale(params.locale); ++ export default function Page() { +// ... +``` + +Note that `generateStaticParams` is naturally still required though. + +### Handling unknown locales + +Not strictly a new feature of Next.js, but in case you're using `generateStaticParams`, the easiest way to ensure that only the locales you've defined are allowed is to configure [`dynamicParams`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams) in your root layout: + +```tsx filename="src/app/[locale]/layout.tsx" +// Return a 404 for any unknown locales +export const dynamicParams = false; +``` + +If you don't use `generateStaticParams`, you can still disallow unknown locales by manually calling `notFound()` in your root layout: + +```tsx filename="src/app/[locale]/layout.tsx" +import {hasLocale} from 'next-intl'; +import {notFound} from 'next/navigation'; +import {routing} from '@/i18n/routing'; + +type Props = { + children: React.ReactNode; + params: Promise<{locale: string}>; +}; + +export default async function RootLayout({children, params}: Props) { + const {locale} = await params; + if (!hasLocale(routing.locales, locale)) { + return notFound(); + } + + // ... +} +``` + +## Try `rootParams` today! + +While this article mentions an upcoming `hasLocale` API from `next-intl@4.0` that simplifies working with `rootParams`, you can already try out the API today in the `3.0` range. + +The one rare case where a change from `next-intl@4.0` is required, is if you need to [manually pass a locale](#locale-override) to async APIs like `getTranslations` in case your UI renders multiple locales in parallel. + +In case you're trying out `rootParams` with `next-intl`, let me know how it goes by joining the discussion here: [Experiences with `rootParams`](https://github.com/amannn/next-intl/discussions/1627). I'm curious to hear how it simplifies your codebase! + +—Jan + + From 0005b917867e314aaa74d3229d4d86d680fccfc7 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 11:36:41 +0100 Subject: [PATCH 05/16] cleanup --- docs/src/pages/blog/nextjs-root-params.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index a5fe55fc6..807bab472 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -141,8 +141,8 @@ Check it out: ```tsx filename="src/i18n/request.ts" import {unstable_rootParams as rootParams} from 'next/server'; -import {hasLocale} from 'next-intl'; import {getRequestConfig} from 'next-intl/server'; +import {hasLocale} from 'next-intl'; import {routing} from './routing'; export default getRequestConfig(async () => { @@ -236,6 +236,8 @@ export async function generateMetadata( The only case where you might still want to pass a locale to `getTranslations` is if your UI renders multiple locales in parallel. If this is the case in your app, you should make sure to accept an override in your `i18n/request.ts` config: ```tsx filename="src/i18n/request.ts" +import {unstable_rootParams as rootParams} from 'next/server'; +import {getRequestConfig} from 'next-intl/server'; import {hasLocale} from 'next-intl'; import {routing} from './routing'; @@ -255,6 +257,8 @@ export default getRequestConfig(async ({locale}) => { }); ``` +This is a very rare case, so if you're unsure, you very likely don't need this. + ### Static rendering If you've previously used `setRequestLocale` to enable static rendering, you can now remove it: From 50694b492be4fc092f7563122d1bdc2d4ff620b2 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 12:21:47 +0100 Subject: [PATCH 06/16] mention `({locale})` change --- docs/src/pages/blog/next-intl-4-0.mdx | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/src/pages/blog/next-intl-4-0.mdx b/docs/src/pages/blog/next-intl-4-0.mdx index 2a709b6e4..d194bcb20 100644 --- a/docs/src/pages/blog/next-intl-4-0.mdx +++ b/docs/src/pages/blog/next-intl-4-0.mdx @@ -194,28 +194,17 @@ With these changes, the bundle size of `next-intl` has been reduced by ~7% ([all ## Preparation for upcoming Next.js features [#nextjs-future] -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 [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) for `next-intl`. +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`. -This led to two minor changes: +This led to three minor changes: 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). 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). +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) (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). -While the mentioned Next.js features are still under development and may change, these two 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. +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. -As a closing note for this section, it seems like another feature is on its way to Next.js: [`rootParams`](https://github.com/vercel/next.js/pull/72837). - -```tsx -import {unstable_rootParams as rootParams} from 'next/server'; - -async function Component() { - // The ability to read params deeply in - // Server Components ... finally! - const {locale} = await rootParams(); -} -``` - -If things go well, I think 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! +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! ## Other breaking changes From 8a23b3bc9c245b23a00d9cd757007a7a1ae76208 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 12:40:58 +0100 Subject: [PATCH 07/16] add date --- docs/src/pages/blog/index.mdx | 2 +- docs/src/pages/blog/next-intl-4-0.mdx | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/src/pages/blog/index.mdx b/docs/src/pages/blog/index.mdx index 150c6ee85..5bbe075db 100644 --- a/docs/src/pages/blog/index.mdx +++ b/docs/src/pages/blog/index.mdx @@ -7,7 +7,7 @@ import StayUpdated from '@/components/StayUpdated.mdx'; Dec XX, 2024 · by Jan Amann +Dec 23, 2024 · by Jan Amann -After a year of feature development, this release focuses on streamlining the API surface while maintaining the lean core architecture of `next-intl`. While many major improvements were already released in [previous minor versions](/blog/next-intl-3-22), this update introduces several valuable enhancements that will improve your development experience and make working with internationalization even more seamless. +After a year of feature development, this release focuses on streamlining the API surface while maintaining the core architecture of `next-intl`. With many major 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. Here's what's new in `next-intl@4.0`: @@ -20,7 +20,7 @@ Here's what's new in `next-intl@4.0`: 5. [**Modernized build output**](#modernized-build-output) 6. [**Preparation for upcoming Next.js features**](#nextjs-future) -Please also have a look at the [other breaking changes](#other-breaking-changes) before you [upgrade](#upgrade-now). +Please also have a look at the [other breaking changes](#other-breaking-changes) listed below before you upgrade. ## Revamped augmented types @@ -200,7 +200,7 @@ This led to three minor changes: 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). 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). -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) (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). +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). 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. @@ -213,7 +213,7 @@ I'm particularly excited about the announcement of `rootParams`, as it seems lik 3. Automatically inherit formats when `NextIntlClientProvider` is rendered from a Server Component (see [PR #1191](https://github.com/amannn/next-intl/pull/1191)) 4. Require locale to be returned from `getRequestConfig` (see [PR #1486](https://github.com/amannn/next-intl/pull/1486)) 5. Disallow passing `null`, `undefined` or `boolean` as an ICU argument (see [PR #1561](https://github.com/amannn/next-intl/pull/1561)) -6. Bump minimum required typescript version to 5 for projects using TypeScript (see [PR #1481](https://github.com/amannn/next-intl/pull/1481)) +6. Bump minimum required TypeScript version to 5 for projects using TypeScript (see [PR #1481](https://github.com/amannn/next-intl/pull/1481)) 7. Remove deprecated APIs (see [PR #1479](https://github.com/amannn/next-intl/pull/1479)) 8. Remove deprecated APIs pt. 2 (see [PR #1482](https://github.com/amannn/next-intl/pull/1482)) @@ -227,8 +227,6 @@ Afterwards, you can upgrade by running: npm install next-intl@v4-beta ``` -If you need help, you can refer to the [examples](/examples) which have all been updated. - I'd also invite you to share your experiences with `next-intl@4.0` in the [discussions](https://github.com/amannn/next-intl/discussions/1631). ## Thank you! From b54034a01393232b931db498a7367fa2578dbb6f Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 15:29:21 +0100 Subject: [PATCH 08/16] wording --- docs/src/pages/blog/nextjs-root-params.mdx | 36 ++++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index 807bab472..b8ed04ee9 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -26,12 +26,12 @@ async function Component() { This addition is a game-changer for `next-intl`. -While the library previously relied on workarounds to provide a locale to all Server Components, this API now provides native support in Next.js for this use case, allowing the library to integrate much tighter with Next.js. +While the library previously relied on workarounds to provide a locale to Server Components, this API now provides native support in Next.js for this use case, allowing the library to integrate much tighter with Next.js. Practically, for users of `next-intl` this means: 1. Being able to support static rendering of apps with i18n routing without `setRequestLocale` -2. Improved integration with the Next.js cache +2. Improved integration with Next.js cache mechanisms 3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) (although there's more work necessary here on the Next.js side) But first, let's have a look at how this API works in practice. @@ -40,22 +40,32 @@ But first, let's have a look at how this API works in practice. Previously, Next.js required a [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) to be present at `app/layout.tsx`. -Now, you can move this to a nested folder, e.g.: +Now, you can move these to a nested folder that can be a dynamic segment, e.g.: + +``` +src +└── app + └── [locale] + ├── layout.tsx (root layout) + └── page.tsx +``` + +A root layout is a layout that has no other layouts located above it. + +In contrast, layouts that do have other layout ancestors are regular layouts: ``` src └── app └── [locale] ├── layout.tsx - ├── page.tsx + ├── (...) └── news - ├── layout.tsx + ├── layout.tsx (regular layout) └── page.tsx ``` -A root layout is defined as the layout that has no pages located above it. In our example, `src/app/[locale]/layout.tsx` is the root layout. In contrast, layouts that do have pages above them are regular layouts (e.g. `src/app/[locale]/news/layout.tsx`). - -That being said, with the addition of `rootParams`, you can now read the params of a root layout in all Server Components that are located below it: +With the addition of `rootParams`, you can now read the param values of a root layout in all Server Components that render within it: ```tsx filename=src/components/LocaleSwitcher.tsx import {unstable_rootParams as rootParams} from 'next/server'; @@ -70,7 +80,7 @@ export async function LocaleSwitcher() { ## Multiple root layouts -Here's where it gets interesting: Since Next.js supports [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups), you can have multiple root layouts: +Here's where it gets interesting: With [route groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups), you can provide another layout for pages that are not located in the `[locale]` segment: ``` src @@ -83,7 +93,7 @@ src └── page.tsx ``` -The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` are on the same level, therefore both qualify as root layouts. Due to this, in this case the returned value of `rootParams` will depend on where the component that calls the function is being rendered from. +The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` both have no other layouts located above them, therefore both qualify as root layouts. Due to this, in this case the returned value of `rootParams` will depend on where the component that calls the function is being rendered from. If you call `rootParams` in shared code that is used by both root layouts, this allows for a pattern like this: @@ -135,9 +145,9 @@ export const dynamicParams = false; So, how can you use this feature in `next-intl`? -Similarly to how we've defined the `getLocale` function above, we do in fact already have a shared place that is called by all server-side functions that require the current locale of the user: [`src/i18n/request.ts`](/docs/usage/configuration#server-client-components). +Similarly to how we've defined the `getLocale` function above, we do in fact already have a shared place that is called by all server-side functions that require the current locale of the user: [`i18n/request.ts`](/docs/usage/configuration#server-client-components). -Check it out: +So let's use `rootParams` here: ```tsx filename="src/i18n/request.ts" import {unstable_rootParams as rootParams} from 'next/server'; @@ -160,7 +170,7 @@ export default getRequestConfig(async () => { `hasLocale` is a new addition scheduled for [`next-intl@4.0`](/blog/next-intl-4-0), but practically simply checks if the provided `locales` array contains a given `locale`. If it doesn't, typically because we're not in the `[locale]` segment, a default locale is used instead. -That's it—a single change to `src/i18n/request.ts` is all you need to do to start using `rootParams`! +That's it—a single change to `i18n/request.ts` is all you need to do to start using `rootParams`! ## Cleaning up From 28b2c7f187abc9d9581a2eedc64a10a6cba97e6a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 15:44:23 +0100 Subject: [PATCH 09/16] wording --- docs/src/pages/blog/nextjs-root-params.mdx | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index b8ed04ee9..a9b0afd78 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -194,7 +194,7 @@ This needs to be removed now as otherwise this will qualify as a root layout ins ### Avoid reading the `[locale]` segment -Since `next-intl` provides the current locale via [`useLocale` and `getLocale`](/docs/usage/configuration#locale), you can seamlessly read the locale from these APIs instead of `params`: +Since `next-intl` provides the current locale via [`useLocale` and `getLocale`](/docs/usage/configuration#locale), you can seamlessly read the locale from these APIs instead of `params` now: ```diff filename="src/app/[locale]/layout.tsx" + import {getLocale} from 'next-intl/server'; @@ -223,7 +223,7 @@ Behind the scenes, if you call `useLocale` or `getLocale` in a Server Component, ### Remove manual locale overrides [#locale-override] -If you're using async APIs like `getTranslations`, you might have previously passed the locale manually, e.g. to enable static rendering in the Metadata API. +If you're using async APIs like `getTranslations`, you might have previously passed the locale manually, typically to enable static rendering in the Metadata API. Now, you can remove this and rely on the locale that is returned from `i18n/request.ts`: @@ -243,7 +243,17 @@ export async function generateMetadata( } ``` -The only case where you might still want to pass a locale to `getTranslations` is if your UI renders multiple locales in parallel. If this is the case in your app, you should make sure to accept an override in your `i18n/request.ts` config: +The only rare case where you might still want to pass a locale to `getTranslations` is if your UI renders messages from multiple locales in parallel: + +```tsx +// Use messages from the current locale +const t = getTranslations(); + +// Use messages from 'en', regardless of what the current user locale is +const t = getTranslations({locale: 'en'}); +``` + +In this case, you should make sure to accept an override in your `i18n/request.ts` config: ```tsx filename="src/i18n/request.ts" import {unstable_rootParams as rootParams} from 'next/server'; @@ -281,9 +291,10 @@ If you've previously used `setRequestLocale` to enable static rendering, you can - } - export default function Page({params}: Props) { -- setRequestLocale(params.locale); +- setRequestLocale(params.locale); + export default function Page() { -// ... + // ... +} ``` Note that `generateStaticParams` is naturally still required though. @@ -297,7 +308,7 @@ Not strictly a new feature of Next.js, but in case you're using `generateStaticP export const dynamicParams = false; ``` -If you don't use `generateStaticParams`, you can still disallow unknown locales by manually calling `notFound()` in your root layout: +If you don't use `generateStaticParams`, you can still disallow unknown locales by manually calling `notFound()` based on `params` in your root layout: ```tsx filename="src/app/[locale]/layout.tsx" import {hasLocale} from 'next-intl'; @@ -321,7 +332,7 @@ export default async function RootLayout({children, params}: Props) { ## Try `rootParams` today! -While this article mentions an upcoming `hasLocale` API from `next-intl@4.0` that simplifies working with `rootParams`, you can already try out the API today in the `3.0` range. +While this article mentions an upcoming `hasLocale` API from `next-intl@4.0` that simplifies working with `rootParams`, you can already try out the API today even in the `3.0` range. The one rare case where a change from `next-intl@4.0` is required, is if you need to [manually pass a locale](#locale-override) to async APIs like `getTranslations` in case your UI renders multiple locales in parallel. From 0c3e628486637fcc29b652813249b34a15cf0e7a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 16:42:28 +0100 Subject: [PATCH 10/16] wording --- docs/src/pages/blog/nextjs-root-params.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index a9b0afd78..d826b625d 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -32,7 +32,7 @@ Practically, for users of `next-intl` this means: 1. Being able to support static rendering of apps with i18n routing without `setRequestLocale` 2. Improved integration with Next.js cache mechanisms -3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) (although there's more work necessary here on the Next.js side) +3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) (although there seems to be more work necessary here on the Next.js side) But first, let's have a look at how this API works in practice. @@ -113,7 +113,7 @@ export async function getLocale() { } ``` -With this, we can use the `getLocale` function across our codebase to read the current locale without having to worry about where it's being called from. +With this, you can use the `getLocale` function across your codebase to read the current locale without having to worry about where it's being called from. In an internationalized app, this can for example be useful to implement a country selection page at the root where you have to rely on a default locale. @@ -172,7 +172,7 @@ export default getRequestConfig(async () => { That's it—a single change to `i18n/request.ts` is all you need to do to start using `rootParams`! -## Cleaning up +## So much cleaner With this change, you can now simplify your codebase in various ways: From 32bdd8af6179d1a49cfdc1fea7c3e2bbb40e7c5a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 15 Jan 2025 11:53:26 +0100 Subject: [PATCH 11/16] Apply suggestions from code review --- docs/src/pages/blog/nextjs-root-params.mdx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index d826b625d..7767ac079 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -10,7 +10,7 @@ import StayUpdated from '@/components/StayUpdated.mdx'; (this post is still a draft) -Next.js v15.X was just released and with it comes a new feature: [`rootParams`](https://github.com/vercel/next.js/pull/72837). +Next.js v15.X was just released and comes with a new feature: [`rootParams`](https://github.com/vercel/next.js/pull/72837). This new API fills in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that allows apps that use top-level dynamic segments like `[locale]` to read segment values deeply in Server Components: @@ -40,7 +40,7 @@ But first, let's have a look at how this API works in practice. Previously, Next.js required a [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) to be present at `app/layout.tsx`. -Now, you can move these to a nested folder that can be a dynamic segment, e.g.: +Now, you can move a root layout to a nested folder that can be a dynamic segment, e.g.: ``` src @@ -58,14 +58,14 @@ In contrast, layouts that do have other layout ancestors are regular layouts: src └── app └── [locale] - ├── layout.tsx + ├── layout.tsx (root layout) ├── (...) └── news ├── layout.tsx (regular layout) └── page.tsx ``` -With the addition of `rootParams`, you can now read the param values of a root layout in all Server Components that render within it: +With the addition of `rootParams`, you can now read param values of a root layout in all Server Components that render within it: ```tsx filename=src/components/LocaleSwitcher.tsx import {unstable_rootParams as rootParams} from 'next/server'; @@ -100,7 +100,7 @@ If you call `rootParams` in shared code that is used by both root layouts, this ```tsx filename="src/utils/getLocale.tsx" import {unstable_rootParams as rootParams} from 'next/server'; -export async function getLocale() { +export default async function getLocale() { // Try to read the locale in case we're in `[locale]/layout.tsx` let {locale} = await rootParams(); @@ -115,7 +115,7 @@ export async function getLocale() { With this, you can use the `getLocale` function across your codebase to read the current locale without having to worry about where it's being called from. -In an internationalized app, this can for example be useful to implement a country selection page at the root where you have to rely on a default locale. +In an internationalized app, this can for example be useful to implement a country selection page at the root where you have to rely on a default locale. Once the user is within the `[locale]` segment, this param value can be used instead for localizing page content. ## Static rendering @@ -132,7 +132,7 @@ export async function generateStaticParams() { // ... ``` -In combination with [`dynamicParams`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams), we can furthermore instruct Next.js to disallow values that are encountered at runtime and do not match the values we provided to `generateStaticParams`: +In combination with [`dynamicParams`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams), we can furthermore instruct Next.js to disallow values that are encountered at runtime and do not match the values we've provided to `generateStaticParams`: ```tsx filename="src/app/[locale]/layout.tsx" // Return a 404 for any unknown locales @@ -172,7 +172,7 @@ export default getRequestConfig(async () => { That's it—a single change to `i18n/request.ts` is all you need to do to start using `rootParams`! -## So much cleaner +## Time for spring cleaning With this change, you can now simplify your codebase in various ways: @@ -219,7 +219,7 @@ export default async function RootLayout({ } ``` -Behind the scenes, if you call `useLocale` or `getLocale` in a Server Component, your `i18n/request.ts` config will be consulted, potentially using a fallback that you've defined. +Behind the scenes, if you call `useLocale` or `getLocale` in a Server Component, your `i18n/request.ts` config will be consulted, potentially using a fallback that you have defined. ### Remove manual locale overrides [#locale-override] @@ -336,7 +336,7 @@ While this article mentions an upcoming `hasLocale` API from `next-intl@4.0` tha The one rare case where a change from `next-intl@4.0` is required, is if you need to [manually pass a locale](#locale-override) to async APIs like `getTranslations` in case your UI renders multiple locales in parallel. -In case you're trying out `rootParams` with `next-intl`, let me know how it goes by joining the discussion here: [Experiences with `rootParams`](https://github.com/amannn/next-intl/discussions/1627). I'm curious to hear how it simplifies your codebase! +If you're giving `rootParams` a go with `next-intl`, let me know how it works for you by joining the discussion here: [Experiences with `rootParams`](https://github.com/amannn/next-intl/discussions/1627). I'm curious to hear how it simplifies your codebase! —Jan From 3dee38262c800fe6cfc251728182c8a83d7d5288 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 19 Feb 2025 13:16:42 +0100 Subject: [PATCH 12/16] Apply suggestions from code review --- docs/src/pages/blog/nextjs-root-params.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index 7767ac079..53b12e8ff 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -38,9 +38,9 @@ But first, let's have a look at how this API works in practice. ## Root layouts -Previously, Next.js required a [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) to be present at `app/layout.tsx`. +Previously, Next.js required a [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) to be present at `app/layout.tsx`—the root of your app. -Now, you can move a root layout to a nested folder that can be a dynamic segment, e.g.: +Now, you can move a root layout to a nested segment, which can also be dynamic: ``` src @@ -50,7 +50,7 @@ src └── page.tsx ``` -A root layout is a layout that has no other layouts located above it. +In this extended definition, a root layout now is any layout that has no other layouts located above it. In contrast, layouts that do have other layout ancestors are regular layouts: @@ -93,7 +93,9 @@ src └── page.tsx ``` -The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` both have no other layouts located above them, therefore both qualify as root layouts. Due to this, in this case the returned value of `rootParams` will depend on where the component that calls the function is being rendered from. +The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` both have no other layouts located above them, therefore both qualify as root layouts. + +Due to this, in this case the returned value of `rootParams` will depend on where the component that calls the function is being rendered from. If you call `rootParams` in shared code that is used by both root layouts, this allows for a pattern like this: From bb47cbb79751cdd174cdb7d985eeb05daa7b9af9 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 19 Feb 2025 13:18:46 +0100 Subject: [PATCH 13/16] highlight --- docs/src/pages/blog/nextjs-root-params.mdx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index 53b12e8ff..877e0b65c 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -151,7 +151,7 @@ Similarly to how we've defined the `getLocale` function above, we do in fact alr So let's use `rootParams` here: -```tsx filename="src/i18n/request.ts" +```tsx filename="src/i18n/request.ts" {7} import {unstable_rootParams as rootParams} from 'next/server'; import {getRequestConfig} from 'next-intl/server'; import {hasLocale} from 'next-intl'; @@ -275,7 +275,10 @@ export default getRequestConfig(async ({locale}) => { : routing.defaultLocale; } - // ... + return { + locale + // ... + }; }); ``` From 35c8435ad0d7fb069ba6477a3b105a6d6aba0c63 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 29 Apr 2025 11:05:38 +0200 Subject: [PATCH 14/16] 4.0 is released now --- docs/src/pages/blog/nextjs-root-params.mdx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index eb0669325..f96a8ce00 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -170,15 +170,13 @@ export default getRequestConfig(async () => { }); ``` -`hasLocale` is a new addition scheduled for [`next-intl@4.0`](/blog/next-intl-4-0), but practically simply checks if the provided `locales` array contains a given `locale`. If it doesn't, typically because we're not in the `[locale]` segment, a default locale is used instead. - That's it—a single change to `i18n/request.ts` is all you need to do to start using `rootParams`! ## Time for spring cleaning With this change, you can now simplify your codebase in various ways: -### Removing a pass-through root layout +### Remove a pass-through root layout For certain patterns like global 404 pages, you might have used a pass-through root layout so far: @@ -337,10 +335,6 @@ export default async function RootLayout({children, params}: Props) { ## Try `rootParams` today! -While this article mentions an upcoming `hasLocale` API from `next-intl@4.0` that simplifies working with `rootParams`, you can already try out the API today even in the `3.0` range. - -The one rare case where a change from `next-intl@4.0` is required, is if you need to [manually pass a locale](#locale-override) to async APIs like `getTranslations` in case your UI renders multiple locales in parallel. - If you're giving `rootParams` a go with `next-intl`, let me know how it works for you by joining the discussion here: [Experiences with `rootParams`](https://github.com/amannn/next-intl/discussions/1627). I'm curious to hear how it simplifies your codebase! —Jan From b351037a530ee0cbe1f528d264a604c90c8dc49a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 27 Aug 2025 10:28:06 +0200 Subject: [PATCH 15/16] `next/root-params` --- docs/src/pages/blog/nextjs-root-params.mdx | 70 ++++++++++++---------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index f96a8ce00..ca6a213e0 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -1,26 +1,26 @@ --- -title: 'New in Next.js 15.X: rootParams' +title: 'Using next/root-params in Next.js 15.X' --- import StayUpdated from '@/components/StayUpdated.mdx'; -# New in Next.js 15.X: `rootParams` +# Using `next/root-params` in Next.js 15.X May XX, 2025 · by Jan Amann (this post is still a draft) -Next.js v15.X was just released and comes with a new feature: [`rootParams`](https://github.com/vercel/next.js/pull/72837). +Next.js v15.X was just released and comes with a new feature: [`next/root-params`](https://github.com/vercel/next.js/pull/80255). This new API fills in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that allows apps that use top-level dynamic segments like `[locale]` to read segment values deeply in Server Components: ```tsx -import {unstable_rootParams as rootParams} from 'next/server'; +import {locale} from 'next/root-params'; async function Component() { // The ability to read params deeply in // Server Components ... finally! - const {locale} = await rootParams(); + const locale = await locale(); } ``` @@ -32,7 +32,7 @@ Practically, for users of `next-intl` this means: 1. Being able to support static rendering of apps with i18n routing without `setRequestLocale` 2. Improved integration with Next.js cache mechanisms -3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) (although there seems to be more work necessary here on the Next.js side) +3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`cacheComponents`](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) But first, let's have a look at how this API works in practice. @@ -65,14 +65,14 @@ src └── page.tsx ``` -With the addition of `rootParams`, you can now read param values of a root layout in all Server Components that render within it: +With the addition of `next/root-params`, you can now read param values of a root layout in all Server Components that render within it: ```tsx filename=src/components/LocaleSwitcher.tsx -import {unstable_rootParams as rootParams} from 'next/server'; +import {locale} from 'next/root-params'; export async function LocaleSwitcher() { // Read the value of `[locale]` - const {locale} = await rootParams(); + const curLocale = await locale(); // ... } @@ -93,25 +93,25 @@ src └── page.tsx ``` -The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` both have no other layouts located above them, therefore both qualify as root layouts. +The layout at `[locale]/layout.tsx` as well as the layout at `(unlocalized)/layout.tsx` both have no other layouts located above them, therefore _both_ qualify as root layouts. -Due to this, in this case the returned value of `rootParams` will depend on where the component that calls the function is being rendered from. +Due to this, in this case the returned value of `next/root-params` will depend on where the component that calls the function is being rendered from. -If you call `rootParams` in shared code that is used by both root layouts, this allows for a pattern like this: +If you call `next/root-params` in shared code that is used by both layouts, this allows for a pattern like this: ```tsx filename="src/utils/getLocale.tsx" -import {unstable_rootParams as rootParams} from 'next/server'; +import {locale} from 'next/root-params'; export default async function getLocale() { // Try to read the locale in case we're in `[locale]/layout.tsx` - let {locale} = await rootParams(); + let curLocale = await locale(); // If we're in `(unlocalized)/layout.tsx`, let's use a fallback - if (!locale) { - locale = 'en'; + if (!curLocale) { + curLocale = 'en'; } - return locale; + return curLocale; } ``` @@ -143,24 +143,24 @@ export const dynamicParams = false; // ... ``` -## Leveraging `rootParams` in `next-intl` +## Leveraging `next/root-params` in `next-intl` -So, how can you use this feature in `next-intl`? +So, how can you use this in `next-intl`? -Similarly to how we've defined the `getLocale` function above, we do in fact already have a shared place that is called by all server-side functions that require the current locale of the user: [`i18n/request.ts`](/docs/usage/configuration#server-client-components). +Similarly to how we've defined the `getLocale` function above, we do in fact already have a central place that is called by all server-side functions that require the current locale of the user: [`i18n/request.ts`](/docs/usage/configuration#server-client-components). -So let's use `rootParams` here: +So let's use `next/root-params` here: ```tsx filename="src/i18n/request.ts" {7} -import {unstable_rootParams as rootParams} from 'next/server'; +import * as rootParams from 'next/root-params'; import {getRequestConfig} from 'next-intl/server'; import {hasLocale} from 'next-intl'; import {routing} from './routing'; export default getRequestConfig(async () => { - const params = await rootParams(); - const locale = hasLocale(routing.locales, params.locale) - ? params.locale + const paramValue = await rootParams.locale(); + const locale = hasLocale(routing.locales, paramValue) + ? paramValue : routing.defaultLocale; return { @@ -170,7 +170,7 @@ export default getRequestConfig(async () => { }); ``` -That's it—a single change to `i18n/request.ts` is all you need to do to start using `rootParams`! +That's it—a single change to `i18n/request.ts` is all you need to start using `next/root-params`! ## Time for spring cleaning @@ -192,9 +192,11 @@ export default function RootLayout({children}: Props) { This needs to be removed now as otherwise this will qualify as a root layout instead of the one defined at `src/app/[locale]/layout.tsx`. +Instead, you can use [`global-not-found.js`](https://nextjs.org/docs/app/api-reference/file-conventions/not-found#global-not-foundjs-experimental) for this now. + ### Avoid reading the `[locale]` segment -Since `next-intl` provides the current locale via [`useLocale` and `getLocale`](/docs/usage/configuration#locale), you can seamlessly read the locale from these APIs instead of `params` now: +Since `next-intl` provides the current locale via [`useLocale` and `getLocale`](/docs/usage/configuration#use-locale), you can seamlessly read the locale from these APIs instead of `params` now: ```diff filename="src/app/[locale]/layout.tsx" + import {getLocale} from 'next-intl/server'; @@ -256,7 +258,7 @@ const t = getTranslations({locale: 'en'}); In this case, you should make sure to accept an override in your `i18n/request.ts` config: ```tsx filename="src/i18n/request.ts" -import {unstable_rootParams as rootParams} from 'next/server'; +import * as rootParams from 'next/root-params'; import {getRequestConfig} from 'next-intl/server'; import {hasLocale} from 'next-intl'; import {routing} from './routing'; @@ -267,9 +269,9 @@ export default getRequestConfig(async ({locale}) => { // 2. A locale from the `[locale]` segment // 3. A default locale if (!locale) { - const params = await rootParams(); - locale = hasLocale(routing.locales, params.locale) - ? params.locale + const paramValue = await rootParams.locale(); + locale = hasLocale(routing.locales, paramValue) + ? paramValue : routing.defaultLocale; } @@ -333,9 +335,11 @@ export default async function RootLayout({children, params}: Props) { } ``` -## Try `rootParams` today! +## Try `next/root-params` today! + +If you're giving `next/root-params` a go with `next-intl`, let me know how it works for you by joining the discussion here: [Experiences with `next/root-params`](https://github.com/amannn/next-intl/discussions/1627). -If you're giving `rootParams` a go with `next-intl`, let me know how it works for you by joining the discussion here: [Experiences with `rootParams`](https://github.com/amannn/next-intl/discussions/1627). I'm curious to hear how it simplifies your codebase! +I'm curious to hear how it simplifies your codebase! —Jan From c15fcb1d0bf1881f29c7811c19232655dd2b6a8a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 27 Aug 2025 10:46:39 +0200 Subject: [PATCH 16/16] custom routing --- docs/src/pages/blog/nextjs-root-params.mdx | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/src/pages/blog/nextjs-root-params.mdx b/docs/src/pages/blog/nextjs-root-params.mdx index ca6a213e0..e05d5400e 100644 --- a/docs/src/pages/blog/nextjs-root-params.mdx +++ b/docs/src/pages/blog/nextjs-root-params.mdx @@ -335,6 +335,40 @@ export default async function RootLayout({children, params}: Props) { } ``` +### Custom routing setups + +While `next-intl` provides mechanisms like [`localePrefix`](/docs/routing/configuration#localeprefix) (and esp. [`prefixes`](/docs/routing/configuration#locale-prefix-prefixes)) that allow you to customize your [routing configuration](/docs/routing), the introduction of `next/root-params` now makes the usage of middleware optional. + +This in turn enables you to use a completely custom routing setup, while still being able to core APIs from `next-intl` like `useTranslations`. + +**Example:** + +``` +app/ +└── [tenant] + ├── layout.tsx + └── page.tsx +``` + +```tsx filename="src/i18n/request.ts" +import * as rootParams from 'next/root-params'; +import {getRequestConfig} from 'next-intl/server'; +import {fetchTenant} from '@/services/tenant'; + +export default getRequestConfig(async () => { + const tenantId = await rootParams.tenant(); + const tenant = await fetchTenant(tenantId); + const locale = tenant.locale; + + return { + locale, + messages: (await import(`../../messages/${locale}.json`)).default + }; +}); +``` + +In this case, you can consider implementing your own [middleware](/docs/routing/middleware) and [navigation APIs](/docs/routing/navigation) if relevant. + ## Try `next/root-params` today! If you're giving `next/root-params` a go with `next-intl`, let me know how it works for you by joining the discussion here: [Experiences with `next/root-params`](https://github.com/amannn/next-intl/discussions/1627).