aralroca / next-translate

Next.js plugin + i18n API for Next.js 🌍 - Load page translations and use them in an easy way!
MIT License
2.62k stars 208 forks source link

Translation not working on client component inside layout #1173

Open zecka opened 9 months ago

zecka commented 9 months ago

What version of this package are you using? next-translate: ^2.6.2 next-translate-plugin: ^2.6.2 next: 14.0.3

What operating system, Node.js, and npm version? node: 18.17.1 npm: 9.6.7 yarn: 1.22.19

What happened? I'm using app router on nextjs In a client component used on a layout, i use useTranslation. On server rende, all translations is correct. But on client side the translation is not loaded.

The same component work well when used on page.tsx

// src/components/TestClient.tsx
'use client';
import useTranslation from 'next-translate/useTranslation';
export const TestClient = () => {
  const { t } = useTranslation('common');
  return <div>This is client component, translation value: {t('test')}</div>;
};
// src/components/TestServer.tsx
import useTranslation from "next-translate/useTranslation";
export const TestServer = () => {
  const { t } = useTranslation("common");
  return <div>This is server component, translation value: {t("test")}</div>;
};
// src/app/layout.tsx
import { TestClient } from "@/components/TestClient";
import { TestServer } from "@/components/TestServer";
export default function RootLayout({ children}: {children: React.ReactNode;}) {
  return (
    <html lang="en">
      <body>
        <h2>Inside LAYOUT</h2>
        <TestClient />
        <TestServer />
        {children}
      </body>
    </html>
  );
}
// src/app/page.tsx
import { TestClient } from "@/components/TestClient";
import { TestServer } from "@/components/TestServer";
const Page= () => {
  return (
    <div>
      <h2>Inside PAGE</h2>
      <TestServer />
      <TestClient />
    </div>
  );
};

Screenshot of render from server (with javascript browser disabled)

image

Screenshot of render from client(with javascript browser enabled)

image

How reproduce?

Github repository for this issue: https://github.com/zeckaissue/next-translate-issue-client-on-layout Codesandbox for this issue: https://codesandbox.io/p/github/zeckaissue/next-translate-issue-client-on-layout/main?file=%2Fsrc%2Fapp%2Flayout.tsx%3A21%2C23&workspaceId=08c0217d-10c7-486d-a55b-8417490cf07b

EDIT:

  1. This seems to be related to https://github.com/aralroca/next-translate-plugin/issues/75
  2. This issue doesn't appear on next13 with next-translate-plugin 2.4.4 (Check this branch of my reproduction repository: https://github.com/zeckaissue/next-translate-issue-client-on-layout/tree/next13)
  3. This still work with next-translate-plugin 2.6.2 on next 13 (https://github.com/zeckaissue/next-translate-issue-client-on-layout/tree/next13-next-translate-plugin-2.6.2)
bmirandan commented 9 months ago

My team encountered the same issue just yesterday. We even did a test to check this out and we've been able to replicate it. It seems to be a problem with the last version only 'cause we downgraded and it works client side.

fuetgeo commented 9 months ago

sounds a lot like https://github.com/aralroca/next-translate/issues/1158 to me. this issue makes the package unusable for many projects

acidfernando commented 8 months ago

Yeah, it's the same issue as #1158. I created a repro: https://codesandbox.io/p/devbox/relaxed-kapitsa-59xwtj

i2gor87 commented 7 months ago

Any updates on this? Because I tend to use one layout tsx for almost all pages where I need consistent layout and this breaks things for me

mikebywaters commented 7 months ago

+1 running into this issue.

i2gor87 commented 7 months ago

I might have a solution here, but it might not be suitable for everyone. For me it does only half the work, because I want to use dynamic language changes via queryParams and this solution does not address this issue (but the content is translated and header on reload is translated as well). I hope someone can provide a more suited solution

Diving into the next.js docs, I found out that

layouts preserve state, remain interactive, and do not re-render.

but there is another option - templates which

are similar to layouts in that they wrap each child layout or page. Unlike layouts that persist across routes and maintain state, templates create a new instance for each of their children on navigation.

That being said, if you have this structure

// src/components/translated-component.tsx
'use client';
import useTranslation from 'next-translate/useTranslation';
export const TranslatedComponent = () => {
  const { t } = useTranslation('common');
  return <div>This is component that needs translation, translation value: {t('test')}</div>;
};
// src/app/layout.tsx
import type {Metadata} from 'next'
import {ReactNode} from "react";
import TranslatedComponent from "@/components/translated-component";
import './globals.css'

export const metadata: Metadata = {
    title: 'Website title',
    description: 'Website description',
}

export default function LocaleLayout({ children } : { children: ReactNode }) {
    return (
        <html lang='en' suppressHydrationWarning>
        <body>
            <TranslatedComponent />
            {children}
        </body>
        </html>
    )
}

and you run into issue mentioned before, you can take out TranslatedComponent from layout.tsx (or js, or jsx) and put it into new file template.tsx. But only moving the component won't solve the issue, you need to use useTranslation inside template.tsx and pass translated strings as props to problematic element:

// src/components/translated-component.tsx
export const TranslatedComponent = ({value} : {value:string}) => {
  return <div>This is component that needs translation, translation value: {value}</div>;
};
// src/app/layout.tsx
import type {Metadata} from 'next'
import {ReactNode} from "react";
import './globals.css'

export const metadata: Metadata = {
    title: 'Website title',
    description: 'Website description',
}

export default function LocaleLayout({ children } : { children: ReactNode }) {
    return (
        <html lang='en' suppressHydrationWarning>
        <body>
            // <TranslatedComponent /> // remove this line
            {children}
        </body>
        </html>
    )
}
// src/app/template.tsx
import TranslatedComponent from "@/components/translated-component";
import useTranslation from 'next-translate/useTranslation';

export default function Template({children}: { children: React.ReactNode }) {
    const { t } = useTranslation('common');
    return <div>
        <TranslatedComponent value{t('test')}/>
        {children}
        <Footer/>
    </div>
}

which would result in following nested structure:

<Layout>
   <Template>{children}</Template>
</Layout>

Basically template starts doing the work of layout, but with useEffect and other client functions

More info in NextJS documentation I also including link to corrected sandbox that I forked from https://github.com/aralroca/next-translate/issues/1173#issuecomment-1871995857

P.S. I'm not very experienced with next.js and typescript, so if there are any hidden problems with using template file over layout file, I really would like to know. But this setup is working for me so far. P.S.S Before fix I just put my dynamic components in page. But that's weird solution =(

acidfernando commented 7 months ago

That works for my use case. I think this is not an error with next-translate but with how nextJS works. I think this issue should be closed with this fix

Also, it would be interesting to have this knowledge somewhere in the docs, I think people will be running into this quite frequently

I'm not sure where to add it, though 😅

i2gor87 commented 7 months ago

@acidfernando This still doesn't solve the issue for people who would like to use the dynamic translation by lang={locale} method. For some reason, the template.tsx is updated only on page transitions, changing translation on the page translates only content, but not strings in the component in the template.tsx. You can see that behaviour in my sandbox - clicking on english / french buttons changes translations for body component, but not the component inside template

kristian240 commented 3 months ago

One thing that might be helpful here is the fact that I experience this when I render the children behind a client-only gurad (we use this pattern https://github.com/gfmio/react-client-only)

When component is not wrapped with client only guard the translation is loaded (but only if the client component is rendered after the {children}.

matmkian commented 3 months ago

Do we have some updates regarding this issue? It's pretty huge. I personally had to use another lib to manage translations because of this :-( It's sad because I love this lib 👍

EdenCoder commented 3 weeks ago

+1, this is a huge breaking issue for us...

isBatak commented 2 weeks ago

This might be a possible solution:

// src/app/layout.tsx
import { TestClient } from "@/components/TestClient";
import { TestServer } from "@/components/TestServer";
import I18nProvider from 'next-translate/AppDirI18nProvider';

export default function RootLayout({ children}: {children: React.ReactNode;}) {
  const { config, lang, namespaces } = globalThis.__NEXT_TRANSLATE__;
  return (
    <html lang="en">
      <body>
       <I18nProvider
         lang={lang}
     namespaces={namespaces}
     config={JSON.parse(JSON.stringify(config))}
    >
          <h2>Inside LAYOUT</h2>
          <TestClient />
          <TestServer />
          {children}
        </I18nProvider>
      </body>
    </html>
  );
}

Make sure to import Provider like this import I18nProvider from 'next-translate/AppDirI18nProvider'; because next-translate-plugin also imports this module here https://github.com/aralroca/next-translate-plugin/blob/99c1faefcd7167a7caa309f2affc7d0d807a76e4/src/templateAppDir.ts#L95

globalThis.__NEXT_TRANSLATE__ is available on the server side because next-translate-plugin actually wrap your component source code with some stuff that is doing side-effects: https://github.com/aralroca/next-translate-plugin/blob/99c1faefcd7167a7caa309f2affc7d0d807a76e4/src/templateAppDir.ts#L127

Additionally, AppDirI18nProvider name is kinda wrong because this component is not a provider, it just doing a side-effect in the render, which seems pretty wrong.

export default function AppDirI18nProvider({
  lang,
  namespaces = {},
  config,
  children,
}: AppDirI18nProviderProps) {
  globalThis.__NEXT_TRANSLATE__ = { lang, namespaces, config } // this is a side-effect

  // It return children and avoid re-renders and also allow children to be RSC (React Server Components)
  return children
}

What might also work is to use Script component like this:


export default function RootLayout({ children}: {children: React.ReactNode;}) {
  const { config, lang, namespaces } = globalThis.__NEXT_TRANSLATE__;

  return (
    <html lang="en">
      <head>
        <Script id={lang}>
          {`globalThis.__NEXT_TRANSLATE__ = ${{ lang, namespaces, config }}`}
        </Script>
      </head>
      <body>
      <h2>Inside LAYOUT</h2>
      <TestClient />
      <TestServer />
      {children}
      </body>
    </html>
  );
}

@aralroca What do you think of this Script solution? Script also have strategy settings to assure it's executed on time https://nextjs.org/docs/app/building-your-application/optimizing/scripts#strategy

aralroca commented 2 weeks ago

@aralroca What do you think of this Script solution? Script also have strategy settings to assure it's executed on time https://nextjs.org/docs/app/building-your-application/optimizing/scripts#strategy

Good to know it @isBatak , probably we can add it internally and solve this AppDirI18nProvider wrong implementation. Lately I am quite busy, I will try to dedicate some time to it when I can, otherwise any PR will be welcome.