Is it possible to use the layout in the _app.jsx component with next-i18next?
Asked Answered
R

1

5

To create a site, I use nextjs, when creating pages, I took the general layout with the header and footer into a separate hoc component and wrapped the page components in the file with it _app.jsx:

function App({ Component, ...rest }) {

  const { store, props } = wrapper.useWrappedStore(rest)

  return (
    <Provider store={store}>
      <Layout>
        <Component {...props.pageProps} />
      </Layout>
    </Provider>
  )
}

Everything worked fine until localization became a problem, after using the next-18next library for translations and adding serverSideTranslations, two errors began to appear on each page:

 react-i18next:: You will need to pass in an i18next instance by using initReactI18next
frontend-node_1      | TypeError: Cannot read properties of undefined (reading 'label')
frontend-node_1      |     at DropdownSwitcher (webpack-internal:///./src/components/header/translation/DropdownSwitcher.jsx:45:36)
frontend-node_1      |     at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5658:16)
frontend-node_1      |     at renderIndeterminateComponent (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5731:15)
frontend-node_1      |     at renderElement (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5946:7)
frontend-node_1      |     at renderMemo (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5868:3)
frontend-node_1      |     at renderElement (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6011:11)
frontend-node_1      |     at renderNodeDestructiveImpl (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6104:11)
frontend-node_1      |     at renderNodeDestructive (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6076:14)
frontend-node_1      |     at renderNode (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6259:12)
frontend-node_1      |     at renderHostElement (/app/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5642:3)

The error with "label" occurs because the i18n object is empty on the server:

const DropdownSwitcher = () => {
  const { i18n } = useTranslation()

  const currentLanguage = useMemo(() => {             // language as undefined 
    return LANGUAGES.find((item) => item.language === i18n.language)
  }, [i18n.language])
  ....

But everything is fine on the client and there are no errors. What could be the reason and how to fix it, since the App itself from the _app.jsx file is wrapped in appWithTranslation from next-i18next.

Therefore, two questions arise, how to fix react-i18next:: You will need to pass in an i18next instance by using initReactI18next and why there is no i18n object on the server?

I moved the layout to the level of the page itself, removing it from _app.js, but for some reason, then something, useEffect() is repeated in the header, although the header component has not changed in any way and bringing the layout to the level of _app.jsx fixes it

If there is not enough information or you need a visual example, I will try to create a small program that demonstrates this with open source. Please write in a comment.

Roguery answered 9/10, 2022 at 13:50 Comment(0)
R
1

I solved my problem, but I forgot to provide an answer here, but I noticed that someone also has this problem, so I will try to help people who come across this post, although it is relevant only for nextjs version 12, since with the appearance of version 14, the structure there has improved a lot with as I think there should be no more questions like mine.

1. Rendering the layout

In the official doc, there is a whole section that describes how to correctly divide the layout so that it works according to the SPA type.

pages/index.jsx

// pages/index.jsx

import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'

export default function Page() {
  return (
    /** Your content */
  )
}

Page.getLayout = function getLayout(page) {
  return (
    <Layout>
      <NestedLayout>{page}</NestedLayout>
    </Layout>
  )
}

pages/_app.js

// pages/_app.js

export default function MyApp({ Component, pageProps }) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

This component method approach is much better than using its direction in _app.jsx because you can extend or replace them and not make a crude monolith, example how I used it:

// pages/ingex.jsx

function HomePage() {
  return (
    <HomeLayout>
      <Main />
    </HomeLayout>
  )
}

HomePage.getLayout = (page) => <MainLayout>{page}</MainLayout>
// pages/about-us.jsx

const AboutUsPage = () => {
  return (
    <>
      <HomeLayout>
        <AboutUs />
      </HomeLayout>
    </>
  )
}

AboutUsPage.getLayout = (page) => (
  <MainLayout withNav>
    <LayoutContext.Consumer>
      {({ device }) => device.isMobile && <NavigationMobile />}
    </LayoutContext.Consumer>
    {page}
  </MainLayout>
)

With this approach, react still works like a spa and a similar page to about-us, which will also have NavigationMobile, will simply compare it.

2. Error with next-i18next

The whole point was that the next-i18next library was configured incorrectly in the first place (more precisely, it needed to be corrected). In order to configure everything correctly, I had to do the following:

- Move the folder with translation files to the public folder. This is necessary so that the library config, which we will configure a little below, can see the translation files and interact with them

- Configure next-i18next.config.js to work with the client. Here is an example setup with some comments. And also a link to the documentation, and some other resources I found while setting up.

next-i18next.config.js

const path = require('path')

const LANGUAGES = ['en', 'pl', 'uk']
const DEFAULT_LANGUAGE = 'en'

// if it is the server, then the full path, if the client, then the relative path.
const localePath =
  typeof window === 'undefined' ? path.resolve('public', 'translation') : '/public/translation'

module.exports = {
  i18n: {
    defaultLocale: DEFAULT_LANGUAGE,
    locales: LANGUAGES,
    fallbackLng: LANGUAGES,
    nsSeparator: '::',
    keySeparator: '::',
    // How to use libraries for i18next like LanguageDetector
    use: [require('i18next-intervalplural-postprocessor')],
    serializeConfig: false,
  },
  localePath: localePath,
}

- Configure next-i18next in the _app.jsx file. Here everything is as described in the documentation.

import { appWithTranslation } from 'next-i18next'
import nextI18NextConfig from '../../next-i18next.config'


function App({ Component, ...rest }) {
  const { store, props } = wrapper.useWrappedStore(rest)
  const getLayout = Component.getLayout || ((page) => page)

 //WARNING!!! You don't have to have your own i18next initialization like i18next.use(LanguageDetector).use(intervalPlural).init({ detection: options }) this is all done by the next-i18next library

  return (
    <Provider store={store}>
      <AppHOC>{getLayout(<Component {...props.pageProps} />)}</AppHOC>
    </Provider>
  )
}

export default appWithTranslation(App, nextI18NextConfig)

- You need to pass the config when calling the serverSideTranslations function. To make your life easier, it is better to transfer the implementation of this function to another file, here is an example of how I did it:

// utils/serverSideTranslations.js

import { serverSideTranslations as baseServerSideTranslations } from 'next-i18next/serverSideTranslations'
import { dt } from '../../constants/defaultTranslate'
import { DEFAULT_LANGUAGE } from '../../constants/languages'
import nextI18NextConfig from '../../../next-i18next.config.js'

const serverSideTranslations = async (locale, domains = []) => {
  return await baseServerSideTranslations(locale, [...dt, ...domains], nextI18NextConfig, [
    DEFAULT_LANGUAGE,
  ])
}

export default serverSideTranslations

- And finally, use this function on the pages.

import MainLayout from '../components/layouts/MainLayout'
import serverSideTranslations from '../utils/serverSideTranslations'
import HomeLayout from '../components/home/HomeLayout'
import Main from '../components/home/main/Main'

function HomePage() {
  return (
    <HomeLayout>
      <Main />
    </HomeLayout>
  )
}

HomePage.getLayout = (page) => <MainLayout>{page}</MainLayout>

export const getServerSideProps = async ({ locale }) => {
  // Wrapping in Promis.all is not necessary, I use it simply so that if there are any other asynchronous operations, then not to use them through await and not to block each other's work
  const [translations] = await Promise.all([
    serverSideTranslations(locale, ['home']),
  ])

  return {
    props: {
      ...translations,
    },
  }
}

export default HomePage

I hope this helped someone, if you have any comments, write in the comments

Roguery answered 16/1, 2023 at 13:8 Comment(1)
Thanks! Setting up localePath solved it for meIngraft

© 2022 - 2024 — McMap. All rights reserved.