I18n Implementation - NextJS v14 404 & Middleware Errors

Describe your question/issue in detail

We're working with a project which requires internationalisation and have been looking through a variety of documents to determine how this works in Prismic (sources in next section).

After enabling en-gb and a custom locale value we've got the locales configured in nextjs.config.mjs as expected. When running locally if we call http://localhost:3000/ we get a scenario where a 404 is thrown by NextJS before it hits the middleware and subsequently seems to terminate any remaining processes.

[next]  GET / 404 in 237ms
[next] CONSOLE LOG: MIDDLEWARE TEST
[next]  GET /favicon.ico 200 in 5ms

If we call http://localhost:3000/en-gb the same issue occurs and it never reaches the console logs in the page.tsx files. The only time it does seem to trigger semi-correctly is when it's called as http://localhost:3000/en_gb which is obviously incorrect and results in this expected error.

[next] MIDDLEWARE TEST
[next] CONSOLE LOG: Page Content
[next] { lang: 'en_gb' }
[next] CONSOLE LOG: Page Meta Data
[next] { lang: 'en_gb' }
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Index (./src/app/[lang]/page.tsx:47:18)
[next] digest: "1732355935"
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Index (./src/app/[lang]/page.tsx:47:18)
[next] digest: "1732355935"
[next] CONSOLE LOG: Page Meta Data
[next] { lang: 'en_gb' }
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Module.generateMetadata (./src/app/[lang]/page.tsx:27:18)
[next] digest: "3879409013"
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Module.generateMetadata (./src/app/[lang]/page.tsx:27:18)
[next] digest: "3879409013"
[next]  GET /en_gb 500 in 948ms
[next] MIDDLEWARE TEST
[next]  GET /favicon.ico 200 in 4ms

When we've added in a work around for the above example (.replace on the underscore to convert it to a hyphen) we know the languages are available as we've got them embedded on the page. These link to the correct places as expected but clicking on any of the links results in a 404 being thrown.

Any advice on fixing this issue would be greatly appreciated.

What steps have you taken to resolve this issue already?

Investigated the following articles and forum posts (amongst others):

Code

I'm unable to provide you with a link to a GitHub repository as the code is sat behind a private repo so the following information is the best I can provide.

Directory Structure

  • src
    • app
      • [lang]
        • [uid]
          • page.tsx
        • article
          • page.tsx
        • page.tsx
        • slice-simulator
      • api
      • favicon.ico
      • icon.png
      • layout.tsx

next.config.mjs

/** @type {Promise<import('next').NextConfig>} */
const nextConfig = async () => {
  const client = createClient(sm.repositoryName);

  const { languages } = await client.getRepository();
  const locales = languages.map((lang) => lang.id);

  const defaultLocale = languages.find(({ is_master }) => is_master).id;

  return {
    i18n: { locales, defaultLocale: defaultLocale, localeDetection: false },
    // Other configuration
  };
};

export default nextConfig;

prismic.ts

// Out of the box imports and exports

const routes: prismic.ClientConfig['routes'] = [
  { type: 'home', path: '/:lang?', uid: 'home' },
  { type: 'page', path: '/:lang?/:uid' },
  { type: 'article', path: '/:lang?/article/:uid' },
];

// Out of the box createClient function 

middleware.ts

import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/prismicio';

export async function middleware(request: NextRequest) {
  const client = createClient();
  const repository = await client.getRepository();

  const locales = repository.languages.map((lang) => lang.id);
  const defaultLocale = locales[0];

  // Check if there is any supported locale in the pathname
  const { pathname } = request.nextUrl;

  const pathnameIsMissingLocale = locales.every(
    (locale) =>
      !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
  );

  // Redirect to default locale if there is no supported locale prefix
  if (pathnameIsMissingLocale) {
    return NextResponse.rewrite(
      new URL(`/${defaultLocale}${pathname}`, request.url),
    );
  }
}

export const config = {
  matcher: ['/((?!_next).*)'],
};

app/page.tsx

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  console.log('Language Meta Data');
  console.log(params);
  const client = createClient();
  const home = await client.getSingle('home', { lang: params.lang });

  return {
    title: prismic.asText(home.data.title),
    description: home.data.meta_description,
    openGraph: {
      title: home.data.meta_title ?? undefined,
      images: [{ url: home.data.meta_image.url ?? '' }],
    },
  };
}

export default async function Index({ params }: PageProps) {
  console.log('Language Page');
  console.log(params);
  const client = createClient();
  const page = await client.getSingle('home', { lang: params.lang });

  const { slices } = page.data;

  return (
    <Layout language={params.lang} page={page}>
      <SliceZone slices={slices} components={components} />
    </Layout>
  );
}

[uid]/page.tsx

export async function generateMetadata({
  params: { lang, uid },
}: PageProps): Promise<Metadata> {
  const client = createClient();

  const page = await client
    .getByUID('page', uid, { lang: lang })
    .catch(() => notFound());

  return {
    title: prismic.asText(page.data.title),
    description: page.data.meta_description,
    openGraph: {
      title: page.data.meta_title || undefined,
      images: [
        {
          url: page.data.meta_image.url || '',
        },
      ],
    },
  };
}

export default async function Page({ params: { lang, uid } }: PageProps) {
  const client = createClient();

  const page = await client
    .getByUID('page', uid, { lang: lang })
    .catch(() => notFound());

  return (
    <Layout language={lang} page={page}>
      <SliceZone slices={page.data.slices} components={components} />
    </Layout>
  );
}

export async function generateStaticParams() {
  const client = createClient();

  const pages = await client.getAllByType('page', { lang: '*' });

  return pages.map(({ uid, lang }) => ({ uid: uid, lang: lang }));
}

package.json

{
  "name": "nextjs-starter-prismic-minimal-ts",
  "version": "0.1.0",
  "private": true,
  "license": "Apache-2.0",
  "author": "Prismic <contact@prismic.io> (https://prismic.io)",
  "scripts": {
    "dev": "concurrently \"npm:next:dev\" \"npm:slicemachine\" --names \"next,slicemachine\" --prefix-colors blue,magenta",
    "next:dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "slicemachine": "start-slicemachine",
    "format": "prettier --write ."
  },
  "dependencies": {
    "@prismicio/client": "^7.5.0",
    "@prismicio/next": "^1.5.0",
    "@prismicio/react": "^2.7.4",
    "classnames": "^2.5.1",
    "next": "^14.2.3",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "uuid": "^9.0.1"
  },
  "devDependencies": {
    "@slicemachine/adapter-next": "^0.3.38",
    "@svgr/webpack": "^8.1.0",
    "@types/node": "^20.12.11",
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.0",
    "@types/uuid": "^9.0.8",
    "concurrently": "^8.2.2",
    "eslint": "^8",
    "eslint-config-next": "^14.2.3",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-react-hooks": "^4.6.2",
    "prettier": "^3.2.5",
    "sass": "^1.77.1",
    "slice-machine-ui": "^1.26.0",
    "typescript": "^5.4.5"
  }
}
1 Like