QuiiBz / next-international

Type-safe internationalization (i18n) for Next.js
https://next-international.vercel.app
MIT License
1.26k stars 59 forks source link

Client Components mismatching translations during static rendering #190

Closed landvibe closed 11 months ago

landvibe commented 11 months ago

After executing next build, if I open the .next/server/app/en.html file, I can see that the keys inputted into the t function of useI18n are exposed. In other words, the translated values are not displayed. The t function of getI18n works properly. Is this a known issue?

QuiiBz commented 11 months ago

Could you share a minimal reproduction? This seems like a bug that I haven't experienced before.

gustaveWPM commented 11 months ago

Lol, I have the same issue. Oops

It can also lead to some Cumulative Layout Shift (CLS) issues, which isn't very pleasant either to see in the Pagespeed Insights. :/

QuiiBz commented 11 months ago

Closing in favor of #191 which provides a reproduction.

gustaveWPM commented 11 months ago

Closing in favor of #191 which provides a reproduction.

ping @QuiiBz Oh, srsly? Idk if the snippets really help to reproduce the bug. Does it? I just sent some lines of my code, dunno if it is a real minimal reproduction.

gustaveWPM commented 11 months ago

I think this issue also points to another problem, because "Fallbacking on the fallback locale" as I mentioned in my issue (https://github.com/QuiiBz/next-international/issues/191) would already be a good step forward, but this issue also takes into account the problem of having to statically generate the various contents with the right language (/fr/, /en/, /it/, etc).

Whereas my issue only points the problem to fallback on the fallback language only (passed to <I18nProviderClient>).

QuiiBz commented 11 months ago

@gustaveWPM see https://github.com/QuiiBz/next-international/issues/191#issuecomment-1733944775 for your issue - it should also solve the static rendering bug.

@landvibe I'll need a reproduction to debug your issue, as I'm not able to reproduce it in the example.

gustaveWPM commented 11 months ago

@gustaveWPM see #191 (comment) for your issue - it should also solve the static rendering bug.

@landvibe I'll need a reproduction to debug your issue, as I'm not able to reproduce it in the example.

Thank you!

I no longer have any raw locales keys in the generated files.

However, exactly what I was afraid of happened: all statically generated files are generated in the default language (actually, I think it's the fallbackLocale).

So I have French in my generated files for English routes. :<

Languages mismatch

All values correspond to the "fr" locale in this generated file. I've just highlighted the areas where you can see it clearly. I use a few anglicisms in my "fr" language, sorry, it can be confusing.

Languages are not blending here (anyway, that would be such strange behavior that I don't think it's technically possible for it to happen, lol. But I'd rather try to be unambiguous).

QuiiBz commented 11 months ago

I think that's because you have a 'use client' in your root layout, so your pages are also client and thus when pre-rendered, uses the fallbackLocale.

I'm wondering how Next.js behaves if we trigger a loading state to force the correct locale to load completely before pre-rendering a page. I'll dig later.

gustaveWPM commented 11 months ago

I think that's because you have a 'use client' in your root layout, so your pages are also client and thus when pre-rendered, uses the fallbackLocale.

I'm wondering how Next.js behaves if we trigger a loading state to force the correct locale to load completely before pre-rendering a page. I'll dig later.

I think you're right about my root layout, haha.

'use client';

import '@/app/globals.css';
import NextTopLoader from '@/components/misc/NextTopLoader';
import SplashScreen from '@/components/misc/SplashScreen';
import SitewideNavbar from '@/components/navbar/SitewideNavbar';
import { fallbackLocale } from '@/config/i18n';
import { ProgressbarConfig } from '@/config/progressbar';
import SessionProvider from '@/contexts/SessionProvider';
import { I18nProviderClient, useCurrentLocale } from '@/i18n/client';
import { LayoutMinimalProps } from '@/types/CustomUtilitaryTypes';

export default function RootLayout({ children }: LayoutMinimalProps) {
  return (
    <I18nProviderClient fallbackLocale={fallbackLocale}>
      <html lang={useCurrentLocale()}>
        <body className="flex flex-col h-screen w-full p-0 m-0">
          <SplashScreen />
          <NextTopLoader {...{ ...ProgressbarConfig.PROPS }} />
          <SessionProvider>
            <SitewideNavbar />
            {children}
          </SessionProvider>
        </body>
      </html>
    </I18nProviderClient>
  );
}
gustaveWPM commented 11 months ago

Well, I tried to workaround it like this:

// RootLayoutClient.tsx

'use client';

import NextTopLoader from '@/components/misc/NextTopLoader';
import SplashScreen from '@/components/misc/SplashScreen';
import SitewideNavbar from '@/components/navbar/SitewideNavbar';
import { fallbackLocale } from '@/config/i18n';
import PROGRESSBAR_CONFIG from '@/config/progressbar';
import SessionProvider from '@/contexts/SessionProvider';
import { I18nProviderClient, useCurrentLocale } from '@/i18n/client';
import { LayoutMinimalProps } from '@/types/CustomUtilitaryTypes';
import { FunctionComponent } from 'react';

interface RootLayoutClientProps extends LayoutMinimalProps {}

const RootLayoutClient: FunctionComponent<RootLayoutClientProps> = ({ children }) => {
  return (
    <I18nProviderClient fallbackLocale={fallbackLocale}>
      <html lang={useCurrentLocale()}>
        <body className="flex flex-col h-screen w-full p-0 m-0">
          <SplashScreen />
          <NextTopLoader {...{ ...PROGRESSBAR_CONFIG }} />
          <SessionProvider>
            <SitewideNavbar />
            {children}
          </SessionProvider>
        </body>
      </html>
    </I18nProviderClient>
  );
};

export default RootLayoutClient;
// src/app/[locale]/layout.tsx

import '@/app/globals.css';
import RootLayoutClient from '@/components/_workarounds/RootLayoutClient';
import { LayoutMinimalProps } from '@/types/CustomUtilitaryTypes';

export default function RootLayout({ children }: LayoutMinimalProps) {
  return <RootLayoutClient {...{ children }} />;
}

But it changed absolutely nothing. :<

QuiiBz commented 11 months ago

I've looked a bit and the issue occurs when using a client component while pre-rendering a page. Because locales are lazy-loaded on the client, the fallbackLocale is used in all pages.

An easy fix would be to set the correct fallbackLocale based on the current locale params:

<I18nProviderClient fallbackLocale={params.locale === 'en' ? en : fr}>
  {children}
</I18nProviderClient>
gustaveWPM commented 11 months ago

Great! I'll try it ASAP.

Maybe fallbackLocale could be initialized "For free" in a next release?

Because locales are lazy-loaded on the client

Importing them all to match the fallbackLocale with the currentLocale isn't breaking this lazy-loading approach, though?

gustaveWPM commented 11 months ago

Hmmm...

import de from '@/i18n/locales/de';
import en from '@/i18n/locales/en';
import fr from '@/i18n/locales/fr';

export const LANGS: Record<LanguageFlag, VocabType> = {
  fr,
  en,
  de
};
const I18nProvider: FunctionComponent<I18nProviderProps> = ({ children }) => (
  <I18nProviderClient fallbackLocale={LANGS[useCurrentLocale()]}>{children}</I18nProviderClient>
);

export default i18nProvider;

It hotfixed the issue. I'm not a big fan of having to manually import all the languages files, do you think you'll be able to do something a bit more "For free" in a future version? Maybe thank to the locales object passed to createI18nClient() ?

QuiiBz commented 11 months ago

The issue with this approach is that all locales are now loaded client-side, and that's not something we want. We have to find a way to trigger to dynamically load the requested locale and wait until it's complete to continue pre-rendering (the current behavior doesn't wait and pre-renders immediately, causing it to show the keys OR the fallback locale).

gustaveWPM commented 11 months ago

The issue with this approach is that all locales are now loaded client-side, and that's not something we want. We have to find a way to trigger to dynamically load the requested locale and wait until it's complete to continue pre-rendering (the current behavior doesn't wait and pre-renders immediately, causing it to show the keys OR the fallback locale).

Would it be possible to get rid off fallbackLocale in the I18nProviderClient and just load it via the useCurrentLocale() hook, based on the locales object provided in createI18nClient()?

Something like:

export const { useI18n: getClientSideI18n, useScopedI18n, I18nProviderClient, useCurrentLocale } = createI18nClient({
  fr: () => import('@/i18n/locales/fr'),
  en: () => import('@/i18n/locales/en'),
  de: () => import('@/i18n/locales/de')
});
const currentLocale = useCurrentLocale();
// => 'fr' | 'en' | 'de'
await loadLocale(currentLocale);
/* => {
  fr: () => import('@/i18n/locales/fr'),
  en: () => import('@/i18n/locales/en'),
  de: () => import('@/i18n/locales/de')
}[currentLocale]
*/

Idk if it is achievable? Is there any "Magic trick" to await loadLocale here?

QuiiBz commented 11 months ago

That's the goal yes, but this doesn't solve our issue:

We have to find a way to trigger to dynamically load the requested locale and wait until it's complete to continue pre-rendering

gustaveWPM commented 11 months ago

That's the goal yes, but this doesn't solve our issue:

We have to find a way to trigger to dynamically load the requested locale and wait until it's complete to continue pre-rendering

Yep. :/ Idk. Sorry.

The only thing I could suggest would be an horrible implementation, using setInterval, but it's awful and probably not reliable in real-world scenarios.

Maybe Streaming ? Idk how this behaves with the static HTML files generation in a SSR context: https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

The new content is automatically swapped in once rendering is complete.

(It doesn't look good, though...)

Since I'm a NextJS beginner, I'm probably talking nonsense.

gustaveWPM commented 11 months ago

Maybe a server action?

I'm curious. The "Challenge" looks like to only trigger the first-render when the data has been loaded. But it's somehow at the opposite of the Next.js philosophy concerning client components.

It is also stressing me since I don't know if it is possible to detect the SSR processing execution context, and hence do some magic.

It might also be stressful if it is badly done and makes Next.js firing errors… (Like Text content does not match server-rendered HTML).

I find this scenario quite unusual, and I'd be glad to know what answer Next.js can provide for this.

QuiiBz commented 11 months ago

I got something working with React's use() hook and will make a PR this weekend to hopefully fix the issue, and remove the need for fallbackLocale

gustaveWPM commented 11 months ago

I got something working with React's use() hook and will make a PR this weekend to hopefully fix the issue, and remove the need for fallbackLocale

GG! I was completely unaware of this React feature. Looking forward to see this PR.

QuiiBz commented 11 months ago

@gustaveWPM could you try installing next-international@rc and removing both fallback and fallbackLocale props from I18nProviderClient?

DennieMello commented 11 months ago

@gustaveWPM could you try installing next-international@rc and removing both fallback and fallbackLocale props from I18nProviderClient?

When you press the back button in the browser, an error occurs - "Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server."

QuiiBz commented 11 months ago

Sorry, I forgot to mention that you also have to wrap I18nClientProvider with a Suspense boundary:

<Suspense fallback={<p>Loading...</p>}>
  <I18nProviderClient>{children}</I18nProviderClient>
</Suspense>
gustaveWPM commented 11 months ago

@gustaveWPM could you try installing next-international@rc and removing both fallback and fallbackLocale props from I18nProviderClient?

Yeh. Gonna try it rn.

gustaveWPM commented 11 months ago

Sorry, I forgot to mention that you also have to wrap I18nClientProvider with a Suspense boundary:

<Suspense fallback={<p>Loading...</p>}>
  <I18nProviderClient>{children}</I18nProviderClient>
</Suspense>

Hmm. The SSG seems to be fixed on my project too.

But I have some flickering when I start the website for the first time.

export const I18nProvider: FunctionComponent<I18nProviderProps> = ({ children }) => (
  <Suspense>
    <I18nProviderClient>{children}</I18nProviderClient>
  </Suspense>
);

I guess that children is re-rendered?

EDIT:

LOL! It seems that the ENTIRE document is re-rendered, so I have a big white flash.

It is due to this:

export default function RootLayout({ children }: LayoutMinimalProps) {
  return (
    <I18nProvider>
      <HtmlElement {...{ children }} />
    </I18nProvider>
  );
}
const HtmlElement: FunctionComponent<HtmlElementProps> = ({ children }) => (
  <html lang={useCurrentLocale()}>
    <body className="flex flex-col min-h-screen w-full p-0 m-0">
      <NextTopLoader {...{ ...PROGRESSBAR_CONFIG }} />
      <SessionProvider>
        <SitewideNavbar />
        {children}
      </SessionProvider>
    </body>
  </html>
);

I would love to refactor it, but since I need to call useCurrentLocale() for the lang attribute of the HTML tag... it looks compromised.

Maybe I should try to use a Memo?

QuiiBz commented 11 months ago

I18nProviderClient should be around the children directly, instead of wrapping even the html, body... tags. Instead of using useCurrentLocale() for the lang prop, you can use the params prop from Next.js to retrieve the locale segment:

const HtmlElement = ({ children, params }: { children: ReactElement, params: { locale: string } }) => (
  <html lang={params.locale}>
    <body className="flex flex-col min-h-screen w-full p-0 m-0">
      <NextTopLoader {...{ ...PROGRESSBAR_CONFIG }} />
      <SessionProvider>
        <SitewideNavbar />
        <I18nProvider>
          {children}
        </I18nProvider>
      </SessionProvider>
    </body>
  </html>
);
gustaveWPM commented 11 months ago

I18nProviderClient should be around the children directly, instead of wrapping even the html, body... tags. Instead of using useCurrentLocale() for the lang prop, you can use the params prop from Next.js to retrieve the locale segment:

const HtmlElement = ({ children, params }: { children: ReactElement, params: { locale: string } }) => (
  <html lang={params.locale}>
    <body className="flex flex-col min-h-screen w-full p-0 m-0">
      <NextTopLoader {...{ ...PROGRESSBAR_CONFIG }} />
      <SessionProvider>
        <SitewideNavbar />
        <I18nProvider>
          {children}
        </I18nProvider>
      </SessionProvider>
    </body>
  </html>
);

Thx! It looks fine now.

QuiiBz commented 11 months ago

Great, thanks for testing! I'll complete the PR and release it as part of the 1.1.0 release.

QuiiBz commented 11 months ago

I've published next-international@1.1.0-rc.1: to upgrade from next-international@1.0.1, simply remove the fallbackLocale prop from I18nProviderClient. You no longer need to manually add a Suspense boundary, as it is now done by next-international.

Static Rendering should now work as expected when having Client Components in a page (and should still work with fully Client Component pages)

gustaveWPM commented 11 months ago

Hmmm, @QuiiBz, have you any error like this in your browser console?

Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

QuiiBz commented 11 months ago

Can you share the PR/commit that have this error? I don't have it on the example app.

gustaveWPM commented 11 months ago

Can you share the PR/commit that have this error? I don't have it on the example app.

I'm refactoring a lot of things in my project, I'm not sure that it comes from the current Next-International RC.