next.js
next.js copied to clipboard
Global 404 page is not available for internationalization
Verify canary release
- [X] I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: win32
Arch: x64
Version: Windows 10 Pro
Binaries:
Node: 20.2.0
npm: N/A
Yarn: N/A
pnpm: N/A
Relevant packages:
next: 13.4.5-canary.3
eslint-config-next: N/A
react: 18.2.0
react-dom: 18.2.0
typescript: 4.9.4
Which area(s) of Next.js are affected? (leave empty if unsure)
App directory (appDir: true), Internationalization (i18n), Routing (next/router, next/navigation, next/link)
Link to the code that reproduces this issue or a replay of the bug
https://github.com/Arctomachine/404-page-internationalization-root-layout
To Reproduce
- Create project as usually, start dev server
- Follow internationalization section guide and create
/app/[lang]
folder. Create layout file (starting from<html lang={lang}>
) that will act as root layout. - Create
/app/not-found.tsx
file according to not found section - Follow console instruction and make top level root layout (containing only `<>{children}</>) for this page to start working
- Stop dev server. Run build and then start scripts. Visit any not existing url that would trigger 404
Describe the Bug
There are multiple problems with it. One of them being actual bug, the rest just would make this the (currently) only solution bad - if it worked.
- 404 page contents blink for a second, then page goes completely white and empty. Numerous errors in browser console.
- This approach makes it impossible to fit error page into acting root layout under
/[lang]
- This approach makes it impossible to translate error page into other languages
Uploaded reproduction: https://404-page-internationalization-root-layout.vercel.app/en
Expected Behavior
Since there is not much to be expected from this solution, I will propose new expected workflow instead. A new way of making 404 page with internationalization in mind:
- treat the topmost available layout as root. Then not-found file inside of it will behave like global 404 page
- allow passing props into not-found file to change its contents based on language of currently visited page
Which browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
any
Also just bumped into this, your proposal makes sense too!
It's actually quite horrible to get this working, since the extra layout file in the app
directory messes up a lot of other stuff for me.
It's a really annoying issue but one way to solve it is to set a header in your middleware
file containing the language and then use headers()
to read that header and set the lang.
This will only work if you have a completely dynamic site
Feels a bit more hacky but it allows you to use not-found
and set a html lang.
middleware.ts
export const middleware = (request: NextRequest) => {
const headers = new Headers(request.headers);
headers.set("x-site-locale", locale);
return NextResponse.next({ request: { headers } })
}
root layout.tsx
import headers from 'next/headers'
export default function RootLayout() {
return <html lang={headers().get('x-site-locale') ?? 'en-US'}>{children}</html>;
}
But then it automatically turns whole site into dynamic rendering. For dynamic site this method might be just strange, but for static sites it is simply impossible solution.
Fair enough, was too focused on dynamic pages, I'll edit the reply.
@Arctomachine I encountered this issues as well today and here's what I think is a working solution (until the logic works as listed in the expected behaviour above):
I got a middleware function that will always prepend a locale to the any URL requested (except for certain allow-listed assets):
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import Negotiator from "negotiator";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import { i18n } from "./i18n-config";
const ALLOW_LISTED_ASSETS = ["/icon.png", "/favicon.ico"];
function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
const locales: string[] = i18n.locales;
return matchLocale(languages, locales, i18n.defaultLocale);
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
if (ALLOW_LISTED_ASSETS.includes(pathname)) return;
// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(`/${locale}/${pathname}`, request.url)
);
}
}
export const config = {
matcher: ["/((?!studio|api|_next/static|_next/image).*)"],
};
This ensures that any URL will hit the /[lang]
route.
My /app
folder is organized like this:
/
/[lang]
/(some-group)
/some-static-route
/[...notFound]
page.tsx
not-found.tsx
layout.tsx
As you can see, in the /[lang]
dynamic segment, I only have static routes, which allows me to use the /[...notFound]
handler with the following page:
import { notFound } from "next/navigation";
export default async function Page() {
notFound();
}
This will ensure that the closest not-found.tsx
file gets hit (ie. the one in /[lang]/not-found.tsx
), which has access to the lang from its layout.
Obviously, this will only work if you don't have another dynamic segment nested at the root of /[lang]
as it would clash with /[lang]/[...notFound]
.
It also relies on any request always getting routed with a lang
prefix.
Hope this helps!
Update re: https://github.com/vercel/next.js/issues/50699#issuecomment-1608167519
After poking around, I realize I forgot that the not-found.tsx
route doesn't accept any props. This means the only way to use the technique above is to turn that component into a client component and fetch the error page data (if needed) at runtime to be able to display the correct localized information. Not ideal at all :\
And not-found.js doesn't work on layout group.
same issue.
Regarding to the reproduction, the approach using root layout that only rendering children and a root not-found that doesn't contain html
/body
will break the client rendering if you directly hit a non-existen route. The root not found page is composed by root layout and root not-found, so the output html of not-found page is missing html
and body
that will lead to hydration errors later.
You can remove the root layout file which renders only children, and place the root not-found to app/[lang]/not-found.js
@huozhi In my project, the not-found.tsx
file is located in the app/[lng]
directory, and the 404 page does not take effect.
@huozhi the solution you describe is not for issue in topic. It works for calling notFound()
function, yes.
But the issue here is global 404 page. If file is placed into /[lang]
folder, then default 404 page will be used instead of our file.
So should we just open new issue since this one does not look like it is ever going to be reopened and bug is not fixed?
@huozhi Any update?
Encountered this today, I am forced to move my RootLayout out of [locale]
and then put a not-found
at the root for it to work. But this should not be the way. the not-found
at rootLayout
level should be the app default 404
handler
Yes, this is very workaroundy.
It gets even more interesting when you start adding error.js
files. You won't use global-error.js
file as it now loses its point (your RootLayout
most likely doesn't include much code).
Instead, you'll be forced to handle errors of the layout below (the LocaleLayout
) in some way if you want to display translated error page. In this case, I had to make my own ErrorBoundary
file (Client Component) and use it in the layer right after the provider supplying translated messages (before other contexts).
Lots of workarounds and mess now in my code. I love the idea about the app router tho and I understand it is a lot of hard work to get this done properly.
We load the translation in server component code and pass it to a provider which is a client component. Because the error page is a client component we cannot use our default handling.
Any update?
Edit by maintainers: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote π on the issue description or subscribe to the issue for updates. Thanks!
It's been 6 months this issue is open and we don't have a hint of fix yet.. My workaround was putting the not-found at the root of the app folder with an empty layout.
In order to avoid console errors for me what worked was creating an empty layout and wrapping the not-found in an html and body tag.
But then the next challenge is having a localized 404. For that I used next/headers
to get the accepted language header (similar technique we use in the middleware) This is the only was I found because things like location.href
or other client methods wouldn't work in production build..
The problem with this method is that if the user's browser language is set to korean but was visiting the site in english then the 404 page will be displayed in korean.
app/layout.jsx
export const metadata = {
title: '404',
description: '404',
}
export default function Layout({ children }) {
return (
<>{children}</>
)
}
app/not-found.jsx
import { headers } from 'next/headers'
import Negotiator from 'negotiator'
import { match } from '@formatjs/intl-localematcher'
const defaultLocale = 'kor'
const locales = ['kor', 'eng']
export default async function NotFound() {
const languageHeaders = headers().get('accept-language')
const languages = new Negotiator({ headers: {'accept-language': languageHeaders }}).languages()
const locale = match(languages, locales, defaultLocale)
const dict = {
eng: 'this page is not found.',
kor: 'μμ²νμ νμ΄μ§λ₯Ό μ°Ύμ μ μμ΅λλ€.',
}
return (
<html>
<body>
<div className="section-error">
{dict[locale]}
</div>
</body>
</html>
)
}
It's been 6 months this issue is open and we don't have a hint of fix yet.. My workaround was putting the not-found at the root of the app folder with an empty layout.
In order to avoid console errors for me what worked was creating an empty layout and wrapping the not-found in an html and body tag. But then the next challenge is having a localized 404. For that I used
next/headers
to get the accepted language header (similar technique we use in the middleware) This is the only was I found because things likelocation.href
or other client methods wouldn't work in production build..The problem with this method is that if the user's browser language is set to korean but was visiting the site in english then the 404 page will be displayed in korean.
app/layout.jsx
export const metadata = { title: '404', description: '404', } export default function Layout({ children }) { return ( <>{children}</> ) }
app/not-found.jsx
import { headers } from 'next/headers' import Negotiator from 'negotiator' import { match } from '@formatjs/intl-localematcher' const defaultLocale = 'kor' const locales = ['kor', 'eng'] export default async function NotFound() { const languageHeaders = headers().get('accept-language') const languages = new Negotiator({ headers: {'accept-language': languageHeaders }}).languages() const locale = match(languages, locales, defaultLocale) const dict = { eng: 'this page is not found.', kor: 'μμ²νμ νμ΄μ§λ₯Ό μ°Ύμ μ μμ΅λλ€.', } return ( <html> <body> <div className="section-error"> {dict[locale]} </div> </body> </html> ) }
Using headers
in your not-found file will make it dynamic and uncachable. When the not found is dynamic, everything in that folder will turn dynamic. So with this method, you are eliminating all caching of your site.
It's been 6 months this issue is open and we don't have a hint of fix yet.. My workaround was putting the not-found at the root of the app folder with an empty layout. In order to avoid console errors for me what worked was creating an empty layout and wrapping the not-found in an html and body tag. But then the next challenge is having a localized 404. For that I used
next/headers
to get the accepted language header (similar technique we use in the middleware) This is the only was I found because things likelocation.href
or other client methods wouldn't work in production build.. The problem with this method is that if the user's browser language is set to korean but was visiting the site in english then the 404 page will be displayed in korean.app/layout.jsx
export const metadata = { title: '404', description: '404', } export default function Layout({ children }) { return ( <>{children}</> ) }
app/not-found.jsx
import { headers } from 'next/headers' import Negotiator from 'negotiator' import { match } from '@formatjs/intl-localematcher' const defaultLocale = 'kor' const locales = ['kor', 'eng'] export default async function NotFound() { const languageHeaders = headers().get('accept-language') const languages = new Negotiator({ headers: {'accept-language': languageHeaders }}).languages() const locale = match(languages, locales, defaultLocale) const dict = { eng: 'this page is not found.', kor: 'μμ²νμ νμ΄μ§λ₯Ό μ°Ύμ μ μμ΅λλ€.', } return ( <html> <body> <div className="section-error"> {dict[locale]} </div> </body> </html> ) }
Using
headers
in your not-found file will make it dynamic and uncachable. When the not found is dynamic, everything in that folder will turn dynamic. So with this method, you are eliminating all caching of your site.
If that's the case they should put a big warning in nextjs's documentation. Because they are using headers
in not-found.jsx
https://nextjs.org/docs/app/api-reference/file-conventions/not-found
Dynamic pages aren't statically generated, but that doesn't mean they are not cached. They still have CDN caching at Vercel.
Dynamic pages aren't statically generated, but that doesn't mean they are not cached. They still have CDN caching at Vercel.
I'm pretty sure using headers will make SSR run for every single request, because for ever user the headers can be different. Therefor, the page is not served from the cache. It will be slower to load than an static page.
The point is that a 404 page is about as "static" a page as you could possibly ever want to create. Localising a static 404 page should be dead easy in any web framework in 2024.
The sad truth is that NextJs has neglected and botched internationalisation for 8 years now βΒ users should not expect anything new.
any updates on this?
Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote π on the issue description or subscribe to the issue for updates. Thanks!
Any update on this thread?
Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote π on the issue description or subscribe to the issue for updates. Thanks!