From 0825f0858a8452fce5d133b89a91023299975a39 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Dec 2024 11:35:58 +0100 Subject: [PATCH] feat: Re-introduce `locale` argument for `getRequestConfig` to be used for overriding the locale (#1625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **tldr;** — Do you use i18n routing and have you already switched to [`await requestLocale`](https://next-intl.dev/blog/next-intl-3-22#await-request-locale) in `getRequestConfig`? If yes, you can skip this. --- ### Deprecation of `locale` in favor of `await requestLocale` In [next-intl 3.22](https://next-intl.dev/blog/next-intl-3-22), the `locale` argument that was passed to `getRequestConfig` was deprecated in favor of [`await requestLocale`](https://next-intl.dev/blog/next-intl-3-22#await-request-locale): ```diff // i18n/request.ts export default function getRequestConfig(async ({ - locale + requestLocale }) => { + const locale = await requestLocale; // ... })); ``` This change was done in preparation for Next.js 15 where reading from headers [became async](https://nextjs.org/blog/next-15#async-request-apis-breaking-change). If you're using i18n routing, please upgrade to `requestLocale` now. ### Preview: `rootParams` are coming to Next.js Now, with [`rootParams`](https://github.com/vercel/next.js/pull/72837) being on the horizon, this API will allow you to read a locale without receiving any param passed to `getRequestConfig`: ```tsx // 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; // ... }); ``` Among other simplifications, this allows to remove manual overrides like this that were merely done for enabling static rendering: ```diff - 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'); // ... } ``` However, in some rare cases, you might want to render messages from multiple locales on the same page: ```tsx // Use messages from 'en', regardless of what the current user locale is const t = getTranslations({locale: 'en'}); ``` If you're using this pattern, you'll be able to accept the overridden locale in `getRequestConfig` as follows: ```tsx // 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 quite an edge case, but this use case will remain supported via the re-introduced `locale` argument. Note that `await requestLocale` considers a potential locale override, therefore the `locale` argument will only be relevant once `rootParams` are a thing. I hope to have more to share on this in the future! --- .../next-intl/src/server/react-server/getConfig.tsx | 2 ++ .../src/server/react-server/getRequestConfig.tsx | 9 ++++++++- packages/use-intl/src/core/hasLocale.test.tsx | 11 ++++++++++- packages/use-intl/src/core/hasLocale.tsx | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/next-intl/src/server/react-server/getConfig.tsx b/packages/next-intl/src/server/react-server/getConfig.tsx index d3daa45d8..4b07ff97b 100644 --- a/packages/next-intl/src/server/react-server/getConfig.tsx +++ b/packages/next-intl/src/server/react-server/getConfig.tsx @@ -38,6 +38,8 @@ See also: https://next-intl.dev/docs/usage/configuration#i18n-request } const params: GetRequestConfigParams = { + locale: localeOverride, + // In case the consumer doesn't read `params.locale` and instead provides the // `locale` (either in a single-language workflow or because the locale is // read from the user settings), don't attempt to read the request locale. diff --git a/packages/next-intl/src/server/react-server/getRequestConfig.tsx b/packages/next-intl/src/server/react-server/getRequestConfig.tsx index d52612e1c..d96097c18 100644 --- a/packages/next-intl/src/server/react-server/getRequestConfig.tsx +++ b/packages/next-intl/src/server/react-server/getRequestConfig.tsx @@ -1,4 +1,4 @@ -import type {IntlConfig} from 'use-intl/core'; +import type {IntlConfig, Locale} from 'use-intl/core'; export type RequestConfig = Omit & { /** @@ -8,6 +8,13 @@ export type RequestConfig = Omit & { }; export type GetRequestConfigParams = { + /** + * If you provide an explicit locale to an async server-side function like + * `getTranslations({locale: 'en'})`, it will be passed via `locale` to + * `getRequestConfig` so you can use it instead of the segment value. + */ + locale?: Locale; + /** * Typically corresponds to the `[locale]` segment that was matched by the middleware. * diff --git a/packages/use-intl/src/core/hasLocale.test.tsx b/packages/use-intl/src/core/hasLocale.test.tsx index 713260179..36c611f3b 100644 --- a/packages/use-intl/src/core/hasLocale.test.tsx +++ b/packages/use-intl/src/core/hasLocale.test.tsx @@ -1,4 +1,4 @@ -import {it} from 'vitest'; +import {expect, it} from 'vitest'; import hasLocale from './hasLocale.tsx'; it('narrows down the type', () => { @@ -24,3 +24,12 @@ it('can be called with a non-matching narrow candidate', () => { candidate satisfies never; } }); + +it('can be called with any candidate', () => { + const locales = ['en-US', 'en-GB'] as const; + expect(hasLocale(locales, 'unknown')).toBe(false); + expect(hasLocale(locales, undefined)).toBe(false); + + // Relevant since `ParamValue` in Next.js includes `string[]` + expect(hasLocale(locales, ['de'])).toBe(false); +}); diff --git a/packages/use-intl/src/core/hasLocale.tsx b/packages/use-intl/src/core/hasLocale.tsx index 5bb68714c..576ee3289 100644 --- a/packages/use-intl/src/core/hasLocale.tsx +++ b/packages/use-intl/src/core/hasLocale.tsx @@ -7,7 +7,7 @@ import type {Locale} from './AppConfig.tsx'; */ export default function hasLocale( locales: ReadonlyArray, - candidate?: string | null + candidate: unknown ): candidate is LocaleType { return locales.includes(candidate as LocaleType); }