crg
crg

Reputation: 4577

Why internationalization is not working on NextJS 13 using a [locale] sub-folder on app/

How to get i18n working on Next 13 ?

I have created a nested [locale]/ folder in app/ but it just gives a 404

See my next.config.js

const nextConfig = {
  experimental: {
    appDir: true,
  },
  i18n: {
    defaultLocale: 'fr,
    locales: ['fr', 'en'],
    localeDetection: true
  }
}

Did you find a way to support i18n with React Server components ?

EDIT:

On the beta.nextjs doc it says :

We are currently not planning to include the following features in app: Internationalization (i18n)

I have as well found an open issue about it, which does not provide any workaround yet.

Upvotes: 9

Views: 14035

Answers (2)

Filip Kowal
Filip Kowal

Reputation: 362

[EDIT] There's a guide in Next.js documentation now.

Middleware is a solution to i18n in Next.js 13. Until there is an officially recommended way.

  1. To use middleware in Next.js 13 with /app directory, you must create the /page directory (even if all your pages are going to be in /app directory) and a .keep file inside of it. Github issue about it
  2. Create a dynamic route [locale] in the app directory and put all other routes there.
  3. Set middleware so it redirects all traffic from routes without a locale param to ones that have it (f.e. based on Accept-language header or location of the client).
  4. Get the value of the locale on a page from params.

A minimal middleware.ts content inspired by Eric Howey's article:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Regex to check whether something has an extension, e.g. .jpg
const PUBLIC_FILE = /\.(.*)$/;

export function middleware(request: NextRequest) {
  const { nextUrl, headers } = request;
  // Cloned url to work with
  const url = nextUrl.clone();
  // Client language, defaults to en
  const language =
    headers
      .get("accept-language")
      ?.split(",")?.[0]
      .split("-")?.[0]
      .toLowerCase() || "en";

  try {
    // Early return if it is a public file such as an image or an api call
    if (PUBLIC_FILE.test(nextUrl.pathname) || nextUrl.pathname.includes("/api")) {
      return undefined;
    }

    // Proceed without redirection if on a localized path
    if (
      nextUrl.pathname.startsWith("/en") ||
      nextUrl.pathname.startsWith("/de") ||
      nextUrl.pathname.startsWith("/fr")
    ) {
      return undefined;
    }

    if (language === "fr") {
      url.pathname = `/fr${nextUrl.pathname}`;
      return NextResponse.redirect(url);
    }

    if (language === "de") {
      url.pathname = `/de${nextUrl.pathname}`;
      return NextResponse.redirect(url);
    }

    if (!["de", "fr"].includes(language)) {
      url.pathname = `/en${nextUrl.pathname}`;
      return NextResponse.redirect(url);
    }

    return undefined;
  } catch (error) {
    console.log(error);
  }
}

Upvotes: 8

adrai
adrai

Reputation: 3178

Even though i18n is no longer directly supported by Next.js in combination with the new app directory, there is still a way to solve this...

With the help of a middleware and with the use of i18next, react-i18next and i18next-resources-to-backend this is solvable.

It works on server side and on client side.

There's a little example showing how this could work directly by using i18next and react-i18next on server side and on client side.

...and here the corresponding blog post.

A snipped on how this could look like:

server side:

import Link from 'next/link'
import { useTranslation } from '../i18n'
import { Footer } from './components/Footer'

export default async function Page({ params: { lng } }) {
  const { t } = await useTranslation(lng)
  return (
    <>
      <h1>{t('title')}</h1>
      <Link href={`/${lng}/second-page`}>
        {t('to-second-page')}
      </Link>
      <br />
      <Link href={`/${lng}/client-page`}>
        {t('to-client-page')}
      </Link>
      <Footer lng={lng}/>
    </>
  )
}

client side:

'use client'

import Link from 'next/link'
import { useTranslation } from '../../i18n/client'
import { Footer } from '../components/Footer/client'
import { useState } from 'react'

export default function Page({ params: { lng } }) {
  const { t } = useTranslation(lng, 'client-page')
  const [counter, setCounter] = useState(0)
  return (
    <>
      <h1>{t('title')}</h1>
      <p>{t('counter', { count: counter })}</p>
      <div>
        <button onClick={() => setCounter(Math.max(0, counter - 1))}>-</button>
        <button onClick={() => setCounter(Math.min(10, counter + 1))}>+</button>
      </div>
      <Link href={`/${lng}`}>
        <button type="button">
          {t('back-to-home')}
        </button>
      </Link>
      <Footer lng={lng} />
    </>
  )
}

Upvotes: 0

Related Questions