skip to Main Content

I am creating web app that can be accessed only when user login.
I use nextjs 13 with App Route (not Pages Route!).

And I am trying to use internalization without having to create sub-route like /en or domain like mywebsite.lang or lang.website. I would like to fetch user data from database of his preferred language and apply it on the whole app. Is it possible?
I read on nextjs doc about i18n but it tells only about using sub-routing or domain. And other blogs or videos available I can find online only demonstrating either sub-route/domain or nextjs using Pages Route.

I have tried this 2 different blogs instruction. But unfortunately it uses Pages Route not App Route, so I can’t implement it to my app that uses App Route.
https://hackernoon.com/implementing-i18n-in-nextjs-a-guide-to-non-route-based-localization
https://iamsannyrai.medium.com/i18n-in-next-js-without-sub-path-or-domain-routing-2443c1a349c6
When I implement instruction from the blogs above, my codes belows in src/app/layout.tsx, it gets error belows.

// src/app/layout.tsx
"use client";

import { appWithI18Next, useSyncLanguage } from "ni18n";

import { ni18nConfig } from "../../ni18n.config.js";

function MyApp({ Component, pageProps }) {
  const locale =
    typeof window !== "undefined" && window.localStorage.getItem("MY_LANGUAGE");

  useSyncLanguage(locale || "en");

  return <Component {...pageProps} />;
}

export default appWithI18Next(MyApp, ni18nConfig);

// ni18n.config.js


export const ni18nConfig = {
  supportedLngs: ["en", "id"],
  ns: ["sign-up"],
};

error

Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'locale')

Call Stack
WithI18Next
node_modules/ni18n/dist/esm/app-with-i18next/app-with-i18next.js (71:0)
renderWithHooks
node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js (10697:0)
updateFunctionComponent
node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js (15180:0)
mountLazyComponent
node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js (15620:0)
...

update: still not working

/// src/pages/_app.tsx

import { useEffect, useState } from "react";
import { I18nextProvider } from "react-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import i18n from "../../i18n";

export default function MyApp({ Component, pageProps }) {
  const [locale, setLocale] = useState(pageProps.locale);

  useEffect(() => {
    // update locale on client
    setLocale(window.localStorage.getItem("locale"));
  }, []);

  return (
    <I18nextProvider i18n={i18n}>
      <Component {...pageProps} />
    </I18nextProvider>
  );
}

export async function getServerSideProps() {
  // fetch user locale from DB
  const locale = "en";

  return {
    props: {
      ...(await serverSideTranslations(locale)),
      locale,
    },
  };
}

// i18n.js
import { initReactI18next } from "react-i18next";
import i18n from "i18next";

i18n.use(initReactI18next).init({
  lng: "en",
  fallbackLng: "en",
});

export default i18n;

// src/app/coba/page.tsx

"use client";

import { useTranslation } from "react-i18next";

function Coba() {
  const { t, i18n } = useTranslation();

  return <div>content Coba: {t("lang")}</div>;
}

export default Coba;

// public/locales/en/sign-up.json

{
  "lang": "language"
}

When I go to url /coba on browser, the result is

content Coba: lang

that should be "content Coba: language"

2

Answers


  1. The issue is that appWithI18Next is a higher order component designed to wrap Pages, not App components in Next.js.

    Since you are using App Routing, you’ll need to handle i18n a bit differently:

    1. In pages/_app.tsx, get the user’s locale preference from the database on the server. Pass it to useState on the client:
    export default function MyApp({ Component, pageProps }) {
      const [locale, setLocale] = useState(pageProps.locale);
      
      useEffect(() => {
        // update locale on client
        setLocale(window.localStorage.getItem('locale')) 
      }, []);
    
      return (
        <I18NextProvider i18n={i18n}>
          <Component {...pageProps} />
        </I18NextProvider>
      )
    }
    
    export async function getServerSideProps() {
      // fetch user locale from DB
      const locale = 'en';
      
      return {
        props: {
          ...(await serverSideTranslations(locale)),
          locale
        }
      }
    }
    
    1. Configure i18next to use the locale from state:
    import i18next from 'i18next';
    
    i18next.use(initReactI18next).init({
      lng: locale, // from state
      fallbackLng: 'en'
    })
    
    1. Use the useTranslation hook in your components.

    This allows you to handle i18n without subpaths while using App Routing.

    Login or Signup to reply.
  2. You should take a different path Because there is a server components too
    First Download the following packages with this command:

    npm install @formatjs/intl-localematcher negotiator next-int && npm install -D @types/negotiator server-only
    

    I will separate the config Because it will be easier to change it later

    i18n.config.ts

    export const i18n = {
      defaultLocale: "en",
      locales: ["en", "de"], /* All locales you have */
    } as const
    
    export type Locale = (typeof i18n)["locales"][number]
    

    Then we will make the configuration for the server:

    dictionary.ts

    import "server-only"
    
    import type { Locale } from "@/config/i18n.config"
    
    const dictionaries = {
      "en": () => import("pathToJsonFile/en.json").then((module) => module.default),
      "de": () => import("pathToJsonFile/de.json").then((module) => module.default),
    }
    
    /* We will use getDictionary function for server components*/
    export const getDictionary = async (locale: Locale) => dictionaries[locale]() 
    
    

    We need to create a function to detect locale

    import { NextRequest } from "next/server"
    import { match as matchLocale } from "@formatjs/intl-localematcher"
    import Negotiator from "negotiator"
    
    function getLocale(request: NextRequest): string | undefined {
      const languageCookie = request.cookies.get("NEXT_LOCALE")?.value
      if (languageCookie) {
        return languageCookie
      }
      const negotiatorHeaders: Record<string, string> = {}
      request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))
    
      // @ts-ignore locales are readonly
      const locales: string[] = i18n.locales
      const languages = new Negotiator({ headers: negotiatorHeaders }).languages()
    
      const locale = matchLocale(languages, locales, i18n.defaultLocale)
      return locale
    }
    

    Than we need middleware:

    middleware.ts

    
    import { type NextRequest } from "next/server"
    import createIntlMiddleware from "next-intl/middleware"
    
    /* I am creating a new function because if you have more than 1 middleware it will be easier to manage but You can directly use it with default export */
    export const MultiLanguageMiddleware = (request: NextRequest) => {
      const defaultLocale =
        getLocale(request) || request.headers.get("x-default-locale") || "en"
    
      const intlMiddleware = createIntlMiddleware({
        /* @ts-ignore */
        locales: i18n.locales,
        localePrefix: "never", /* we will give "never" value because we don't want domain or prefix based locale. */
        defaultLocale: defaultLocale || "en-US", 
        localeDetection: false,
      })
    
      return intlMiddleware(request)
    }
    
    export async function middleware(request: NextRequest) {
    
     /* Then you can can create custom logic or directly call here */
    
    return MultiLanguageMiddleware
    }
    
    export const config = {
      // Skip all paths that should not be internationalized. This example skips the
      // folders "api", "_next" and all files with an extension (e.g. favicon.ico)
      matcher: ['/((?!api|_next|.*\..*).*)']
    };
    

    Then we have to define types for server component IntelliSense:

    global.d.ts

    type Messages = typeof import("pathToYourFile/en.json")
    declare interface IntlMessages extends Messages {}
    

    Now we need a provider for client components:

    providers.tsx

    "use client"
    
    import { FC, useEffect, useState } from "react"
    import { NextIntlClientProvider } from "next-intl"
    
    interface ProvidersProps {
      children: React.ReactNode
      lang: string
      messages: any
    }
    
    const Providers: FC<ProvidersProps> = ({ children, lang, messages }) => {
      /* We are ensuring we are in client because of avoiding next.js 13 hydration errors */
      const [isMounted, setIsMounted] = useState(false)
      useEffect(() => {
        setIsMounted(true)
      }, [])
      if (!isMounted) return null
    
      return (
        
          <NextIntlClientProvider locale={formatLanguage(lang)} messages={messages}>
          {children}
          </NextIntlClientProvider>
        
      )
    }
    
    export default Providers
    

    before we add our layout file we have to move every route in the app directory to the [lang] folder like this:
    enter image description here

    layout.tsx

    import { i18n, Locale } from "pathToConfig/i18n.config"
    import Providers from "pathToProvidersComponent/providers"
    
    export default async function RootLayout({
      children,
      params: { lang },
    }: {
      children: React.ReactNode
      params: { lang: Locale }
    }) {
      let messages
      try {
        messages = (await import(`pathToFile/lang.json`)).default
      } catch (error) {
        console.error(error)
    /* our fallback locale */
        messages = (await import(`pathToFile/en.json`)).default
      }
      return (
        <html lang={lang}>
          <body>
              <Providers lang={lang} messages={messages}>
               {children}
              </Providers>
          </body>
        </html>
      )
    }
    
    

    client component usage:

    "use client"
    import { useTranslations } from "next-intl"
    
    const Page = () => {
      const t = useTranslations("yours")
    
    
    return (
    <div>
    {t("path.to.message")}
    </div>
    )
    
    }
    
    

    server component usage:

    import { getDictionary } from "filePath/dictionary"
    import { useTranslations } from "next-intl"
    import { Locale } from "pathToI18nConfig/i18n.config"
    
    interface pageProps {
      params: {
        lang: Locale
      }
    }
    
    const Page: FC<pageProps> = async ({ params: { lang } }) => {
      const translation = await getDictionary(lang)
    
    return (
    <div>
    {translation.yours}
    </div>
    )
    
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search