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 5bbe075db..af7f9b3c0 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 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: + +```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 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 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) + +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 a root layout 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 (root layout) + ├── (...) + └── news + ├── layout.tsx (regular layout) + └── 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: + +```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: 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 +└── 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` 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: + +```tsx filename="src/utils/getLocale.tsx" +import {unstable_rootParams as rootParams} from 'next/server'; + +export default 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, 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. Once the user is within the `[locale]` segment, this param value can be used instead for localizing page content. + +## 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've 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: [`i18n/request.ts`](/docs/usage/configuration#server-client-components). + +So let's use `rootParams` here: + +```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'; + +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 `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 + +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` now: + +```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 have defined. + +### Remove manual locale overrides [#locale-override] + +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`: + +```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 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'; +import {getRequestConfig} from 'next-intl/server'; +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; + } + + // ... +}); +``` + +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: + +```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()` based on `params` 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 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 + +