Skip to content

Commit

Permalink
feat: Invoke notFound() when no locale was attached to the request …
Browse files Browse the repository at this point in the history
…and update docs to suggest validating the locale in `i18n.ts` (#742)

Users are currently struggling with errors that are caused by these two
scenarios:
1. An invalid locale was passed to `next-intl` APIs (e.g.
[#736](#736))
2. No locale is available during rendering (e.g.
[#716](#716))

**tldr:**
1. We now suggest to validate the incoming `locale` in
[`i18n.ts`](https://next-intl-docs.vercel.app/docs/usage/configuration#i18nts).
This change is recommended to all users.
2. `next-intl` will call the `notFound()` function when the middleware
didn't run on a localized request and `next-intl` APIs are used during
rendering. Previously this would throw an error, therefore this is only
relevant for you in case you've encountered [the corresponding
error](https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale).
Make sure you provide a relevant [`not-found`
page](https://next-intl-docs.vercel.app/docs/environments/error-files#not-foundjs)
that can be rendered in this case.

---

**Handling invalid locales**

Users run into this error because the locale wasn't sufficiently
validated. This is in practice quite hard because we currently educate
in the docs that this should happen in [the root
layout](https://next-intl-docs.vercel.app/docs/getting-started/app-router#applocalelayouttsx),
but due to the parallel rendering capabilities of Next.js, potentially a
page or `generateMetadata` runs first.

Therefore moving this validation to a more central place seems
necessary. Due to this, the docs will now suggest validating the locale
in `i18n.ts`. By doing this, we can catch erroneous locale arguments in
a single place before e.g. importing JSON files.

The only edge case is if an app uses no APIs of `next-intl` in Server
Components at all and therefore `i18n.ts` doesn't run. This should be a
very rare case though as even `NextIntlClientProvider` will call
`i18n.ts`. The only case to run into this is if you're using
`NextIntlClientProvider` in a Client Component and delegate all i18n
handling to Client Components too. If you have such an app, `i18n.ts`
will not be invoked and you should validate the `locale` before passing
it to `NextIntlClientProvider`.

**Handling missing locales**

This warning is probably one of the most annoying errors that users
currently run into:

```
Unable to find next-intl locale because the middleware didn't run on this request.
```

The various causes of this error are outlined in [the
docs](https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale).

Some of these cases should simply be 404s (e.g. when your middleware
matcher doesn't match `/unknown.txt`), while others require a fix in the
matcher (e.g. considering `/users/jane.doe` when using `localePrefix:
'as-necessary'`).

My assumption is that many of these errors are false positives that are
caused by the `[locale]` segment acting as a catch-all. As a result, a
500 error is encountered instead of 404s. Due to this, this PR will
downgrade the previous error to a dev-only warning and will invoke the
`notFound()` function. This should help in the majority of cases. Note
that you should define [a `not-found`
file](https://next-intl-docs.vercel.app/docs/environments/error-files#not-foundjs)
to handle this case.

I think this change is a good idea because if you're using
`unstable_setRequestLocale` and you have a misconfigured middleware
matcher, you can provide any kind of string to `next-intl` (also
`unknown.txt`) and not run into this error. Therefore it only affects
users with dynamic rendering. Validating the locale in `i18n.ts` is the
solution to either case (see above). Also in case something like
[`routeParams`](#663) gets
added to Next.js, the current warning will be gone entirely—therefore
tackling it from a different direction is likely a good idea.

The false negatives of this should hopefully be rather small as we
consistently point out that you need to adapt your middleware matcher
when switching the `localePrefix` to anything other than `always`.
Dev-only warnings should help to get further information for these
requests.

---

Closes #736
Closes #716
Closes #446
  • Loading branch information
amannn committed Dec 21, 2023
1 parent b9e17a9 commit e6d9878
Show file tree
Hide file tree
Showing 25 changed files with 282 additions and 201 deletions.
42 changes: 21 additions & 21 deletions docs/pages/docs/environments/error-files.mdx
Expand Up @@ -45,23 +45,23 @@ After this change, all requests that are matched within the `[locale]` segment w

### Catching non-localized requests

When the user requests a route that is not matched by [the `next-intl` middleware](/docs/routing/middleware), there's no locale associated with the request (depending on your `matcher` config, e.g. `/unknown.txt` might not be matched).
When the user requests a route that is not matched by [the `next-intl` middleware](/docs/routing/middleware), there's no locale associated with the request (depending on your [`matcher` config](/docs/routing/middleware#matcher-config), e.g. `/unknown.txt` might not be matched).

You can add a root `not-found` page to handle these cases too.

```tsx filename="app/not-found.tsx"
'use client';

import {redirect, usePathname} from 'next/navigation';

// Can be imported from a shared config
const defaultLocale = 'en';
import Error from 'next/error';

export default function NotFound() {
const pathname = usePathname();

// Add a locale prefix to show a localized not found page
redirect(`/${defaultLocale}${pathname}`);
return (
<html lang="en">
<body>
<Error statusCode={404} />
</body>
</html>
);
}
```

Expand All @@ -75,26 +75,26 @@ export default function RootLayout({children}) {
}
```

For the 404 page to render, we need to call the `notFound` function within `[locale]/layout.tsx` when we detect an incoming `locale` param that isn't a valid locale.
For the 404 page to render, we need to call the `notFound` function in [`i18n.ts`](/docs/usage/configuration#i18nts) when we detect an incoming `locale` param that isn't a valid locale.

```tsx filename="app/[locale]/layout.tsx"
import {useLocale} from 'next-intl';
import {notFound} from 'next/navigation';
```tsx filename="i18n.ts"
import {getRequestConfig} from 'next-intl/server';

// Can be imported from a shared config
const locales = ['en', 'de'];

export default function LocaleLayout({children, params}) {
export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();

return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}
return {
// ...
};
});
```

Note that `next-intl` will also call the `notFound` function internally when it tries to resolve a locale for component usage, but can not find one attached to the request (either from the middleware, or manually via [`unstable_setRequestLocale`](https://next-intl-docs.vercel.app/docs/getting-started/app-router#static-rendering)).

## `error.js`

When an `error` file is defined, Next.js creates [an error boundary within your layout](https://nextjs.org/docs/app/building-your-application/routing/error-handling#how-errorjs-works) that wraps pages accordingly to catch runtime errors:
Expand Down Expand Up @@ -159,7 +159,7 @@ export default function Error({error, reset}) {
}
```

Note that `error.tsx` is loaded as soon as the app starts. If your app is performance-senstive and you want to avoid loading translation functionality from `next-intl` as part of the initial bundle, you can export a lazy reference from your `error` file:
Note that `error.tsx` is loaded right after your app has initialized. If your app is performance-senstive and you want to avoid loading translation functionality from `next-intl` as part of the initial bundle, you can export a lazy reference from your `error` file:

```tsx filename="app/[locale]/error.tsx"
'use client';
Expand Down
1 change: 0 additions & 1 deletion docs/pages/docs/environments/server-client-components.mdx
Expand Up @@ -26,7 +26,6 @@ Moving internationalization to the server side unlocks new levels of performance
2. Library code for internationalization doesn't need to be loaded on the client side
3. No need to split your messages, e.g. based on routes or components
4. No runtime cost on the client side
5. No need to handle environment differences like different time zones on the server and client

## Using internationalization in Server Components

Expand Down
44 changes: 17 additions & 27 deletions docs/pages/docs/getting-started/app-router.mdx
Expand Up @@ -66,9 +66,17 @@ module.exports = withNextIntl({
```tsx filename="src/i18n.ts"
import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async ({locale}) => ({
messages: (await import(`../messages/${locale}.json`)).default
}));
// Can be imported from a shared config
const locales = ['en', 'de'];

export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();

return {
messages: (await import(`../messages/${locale}.json`)).default
};
});
```

<details>
Expand Down Expand Up @@ -117,15 +125,7 @@ export const config = {
The `locale` that was matched by the middleware is available via the `locale` param and can be used to configure the document language.

```tsx filename="app/[locale]/layout.tsx"
import {notFound} from 'next/navigation';

// Can be imported from a shared config
const locales = ['en', 'de'];

export default function LocaleLayout({children, params: {locale}}) {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();

return (
<html lang={locale}>
<body>{children}</body>
Expand Down Expand Up @@ -190,6 +190,7 @@ By using APIs like `useTranslations` from `next-intl` in Server Components, your
Since we use a dynamic route segment for the `[locale]` param, we need to provide all possible values via [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) to Next.js, so the routes can be rendered at build time.

```tsx filename="app/[locale]/layout.tsx"
// Can be imported from a shared config
const locales = ['en', 'de'];

export function generateStaticParams() {
Expand All @@ -199,20 +200,12 @@ export function generateStaticParams() {

### Add `unstable_setRequestLocale` to all layouts and pages

`next-intl` provides a temporary API that can be used to distribute the locale that is received via `params` in a layout or page for usage in all Server Components that are rendered as part of the request.
`next-intl` provides a temporary API that can be used to distribute the locale that is received via `params` in layouts and pages for usage in all Server Components that are rendered as part of the request.

```tsx filename="app/[locale]/layout.tsx"
import {unstable_setRequestLocale} from 'next-intl/server';

const locales = ['en', 'de'];

export default async function LocaleLayout({
children,
params: {locale}
}) {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();

export default async function LocaleLayout({children, params: {locale}}) {
unstable_setRequestLocale(locale);

return (
Expand All @@ -223,11 +216,8 @@ export default async function LocaleLayout({

```tsx filename="app/[locale]/page.tsx"
import {unstable_setRequestLocale} from 'next-intl/server';
import {locales} from '..';

export default function IndexPage({
params: {locale}
}) {
export default function IndexPage({params: {locale}}) {
unstable_setRequestLocale(locale);

// Once the request locale is set, you
Expand All @@ -242,7 +232,7 @@ export default function IndexPage({

**Keep in mind that:**

1. `unstable_setRequestLocale` needs to be called after the `locale` is validated, but before you call any hooks from `next-intl`. Otherwise, you'll get an error when trying to prerender the page.
1. The locale that you pass to `unstable_setRequestLocale` should be validated (e.g. in [`i18n.ts`](/docs/usage/configuration#i18nts)).

2. You need to call this function in every page and every layout that you intend to enable static rendering for since Next.js can render layouts and pages independently.

Expand Down Expand Up @@ -275,7 +265,7 @@ Due to this, `next-intl` uses its middleware to attach an `x-next-intl-locale` h

However, the usage of `headers` opts the route into dynamic rendering.

By using `unstable_setRequestLocale`, you can provide the locale that is received in layouts and pages via `params` to `next-intl`. By doing this, all APIs from `next-intl` can now read from this value instead of the header, enabling static rendering.
By using `unstable_setRequestLocale`, you can provide the locale that is received in layouts and pages via `params` to `next-intl`. All APIs from `next-intl` can now read from this value instead of the header, enabling static rendering.

</details>

Expand Down
6 changes: 4 additions & 2 deletions docs/pages/docs/routing/middleware.mdx
Expand Up @@ -630,5 +630,7 @@ To recover from this error, please make sure that:
1. You've [set up the middleware](/docs/getting-started/app-router#middlewarets).
2. You're using APIs from `next-intl` (including [the navigation APIs](/docs/routing/navigation)) exclusively within the `locale` segment.
3. Your [middleware matcher](#matcher-config) matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. `/users/jane.doe`).
4. If you're using [`localePrefix: 'as-needed'`](#locale-prefix-as-needed), the `locale` segment effectively acts like a catch-all for all unknown routes. You should make sure that `params.locale` is [validated](/docs/getting-started/app-router#applocalelayouttsx) before it's used by any APIs from `next-intl`.
5. To implement static rendering properly, make sure to [provide a static locale](/docs/getting-started/app-router#static-rendering) to `next-intl`.
4. If you're using [`localePrefix: 'as-needed'`](#locale-prefix-as-needed), the `locale` segment effectively acts like a catch-all for all unknown routes. You should make sure that `params.locale` is [validated](/docs/usage/configuration#i18nts) before it's used by any APIs from `next-intl`.
5. To implement static rendering, make sure to [provide a static locale](/docs/getting-started/app-router#static-rendering) to `next-intl` instead of using `force-static`.

Note that `next-intl` will print this warning only during development and will invoke the `notFound()` function to abort the render. You should consider adding [a `not-found` page](/docs/environments/error-files#not-foundjs) due to this.

2 comments on commit e6d9878

@vercel
Copy link

@vercel vercel bot commented on e6d9878 Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on e6d9878 Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-intl-docs – ./docs

next-intl-docs.vercel.app
next-intl-docs-git-main-next-intl.vercel.app
next-intl-docs-next-intl.vercel.app

Please sign in to comment.