I'm struggling to get the localized not-found
page to work with NextJS 13 (app-dir). I'm following the official guide on internationalization, as well as this solution I have found on GitHub.
[locale]
-- [...not-found]
- page.tsx <- empty page, just calls notFound() (catch-all segment)
-- (browse)
- someFolder1 <- may multiple pages and maybe nested layouts
- layout.tsx
-- (public)
- someFolder2
- layout.tsx
-- (private)
- someFolder3
- layout.tsx
-- layout.tsx
-- not-found.tsx <- should be served for all notFound() errors (including catch-all segment)
When I call the notFound
method from any of the route groups or visit an unmatched route; I receive an error stating: Unsupported Server Component type: Null
. What seems pretty strange since both files definitely have a react component as default export.
import { notFound } from "next/navigation";
export default function CatchAllUnmatched(): JSX.Element {
notFound();
return <></>;
}
I also tried to append a header to each response via a custom X-Language-Preference
property I am adding inside my middleware file. In doing so I have moved the root layout as direct descendant of the app folder and retrieved the locale as follows:
export const getLocale = cache((): Language => {
const preference = headers().get("X-Language-Preference");
return (preference ?? fallbackLanguage) as Language;
});
The problem in this approach is that the lang
property on the main html tag does not reset on client side navigation from /de
to /fr
for example. Also the 404 page is only displayed when an unmatched route is visited, calling the notFound
method still results in the same error. So this solution is not viable as well.
Any resources that display a real world example of a working internationalization example including a multilingual not found page, preferably also using route groups with nested layouts.
Possible reasons why I am encountering the error Unsupported Server Component type: Null
when both files definitely have a react component as default export.
If you need more context, code or anything is unclear about the general setup, just comment and I'll update the original question to fit the requirements.
If anyone has this problem for a similarly advanced use case I now finally have the solution. Using the 2nd approach is the correct way to address this issue.
Move the root layout out of the [locale]
folder and define a global not-found.tsx
file containing all of your UI. Inside your layout you can then call the following function to retrieve the locale:
export const getLocale = cache((): Language => {
const preference = headers().get("X-Language-Preference");
return (preference ?? fallbackLanguage) as Language;
});
Add a X-Language-Preference
header (or however you want to call it) to each response passing through your middleware, here you can define your own resolution strategy to retrieve the locale that shall be saved.
const headerName = "X-Language-Preference";
function getRequestLocale(request: NextRequest) {
// ...
}
export default async function middleware(request: NextRequest) {
const locale = getRequestLocale(request);
const response = NextResponse.next();
response.headers.set(headerName, locale);
return response;
}
lang
tag in the root layout componentThe lang property on the main html tag does not reset on client side navigation from /de to /fr for example
As I have mentioned in the original issue. This issue persists after the update too, but I have created a client side component named HTML
that will parse the locale from the URL or fallback to the default locale for the direction and language properties of the HTML tag.
interface Props extends React.HTMLAttributes<HTMLHtmlElement> {}
const HTML = memo(function HTML(props: Props): JSX.Element {
const locale = useLocale();
return <html lang={locale} dir={dir(locale)} {...props} />;
});
export default HTML;
export type { Props as HTMLProps };
The useLanguage
hook tries to parse the locale from the useParams hook, the usePathname hook and finally falls back to the default locale. Here is the implementation I have used:
export default function useLocale(): Language {
const params = useParams();
const pathname = usePathname();
const localeFromParams = useMemo(() => {
return params?.locale as Language | undefined;
}, [params.locale]);
const localeFromPathname = useMemo(() => {
return pathname?.split?.("/")?.[1] as Language | undefined;
}, [pathname]);
const finalLocale = useMemo(() => {
const decision = localeFromParams ?? localeFromPathname;
if (!!decision && languages.includes(decision)) return decision;
return fallbackLanguage;
}, [localeFromParams, localeFromPathname]);
return finalLocale;
}
After updating to version v13.4.12 containing this PR the 2nd solution suddenly started working, may been related to a bug or unwanted behavior.