reactjsnext.jsinternationalizationnext.js13next-i18next

NextJS 13 - How to create a multilingual not-found page using route groups


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.

The first solution I have tried

[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)
The issue with this solution

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 <></>;
}

The other solution I have tried

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 issue with this solution

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.

What kind of answer would help me?

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.


Solution

  • 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;
    });
    

    Implement inside your middleware

    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;
    }
    

    Fix the lang tag in the root layout component

    The 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;
    }
    

    Why does this approach suddenly work?

    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.