vercel / next.js

The React Framework
https://nextjs.org
MIT License
124.46k stars 26.57k forks source link

Docs: Internationalization middleware.js example does not work #58556

Open kaganAhmetOkan opened 9 months ago

kaganAhmetOkan commented 9 months ago

What is the improvement or update you wish to see?

Example from https://nextjs.org/docs/app/building-your-application/routing/internationalization

import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

let headers = { 'accept-language': 'en-US,en;q=0.5' }
let languages = new Negotiator({ headers }).languages()
let locales = ['en-US', 'nl-NL', 'nl']
let defaultLocale = 'en-US'

match(languages, locales, defaultLocale) // -> 'en-US'

Example won't be working because Negotiator will not recognize the NextRequest object that the middleware passes in the next example.

Next example:

let locales = ['en-US', 'nl-NL', 'nl']

// Get the preferred locale, similar to the above or using a library
function getLocale(request) { ... }

export function middleware(request) {
  // Check if there is any supported locale in the pathname
  const { pathname } = request.nextUrl
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (pathnameHasLocale) return

  // Redirect if there is no locale
  const locale = getLocale(request)
  request.nextUrl.pathname = `/${locale}${pathname}`
  // e.g. incoming request is /products
  // The new URL is now /en-US/products
  return Response.redirect(request.nextUrl)
}

export const config = {
  matcher: [
    // Skip all internal paths (_next)
    '/((?!_next).*)',
    // Optional: only run on root (/) URL
    // '/'
  ],
}

Comment from Negotiator's creator: https://github.com/jshttp/negotiator/issues/65#issuecomment-1815325586 Yes, I know the second example says getLocale() is "similar" to above code and not exactly the same. But it would be nice if both examples could work with each other.

Is there any context that might help us understand?

This is the function I wrote using the docs:

import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";

export default function getLocale(request, locales) {
  const { headers } = request;
  const languages = new Negotiator({ headers }).languages(); // returns ["*"] causing match() to throw an error
  const defaultLocale = "en";

  return match(languages, locales, defaultLocale); // throws error "Incorrect locale information provided"
};

The request param is the request object given by the middleware and the locales is an array of locales.

new Negotiator({ headers }).languages() returns ["*"]. Which causes the match function to throw this error:

 ⨯ node_modules/@formatjs/intl-localematcher/lib/abstract/CanonicalizeLocaleList.js (7:16) @ getCanonicalLocales
 ⨯ Incorrect locale information provided

The middleware:

import getLocale from "./lib/getLocale";

const locales = ["en", "tr"];

export function middleware(request) {
  const { pathname } = request.nextUrl;
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return;

  const locale = getLocale(request, locales); // returns null
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return Response.redirect(request.nextUrl);
};

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

Does the docs page already exist? Please link to it.

https://nextjs.org/docs/pages/building-your-application/routing/internationalization

pier7529 commented 8 months ago

Same issue in v14.0.4 (with default locale defined as 'fr')

probably fixed with this answer :

https://stackoverflow.com/questions/76447732/nextjs-13-i18n-incorrect-locale-information-provided

nhuethmayr commented 6 days ago

This should do the trick

// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;

// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
  locales,
);

Taken from: https://github.com/vercel/next.js/blob/canary/examples/app-dir-i18n-routing/middleware.ts