How to implement Default Language without URL Prefix in Next.js 14 for Static Export?
Asked Answered
T

6

7

I am currently working on a website using Next.js 14, with the aim of exporting it as a static site for distribution via a CDN (Cloudflare Pages). My site requires internationalization (i18n) support for multiple languages. I have set up a folder structure for language support, which looks like this:

- [language]
  -- layout.tsx  // generateStaticParams with possible languages
  -- page.tsx
  -- ...

This setup allows me to access pages with language prefixes in the URL, such as /en/example and /de/example.

However, I want to implement a default language (e.g., English) that is accessible at the root path without a language prefix (/example). Importantly, I do not wish to redirect users to a URL with the language prefix for SEO purposes. Nor can I use the rewrite function because I'm using static export.

Here are my specific requirements:

  1. Access the default language pages directly via the root path (e.g., /example for English).
  2. Avoid redirects to language-prefixed URLs (e.g., not redirecting /example to /en/example).
  3. Maintain the ability to access other languages with their respective prefixes (e.g., /de/example for German).

I am looking for guidance on:

How to realise this with Next.js 14 to serve the default language pages at the root path without a language prefix. Ensuring that this setup is compatible with the static export feature of Next.js.

Any insights, code snippets, or references to relevant documentation would be greatly appreciated.

Tight answered 15/1 at 18:30 Comment(4)
"I do not wish to redirect users to a URL with the language prefix for SEO purposes" - but do you want to redirect users from /en/example to /example then? For SEO purposes, you should not serve the same content twice. (Or if you really have to, use a canonical link)Picrate
@Picrate Exactly. I mark them correctly as canonical.Tight
Were you able to get this working as expected? I am having a similar requirement right nowWindstorm
Same problem here. Before I did it with page router, but with app router it's complicatedNichol
R
2

If I understand your question, Nextjs is a file routing system every page.js/ts or route.js/ts files are standing for a page in the web app. So, if you going with the structure you provided in you question, you will need to structure your folders and files this way and to avoid duplicate in your code.

components
  - HomePage.ts // Shared component for languages
  - AboutPage.ts
App
 - layout.tsx
 - page.ts // for default language pages
 - about
   -- page.ts
 - contactus
   -- page.ts
 - [language]
   -- layout.tsx
   -- page.ts  // for other languages pages
   -- about
      --- page.ts
   -- contactus
      --- page.ts

The other approach you can follow is to manage the language by state managements like Redux or Zustand and translate/fetch the data the current language in the state, this way you do not need to create about pages files.

- layout.tsx
  - page.ts // for all language pages
- about
  -- page.ts
- contactus
  -- page.ts

additionally you can configure you next.config.js this way:

module.exports = {
  i18n: {
    locales: ['en', 'de'], // Add your default language and other languages
    defaultLocale: 'en', // Set the default language
  },
};
Ringlet answered 27/1 at 11:27 Comment(1)
can you make a full example on it? the nature of server and client components makes this over complicatedEscuage
E
1

I think that there is a more elegant way how to default to / instead of /en for default locale and it is included into the next-intl documentation already, although it is not easy to find it: https://next-intl-docs.vercel.app/docs/routing#locale-prefix

Don't use a locale prefix for the default locale

If you only want to include a locale prefix for non-default locales, you can configure your routing accordingly.

// config.ts
import {LocalePrefix} from 'next-intl/routing';
 
export const localePrefix = 'as-needed' satisfies LocalePrefix;
 
// ...

In this case, requests where the locale prefix matches the default locale will be redirected (e.g. /en/about to /about). This will affect both prefix-based as well as domain-based routing.

Note that:

  1. If you use this strategy, you should make sure that your middleware matcher detects unprefixed pathnames.

  2. If you use the Link component, the initial render will point to the prefixed version but will be patched immediately on the client once the component detects that the default locale has rendered. The prefixed version is still valid, but SEO tools might report a hint that the link points to a redirect.

Here is my middleware config for the languages i use

import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  // A list of all locales that are supported
  locales: ["en", "es", "es-419", "it", "pt", "tr"],

  // Used when no locale matches
  defaultLocale: "en",
  localePrefix: "as-needed",
});

export const config = {
  // Match only internationalized pathnames
  matcher: [
    // Match all pathnames except for
    // - … if they start with `/api`, `/_next` or `/_vercel`
    // - … the ones containing a dot (e.g. `favicon.ico`)
    "/((?!api|_next|_vercel|.*\\..*).*)",
  ],
};
Eakin answered 1/7 at 13:21 Comment(4)
But does this work with export static?Tight
it does not use rewrites give it a try. I have not tested it with static exports but if you do please share the outcome with us.Eakin
You can't use the middleware with output: 'export', that's the whole point of the question, how to do it without the middleware.Intrauterine
The sad truth is that you cannot do it properly, it's a limitation of Next.js, as it stated here: next-intl-docs.vercel.app/docs/routing/…. You can either duplicate your code, as suggested in another answer, or make your peace with the default prefix.Intrauterine
S
1

You obviously can't use middleware (because it's a static site and you need output: "export"), so you have to play around with the Next.js router and its folder structure. This means:

  • /app/[locale]/example/page.tsx for /de/example
  • /app/example/page.tsx for /example

In /app/[locale]/example/page.tsx you put your page as usual:

export default function Page({ params: { locale } }) {
  const t = getTranslations(locale || "en");
  return (
    <div>
      {t.content}
    </div>
  );
}

…and in /app/example/page.tsx you just import the page from the other file:

export { default as default } from "../[locale]/example/page";

I've made a post that explains this and more issues relating to static Next.js sites with output: "export". I've also made a repo where you can check a working setup of this.

Sidle answered 19/8 at 21:14 Comment(1)
That's a great and easy way :)Tight
K
0

I have used a seprate wrapper for using Link conditionally here is my Link component

import { Link as I18Link } from "@/i18n/routing";
import { useLocale } from "next-intl";
import Link from "next/link";
import React, { HTMLAttributes } from "react";

interface MyLinkProps extends HTMLAttributes<HTMLAnchorElement> {
  href: string;
  children?: React.ReactNode;
}
const MyLink: React.FC<MyLinkProps> = ({ href, children, ...props }) => {
  const loca = useLocale();
  if (loca === "en") {
    return (
      <Link href={href} {...props}>
        {children}
      </Link>
    );
  } else {
    return (
      <I18Link href={href} {...props}>
        Link
      </I18Link>
    );
  }
};

export default MyLink;

and i also have a changeLanguage component in my header that also do this with useRouter

"use client";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { usePathname, useRouter as useRouterI18 } from "@/i18n/routing";

import { Locale } from "@/types/Types";
import { Globe } from "lucide-react";
import { Button } from "./ui/button";
import { useTranslations } from "next-intl";
import { useRouter as useRouterNext } from "next/navigation";

export const LangSelect = () => {
  const t = useTranslations();
  const routerNext = useRouterNext();
  const routerI18 = useRouterI18();
  const pathname = usePathname();
  const changeLanguage = ({ locale }: Locale) => {
    if (locale === "en") {
      console.log("Pathname:", pathname);
      routerNext.replace(pathname);
    } else {
      routerI18.replace(pathname, { locale });
    }
  };
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon" aria-label={t("changeLanguage")}>
          <Globe className="h-5 w-5 text-green-600 dark:text-green-400" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => changeLanguage({ locale: "en" })}>
          English
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => changeLanguage({ locale: "hi" })}>
          हिन्दी (Hindi)
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
};

This works for static Rendering

by runnning npm run build the files are in diffent folder for the specified locale so i have extracted the default local i.e. en in the root directory

and localePrefix: "as-needed",

Here is how my folder look likes, "en" folder is empty

Kaiak answered 30/9 at 7:46 Comment(0)
M
-1

Please modify the value of the localePrefix from "always" to "as-needed" in the middleware.ts or in navigation.ts if you use it

Malmo answered 16/4 at 6:33 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.That
F
-2

I had this issue, I did not want to use the url to set/change the language (ex: /en, /fr, /es), and also be able to read the language on server side components.

What I did was use the cookies to set the language, so the logic is something like this:

If language cookie doesn't exist, use next internationalization strategy to get the language, and pass it to the page in the header response.

Then in pages you can add some logic to read cookies and header, and language should be something like:

const lang = languageCookie.value ?? headerCookie

So my middleware looks something like:

const getLocale = (request: NextRequest) => {
  const languageInCookie = request.cookies.get('language')

  if (languageInCookie) {
    console.log('---> Returning language from cookie', languageInCookie)
    return languageInCookie.value
  }
  
  const headers = { 'accept-language': 'en-US,en;q=0.5' }
  const languages = new Negotiator({ headers }).languages()
  const locales = ['en', 'es', 'pt']
  const defaultLocale = 'pt'
  
  return match(languages, locales, defaultLocale)
}

export function middleware(request: NextRequest) {
  const locale = getLocale(request)

  console.log('-----> Locale from middleware', locale)

  // Set header in middleware
  return NextResponse.next({ 
    headers: {
      'x-language': locale
    }
  })
}

Now, in the page, it's important to set the cookie with expiration time, this will allow the cookie to be kept even when the browser is closed.

I used a server action:

'use server'
 
import { cookies } from 'next/headers'
 
export async function setLanguage(lang: string) {
  cookies().set('language', lang, {
    // Set cookie for 1 year
    expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
  })
}

The cookie also can be changed in server components.

Now you can use a provider to allow use this language in client components, passing the server language within the main layout.

Fishplate answered 20/4 at 9:19 Comment(2)
The question is only to solve websites which are "exporting it as a static site". Your solution works with cookies and a server. So it is not an answer to this question.Tight
This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. To get notified when this question gets new answers, you can follow this question. Once you have enough reputation, you can also add a bounty to draw more attention to this question. - From ReviewBennink

© 2022 - 2024 — McMap. All rights reserved.