Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NextJS App Router and RSC Support path #1698

Open
3 tasks
thekip opened this issue Jun 12, 2023 · 46 comments · Fixed by #1762 · May be fixed by #1945
Open
3 tasks

NextJS App Router and RSC Support path #1698

thekip opened this issue Jun 12, 2023 · 46 comments · Fixed by #1762 · May be fixed by #1945

Comments

@thekip
Copy link
Collaborator

thekip commented Jun 12, 2023

Since NextJS app router is marked as stable we should provide a path fo users how to use Lingui with that.

Few main caveats here:

  1. Server Components doesn't support Context, so you could not use <Trans> directly in RSC.
  2. There is no native support for i18n as in Pages router. So a migration path and some suggestions should be provided.

For the point number one there are few solutions:

  1. Users could use just t or i18n._ directly in server components. While that would work, it is hard to translate messages which may have other JSX elements inside.
  2. We can create a separate <TransWithoutContext> component (and macro as well), which will not use context and context would be passed as props directly to component.

Checklist:

  • Support Server components
  • Create example for nextjs with app router
  • Add guide to the documentation (Next.js tutorial #1876)
@raphaelbadia
Copy link

I think that the main challenge would be to be able to detect if the component is a server component or a client one.

Then the second problem would be to remove the useLingui() context hook in the Trans component.

For that, maybe you could just read the i18n instance on the server 🤔

Here's what I tried on local :

In my root layout, I do :

const RootLayout = async ({ children }: { children: ReactNode }): Promise<JSX.Element> => {
  const language = getLanguage();
  const messages = await getDictionary(language);
  i18n.loadAndActivate({ locale: language, messages });
...

Then in a page, I've logged the i18n object and my messages were stored in i18n.messages.

CleanShot 2023-06-19 at 12 16 24

So I just wrote in a RSC page :

const Page = (): JSX.Element => {
  const cookieStore = cookies();

  return (
    <div>
      <TransServer
        id="greeting"
        message="Hello {name}"
        values={{ name: 'Arthur' }}
      />
     // here I put the id that lingui generated
      <TransServer id="XkgyrZ" message="Vous gagnez {reward}" values={{ reward: 'a cookie' }} />

It seems to work !

CleanShot 2023-06-19 at 12 17 19@2x

For my TransServer component I just copied the Trans component in lingui and changed it like the following gist (only take a look at the latest diff) https://gist.github.com/raphaelbadia/82f1c202e57b557bf88ea04cbbc0be29/revisions

This approach is very naive, but it seems to work, what do you think about it?

@thekip
Copy link
Collaborator Author

thekip commented Jun 19, 2023

Extractor will not extract messages from your custom Trans components. But approach in the right direction. I wrote it as

We can create a separate element (and macro as well), which will not use context and context would be passed as props directly to component.

Something like

import { TransNoContext as Trans } from '@lingui/macro';
import {setupI18n, I18n} from "@lingui/core"

async function getI18n(local: string): I18n {
   // assume you already implemented a way to load your message catalog 
   const messages = await loadMessages();
   return setupI18n({locale, messages})
}

// RSC
export async function Page() => {
  const params = useParams();
  const i18n = await getI18n(params['locale']);

  return (<div>
    <Trans i18n={i18n}>Hello world!</Trans> 
   </div>)
}

So changes in macro and swc plugin would be needed as well as new component is introduced in lingui/react

@raphaelbadia
Copy link

raphaelbadia commented Jun 19, 2023

I don't know much about babel / swc, but couldn't these plugins read the file, see if it contains the string "use client", and automatically choose between TransNoContext and Trans?

Would be better for the user to write import { Trans } from '@lingui/macro'; in all cases 😃

Also, your RSC example would be great but are you sure it's necessary to do const i18n = await getI18n(); and pass i18n to the Trans component in every server component?

@thekip
Copy link
Collaborator Author

thekip commented Jun 19, 2023

I don't know much about babel / swc, but couldn't these plugins read the file, see if it contains the string "use client", and automatically choose between TransNoContext and Trans?

Would be better for the user to write import { Trans } from '@lingui/macro'; in all cases

Typescript users would not be happy. Explicit is always better than implicit. TransNoContext would have i18n prop as required, so developer could understand that they have to pass i18n instance in that. Where regular Trans would not have this requirement.

Also, your RSC example would be great but are you sure it's necessary to do const i18n = await getI18n(); and pass i18n to the Trans component in every server component?

Yes, it's not avoidable. The design of RSC & nextjs is strange in my opinion.

You could not use a global instance of i18n because your server is asynchronous, and could process few requests at the same time. That may cause that global instance would be switched to different language even before first request would be fully generated. Unfortunately, nextjs also does not expose any native request or even syntetic event / context for each ssr request where we can create and store i18n instance for later reuse. So the only one way i'm seeing is instantiating it explicitly where you need it.

I haven't dug too much in NextJs internals, i saw that they somehow implement useParams() and cookies() as a global functions. May be some compilation time transformations involved to make it work.

@raphaelbadia
Copy link

I've dug through @amannn's next-intl package and I found their approach interesting, here's how I (think ?) it works:

  • User has to create and i18n.ts file in the root repository that exports a function that fetch translations loadMessages()
  • User calls useTranslations() in a RSC
  • useTranslations() calls getRuntimeConfig from a fake package next-intl/config which is aliased by a webpack plugin to userland's i18n.ts

nextjs also does not expose any native request or even syntetic event / context for each ssr request where we can create and store i18n instance for later reuse.

react provides this capability with the cache() function so I think it would be possible to do something like next-intl.

From your previous example :

// in lingui.config.ts
import { setupI18n } from "@lingui/core"

export const getI18nInstance = cache(() => setupI18n());

export const loadMessages = cache(() => import(`@lingui/loader!./locales/${locale}/messages.po`))


// linguimonorepo/packages/react/server/TransNoContext.tsx
import { getI18nInstance } from '@lingui/webpack-plugin-get-userland-config';

export function TransNoContext(TransProps) {
    const i18n = getI18nInstance();
    const messages = loadMessages();

    return <Trans asUsual........../>
}

// then in RSC
export function Page() {
    return <Trans>Hello</Trans>
}

@raphaelbadia
Copy link

I haven't dug too much in NextJs internals, i saw that they somehow implement useParams() and cookies() as a global functions. May be some compilation time transformations involved to make it work.

I dug into the cookies() part of Nextjs internal, in their server they use an AsyncLocalStorage that wraps the whole render process. The cookies() and headers() fn just read the store (and it's read only). Would require to patch nextjs in node modules to add something to it, don't think it's possible

@amannn
Copy link

amannn commented Jun 20, 2023

@raphaelbadia Your summary from above looks correct to me! I found that @next/mdx also uses the approach with the webpack alias: https://github.com/vercel/next.js/blob/056ab7918fef27133072f62f41b991fa47e52543/packages/next-mdx/index.js#L24

In case you need an i18n routing middleware for Lingui to work with the App Router, the next-intl middleware is really decoupled from all the other i18n code, so you could likely use/suggest it too if you're interested!

The hardest part for next-intl currently is to retrieve the users locale in React components. We use a workaround where the locale is negotiated in the middleware and then read via a header from components. This works well, but unfortunately doesn't support SSG. The alternative is passing the locale through all component layers to your translation calls. I really hope that React Server Context becomes a thing to help with this.

I'm curious to follow along where Lingui ends up with the RSC support! 🙌

@thekip
Copy link
Collaborator Author

thekip commented Jun 20, 2023

Thanks for investigation.

export const getI18nInstance = cache(() => setupI18n());

We still need to create an i18n instance per request with specific locale. I don't see how this cache function could help.

In regular express application i would do something like:

app.use((req) => {
 // define and store instance in the middleware
   req.i18n = setupI18n(req.params.locale);
})

app.get('/page', (req) => {
  // use already crerated instance from Request, don't have to struggle with creating it every place. 
  console.log(req.i18n._('Hello'));
})

@paales
Copy link

paales commented Jun 27, 2023

I believe React.cache is a cache per request.

@raphaelbadia
Copy link

It is !

@jiangmaniu
Copy link

jiangmaniu commented Jul 28, 2023

What's the progress? Lingui is really great. I like it very much and thank you for your active maintenance, but this question may determine whether my project can use it. I'm looking forward to using Lingui in RSC.

@thekip
Copy link
Collaborator Author

thekip commented Jul 28, 2023

I was able to make it all work together with a lot of dirty words and some webpack magic. The very early PoC is here https://github.com/thekip/nextjs-lingui-rsc-poc

How it works

There few key moments making this work:

  1. I was able to "simulate" context feature in RSC with cache function usage. Using that, we can use <Trans> in any place in RSC tree without prop drilling.
// i18n.ts

import { cache } from 'react';
import type { I18n } from '@lingui/core';

export function setI18n(i18n: I18n) {
  getLinguiCtx().current = i18n;
}

export function getI18n(): I18n | undefined {
  return getLinguiCtx().current;
}

const getLinguiCtx = cache((): {current: I18n | undefined} => {
  return {current: undefined};
})

Then we need to setup Lingui for RSC in page component:

export default async function Home({ params }) {
  const catalog = await loadCatalog(params.locale);

  const i18n = setupI18n({
    locale: params.locale,
    messages: { [params.locale]: catalog },
  });

  setI18n(
    i18n,
  );

And then in any RSC:

const i18n = getI18n()
  1. Withing this being in place, we have to create a separate version of <Trans> which is using getI18n() instead of useLingui(). I did it temporary right in the repo by copy-pasting from lingui source.
  2. Having this is already enough, you can use RSC version in the server components and regular version in client. But that not really great DX, and as @raphaelbadia mentioned we can automatically detect RSC components and swap implementation thanks to webpack magic. This is done by:
    • macro configured to insert import {Trans} from 'virtual-lingui-trans'
    • webpack has a custom resolve plugin which depending on the context will resolve the virtual name to RSC or Regular version.
const TRANS_VIRTUAL_MODULE_NAME = 'virtual-lingui-trans';

class LinguiTransRscResolver {
  apply(resolver) {
    const target = resolver.ensureHook('resolve');
    resolver
      .getHook('resolve')
      .tapAsync('LinguiTransRscResolver', (request, resolveContext, callback) => {

        if (request.request === TRANS_VIRTUAL_MODULE_NAME) {
          const req = {
            ...request,
            // nextjs putting  `issuerLayer: 'rsc' | 'ssr'` into the context of resolver. 
            // We can use it for our purpose:
            request: request.context.issuerLayer === 'rsc'
              // RSC Version without Context (temporary a local copy with amendments)
              ? path.resolve('./src/i18n/Trans.tsx')
              // Regular version
              : '@lingui/react',
          };

          return resolver.doResolve(target, req, null, resolveContext, callback);
        }

        callback();
      });
  }
}

Implementation consideration:

  • Detecting language and routing is up to user implementation. I used segment based i18n by just creating app/[locale] folder. I think it's out of the Lingui scope.
  • We still need to configure separately Lingui for RSC and client components. It seems it's a restriction of RSC design which is not avoidable. So you have to use I18nProvider in every root with client components. (or do everything as RSC)

@413n
Copy link

413n commented Aug 28, 2023

Hi @thekip, I love your solution but I am encountering a small problem that maybe you could help me with.

Problem

I'm using the Provider as you have in your repo where you pass setupI18n({messages, locale}) to the I18nProvider. That works fine for just Client components.
On the side I also have a validations file (I'm using zod for forms) where I have some customs messages that I want to translate with i18n, like:

import { i18n } from "@lingui/core";

export const schemaUpdateAccountSettings = z.object({
  username: z.string().regex(usernameRegex, {
    message: i18n.t(
      "Username can only contain letters, numbers, underscores and dots.",
    ),
  }),
  locale: z.enum(languages),
});

I noticed that since you are creating a new i18n instance with setupI18n to pass to the provider, when I import this validation file in my Client component (wrapped by the provider), it does not get correct translation since, of course, the instance is different.

My current solution

What I was trying was reusing the @lingui/core instance also in the Provider.
I have this Client provider wrapped by a RSC that gets the locale and the messages and pass them as props.

I18nProvider.tsx

export const I18nProvider = async ({
  children,
}: PropsWithChildren<unknown>) => {
  const locale = await getLocale();
  const messages = getMessages(locale);

  return (
    <ClientI18nProvider locale={locale} messages={messages}>
      {children}
    </ClientI18nProvider>
  );
};

I18nProvider.client.tsx

// ... other imports
import { i18n } from "@lingui/core";

export const ClientI18nProvider = ({
    locale,
    messages,
    children,
  }: PropsWithChildren<{ locale: string; messages: Messages }>) => {
    i18n.load(locale, messages);
    i18n.activate(locale);

    return (
      <I18nProvider i18n={i18n}>
        {children}
      </I18nProvider>
    );
},

But unfortunately I'm getting a React error in the Client provider when I'm changing the locale:

Warning: Cannot update a component (`I18nProvider`) while rendering a different component (`ClientI18nProvider`). 

This is probably due to the Provider that internally changes state of locale when the props changes but I have no idea how to solve this.

Do you have other solution for my use case (i18n on files outside the provider but still Client side)?

@thekip
Copy link
Collaborator Author

thekip commented Aug 31, 2023

Hi all, since [email protected] there is no need to copy Trans implementation into the project. Check this commit: thekip/nextjs-lingui-rsc-poc@088d04a

@thekip
Copy link
Collaborator Author

thekip commented Aug 31, 2023

@413n firstly, this code is potentially broken:

import { i18n } from "@lingui/core";

export const schemaUpdateAccountSettings = z.object({
  username: z.string().regex(usernameRegex, {
    message: i18n.t(
      "Username can only contain letters, numbers, underscores and dots.",
    ),
  }),
  locale: z.enum(languages),
});

Due to zod's schema definition happened on the module level and will not react on language changes or might suffer from race conditions (when catalogs are not loaded yet).

A better approach would be to use msg macro, and store an ID of message in the zod schema. And retrieve real message from id in the place where you're actually executing your validations.

import { msg } from "@lingui/macro";

export const schemaUpdateAccountSettings = z.object({
  username: z.string().regex(usernameRegex, {
    message: (msg`Username can only contain letters, numbers, underscores and dots.`).id,
  }),
  locale: z.enum(languages),
});

Check the documentation for more info on this pattern https://lingui.dev/tutorials/react-patterns#lazy-translations

This will effectevely resolve your next problem, because you will not rely on the global i18n instance anymore.

@413n
Copy link

413n commented Sep 1, 2023

@thekip I installed the latest version but TransNoContext seems to not be exported. I checked in the index and it seems like that. Could you check?

@thekip I just cloned your PoC repo (using pnpm) and it gives this error
image

@413n
Copy link

413n commented Sep 1, 2023

import { msg } from "@lingui/macro";

export const schemaUpdateAccountSettings = z.object({
  username: z.string().regex(usernameRegex, {
    message: (msg`Username can only contain letters, numbers, underscores and dots.`).id,
  }),
  locale: z.enum(languages),
});

This will effectevely resolve your next problem, because you will not rely on the global i18n instance anymore.

I tried it but I had some problems with the TransNoContext import and then I also had some errors that said "Module 'fs' not found".
I will retry it once the TransNoContext problem is gone, but just to be sure: should I just add the swcPlugin, install @lingui/macro and wrap the error in i18n.t(here) in order to apply the fix that you suggested me?

@mahirocoko
Copy link

@thekip I installed the latest version but TransNoContext seems to not be exported. I checked in the index and it seems like that. Could you check?

@thekip I just cloned your PoC repo (using pnpm) and it gives this error image

I have the same problem.

@thekip
Copy link
Collaborator Author

thekip commented Sep 14, 2023

Hey guys, sorry, there was some mess with these exports. Created a new PR with a fix, hope it would be published soon.

@413n your case is not particular related to this issue, i proposing opening a new discussion with your problems and continue discussion there. Or ask me in discord.

@andrii-bodnar
Copy link
Contributor

Hi guys, the fix is already available in v4.5.0

@andrii-bodnar andrii-bodnar linked a pull request Sep 14, 2023 that will close this issue
7 tasks
@rwu823
Copy link

rwu823 commented Sep 30, 2023

I was able to make it all work together with a lot of dirty words and some webpack magic. The very early PoC is here https://github.com/thekip/nextjs-lingui-rsc-poc

How it works

There few key moments making this work:

  1. I was able to "simulate" context feature in RSC with cache function usage. Using that, we can use <Trans> in any place in RSC tree without prop drilling.
// i18n.ts

import { cache } from 'react';
import type { I18n } from '@lingui/core';

export function setI18n(i18n: I18n) {
  getLinguiCtx().current = i18n;
}

export function getI18n(): I18n | undefined {
  return getLinguiCtx().current;
}

const getLinguiCtx = cache((): {current: I18n | undefined} => {
  return {current: undefined};
})

Then we need to setup Lingui for RSC in page component:

export default async function Home({ params }) {
  const catalog = await loadCatalog(params.locale);

  const i18n = setupI18n({
    locale: params.locale,
    messages: { [params.locale]: catalog },
  });

  setI18n(
    i18n,
  );

And then in any RSC:

const i18n = getI18n()
  1. Withing this being in place, we have to create a separate version of <Trans> which is using getI18n() instead of useLingui(). I did it temporary right in the repo by copy-pasting from lingui source.
  2. Having this is already enough, you can use RSC version in the server components and regular version in client. But that not really great DX, and as @raphaelbadia mentioned we can automatically detect RSC components and swap implementation thanks to webpack magic. This is done by:
    • macro configured to insert import {Trans} from 'virtual-lingui-trans'
    • webpack has a custom resolve plugin which depending on the context will resolve the virtual name to RSC or Regular version.
const TRANS_VIRTUAL_MODULE_NAME = 'virtual-lingui-trans';

class LinguiTransRscResolver {
  apply(resolver) {
    const target = resolver.ensureHook('resolve');
    resolver
      .getHook('resolve')
      .tapAsync('LinguiTransRscResolver', (request, resolveContext, callback) => {

        if (request.request === TRANS_VIRTUAL_MODULE_NAME) {
          const req = {
            ...request,
            // nextjs putting  `issuerLayer: 'rsc' | 'ssr'` into the context of resolver. 
            // We can use it for our purpose:
            request: request.context.issuerLayer === 'rsc'
              // RSC Version without Context (temporary a local copy with amendments)
              ? path.resolve('./src/i18n/Trans.tsx')
              // Regular version
              : '@lingui/react',
          };

          return resolver.doResolve(target, req, null, resolveContext, callback);
        }

        callback();
      });
  }
}

Implementation consideration:

  • Detecting language and routing is up to user implementation. I used segment based i18n by just creating app/[locale] folder. I think it's out of the Lingui scope.
  • We still need to configure separately Lingui for RSC and client components. It seems it's a restriction of RSC design which is not avoidable. So you have to use I18nProvider in every root with client components. (or do everything as RSC)

@thekip How to make the t function work on both client and server? I tried this example does not work.

@Janhouse
Copy link

Janhouse commented Oct 6, 2023

Seems like it mostly works but I noticed that if I use setI18n only in the layout.tsx it sometimes triggers server side error: Error: Lingui for RSC is not initialized. Use setI18n() first in root of your RSC tree. when navigating between pages in Next.js

It doesn't happen on every request though. Probably something to do with that cache?

image

I even tried out using the createServerContext to get this working consistently when setting it only in the layout, but then I found out in the react github issues that it will be deprecated and I should not use it. 🙁

All in all - this feels somewhat hackish and I don't exactly like using it this way. In my opinion react/next.js developers should provide some additional functionality within the framework, that helps with setting and getting selected locales.

Also, would it be possible to somehow use the currently experimental dependency tree crawling?

@thekip
Copy link
Collaborator Author

thekip commented Oct 9, 2023

Seems like it mostly works but I noticed that if I use setI18n only in the layout.tsx it sometimes triggers server side error: Error: Lingui for RSC is not initialized. Use setI18n() first in root of your RSC tree. when navigating between pages in Next.js

May be because of race-condition? As far as i remember, layouts are executed after page. So you need to initialize i18n in page not in a layout.

Also, would it be possible to somehow use the currently experimental dependency tree crawling(https://lingui.dev/guides/message-extraction#dependency-tree-crawling-experimental)?

It should work. With next's app folder and layouts, it even could be more granular than with regular pages

@marcmarcet
Copy link

I've been playing with lingui and the app router for the last couple of days and it seems that if you need to translations in both, pages and layouts, you have to basically instance two i18n contexts. One for the page, and another one for the layout. Otherwise the context might be null sometimes, dependening if you do a full refresh or a client navigation.

This seems to be aligned with the approach suggested here for i18next, were they basically instance a new context on every useTranslation() call (for server components).

This seems very wasteful, but also doesn't look like there is any workaround...

@thekip
Copy link
Collaborator Author

thekip commented Oct 20, 2023

@marcmarcet could you create a PR with improvements you mentioned in this repo?
https://github.com/thekip/nextjs-lingui-rsc-poc

@marcmarcet
Copy link

@thekip not sure if it's going to be helpful, but this is what i´m playing with:

https://github.com/thekip/nextjs-lingui-rsc-poc/pull/2/files

As is it right now, the app crashes when navigating from parent to child page. Uncomenting the await useLinguiSSR(params.locale) on every page ensures there is a context available at all times.

if we could find a way to get the locale in the Trans component, then everything would work out of the box without having to add await useLinguiSSR(params.locale) to each page.

Also notice I added an extra layout in app/[locale], so the LinguiProvider can be shared with all client components, regardless of the page.

@andrii-bodnar andrii-bodnar pinned this issue Nov 29, 2023
@bryanltobing
Copy link

Hi guys. want to ask. Is there a guide on how to set up Lingui App router RSC support from the beginning?

@thekip
Copy link
Collaborator Author

thekip commented Jan 9, 2024

Unfortunately, no, I don't use it in my own project because it's seems almost impossible to migrate a big productiuon application from pages to app router. And i don't have a capacity to investigate and write it up in my free time. But there is a lot of info already in this thread.

@mhfaust
Copy link

mhfaust commented Feb 29, 2024

So changes in macro and swc plugin would be needed as well as new component is introduced in lingui/react

I've started a new app for a major project at work using the app directory and I'm spiking on i18n frameworks. I really like Lingui's API with its extraction tool, but it doesn't work yet for this TransNoContext RSC, which is a borderline show-stopper. Are there plans to do get that fixed?

Also, the t macro doesn't work in RSC's. I can probably live without that. But again, any plans to get that working?

@thekip
Copy link
Collaborator Author

thekip commented Feb 29, 2024

What you mean by saying TransNoContext is not working? Check my PoC above, there are examples of how to use TransNoContext and extrator. btw, t is not going to work, since it's global, but msg works as charm

@vonovak
Copy link
Collaborator

vonovak commented Mar 4, 2024

I might be not be seeing this correctly but... @thekip said

no, I don't use it in my own project... And i don't have a capacity to investigate and write it up in my free time...

guys - if you like lingui but struggle with RSC - @thekip has spent some time with this, and has the necessary context. Why don't you just hire him to solve the problem for you and write up documentation that you can follow?

You'll get what you need, and you'll get it from the person who knows how it should be done.

I just don't understand why people don't see the "pay to get a OSS problem solved" option and rather than taking progress in their hands, they wait and wait for someone to contribute or for some magic to happen (I guess?).


Please: don't take this personally, I really don't mean it badly in any way. It's just that I've seen many times (in other repos too) people being seemingly stuck when the option is right there...

@mhfaust
Copy link

mhfaust commented Mar 5, 2024

Thank you @thekip, after some efforts with the plugin, I did get this to work in server components.

@andrii-bodnar andrii-bodnar mentioned this issue Mar 7, 2024
11 tasks
@omattman
Copy link

@mhfaust can you share how you managed to make it work in server components?

@fromthemills
Copy link

fromthemills commented Mar 29, 2024

I think we don't need to make things more complicated than they really are. In my opinion we can solve most server component issues/use-cases by just adding "use client" to the Trans component. Is there a reason why we are not doing that?

When I use Lingui with the app dir everything works great except that I need to add "use client" to all server component that use the Trans component to avoid the error below

Error: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it.

Because not all libraries have this new directive defined, the React team recommends that in these cases you just create your own version for the component and add "use client" at the top. For the lingui Trans component this would look like:

// trans.js
'use client'

import React from 'react'
import { Trans as LinguiTrans, TransProps } from '@lingui/react'

export function Trans(props: TransProps) {
    return <LinguiTrans {...props} />
}

Unfortunately this breaks extraction. So this is not possible. 😢

I don't really see what the problem is. If you are worried about server side rendering, remember client side component also render on the server. They just use the initial state or context they are provided. So for Lingui we can just provide a default i18n instance with the correct catalog for the selected language to the Provider and everything works like a charm. The Lingui components will be a part of the client side bundle but this should not matter to much.

You can also only pass the correct catalog from you server component so you don't ship 20 catalogs with your client bundle. An example like below works great for me.

import { I18nProvider } from '@/components/providers/i18nProvider'
import { loadMessages } from '@/utils/locales'
import './globals.css'

type RootLayoutProps = {
    params: { locale: string }
    children: React.ReactNode
}

export async function generateStaticParams() {
    return [{ locale: 'en' }, { locale: 'nl-be' }]
}

export default async function RootLayout({ params, children }: RootLayoutProps) {
    const { locale } = params
    let messages = loadMessages(locale)
    return (
        <html lang={locale}>
            <body>
                <I18nProvider locale={locale} messages={messages}>
                    {children}
                </I18nProvider>
            </body>
        </html>
    )
}
// I18nProvider.ts
'use client'

import { setupI18n } from '@lingui/core'
import { I18nProvider as LinguiProvider } from '@lingui/react'

type Props = {
    locale: string
    messages?: any
    children: React.ReactNode
}

export function I18nProvider({ locale, messages, ...props }: Props) {
    return (
        <LinguiProvider
            i18n={setupI18n({
                locale: locale,
                messages: { [locale]: messages },
            })}
            {...props}
        />
    )
}

You can't switch languages client side with the approach above. In most cases for Nextjs this would be a redirect anyway so this does not matter to much in my opinion.

Of course having a fully uniform way of using Trans for switching between client and server component dynamically based on the render context like the POC @thekip made is nice. Its just that "use client" seems like an easy win which gets us 90% of the way.

@mhfaust
Copy link

mhfaust commented Mar 29, 2024

@fromthemills the problem is forcing a server component to become a client component might drastically bloat the js that comes with that page. Many may want their sites to be primarily SSG, and don't want a huge ball of JS stuffed in the page, for various reasons.

@fromthemills
Copy link

@mhfaust I understand, but this is also part of the point I am making. If you just add "use client" at the Trans component level, it is only the Trans component that will be part of the js bundle. You wont need to force your entire server components based page to become a client component and be part of the js bundle.

If you are looking for a "pure" server components based page. No Javascript at all to the client, I follow you. We need a new strategy for that. But in most cases your shipping some js anyway so the few extra Lingui components won't make the difference. Especially if you have some translations in your client components anyway.

I am not saying it is not worth pursuing a more uniform way for the future. Just thinking that "use client" in the Trans component is a good first step. Was just wondering why this was not added jet? It already uses a useContext and is thus presumed a client component so adding this won't break anything as far as I can see.

@mhfaust
Copy link

mhfaust commented Mar 29, 2024

@fromthemills, well, good point. it's a 1-line change, so I just put in a PR for it: #1899.
If the owners have a reason not to they can reject it and maybe we'll get an explanation.

...ok I owe it to them to follow the Code of Conduct and do the appropriate testing, etc. I'm doing that so I've marked it draft.

@thekip
Copy link
Collaborator Author

thekip commented Apr 2, 2024

For the lingui Trans component this would look like:
Unfortunately this breaks extraction. So this is not possible. 😢

You can do this with ease if you use macro. You can create your own runtime Trans component, and set runtimeConfigModule to your custom Trans with 'use client'.

AFAIK, changing Trans to use client is not enough, since you also need to mark an I18nProvider as client side as well, And since this component is usually very close to the root of React tree this will convert all children components to the client side defeating the purpose of the RSC.

@fromthemills
Copy link

fromthemills commented Apr 2, 2024

@thekip Thank you for the runtimeConfigModule suggestion.

Your assessment that marking I18nProvider as "use client" will make the entire server component tree client side is not correct though. It is perfectly possible to wrap server components in client only providers you just need to make sure you pass children. You can read more about it here or here. If you think about it, this makes sense because else we would never be able to wrap something in a provider, as a provider uses a context and is thus presumed client. Below is a good visualisation I think.
Screenshot 2024-04-02 at 13 48 01
That is also what I meant by "I think we don't need to make things more complicated than they really are." Lingui works perfectly fine with server components. The messages and I18nProvider and Trans will be part of js bundle but this is fine for now. We just need to make sure we set the initial context to the correct language so that server and client render will match.

@andrii-bodnar
Copy link
Contributor

Just released a new version that adds the use client directive to the React bundle - 4.8.0

@AndreeaCsecs
Copy link

Hi @thekip,

Thank you for providing a working PoC of Lingui with React Server Components in NextJS and for the detailed instructions on setting it up. However, I'm experiencing an issue with the t`` macro that I can't seem to resolve.

In my setup, the text inside <Trans> tags gets translated correctly, but the text inside t`` does not. Here's what I've done so far:

Don't use t macro with global i18n, use t(i18n)`Hello!` or i18n._(msg`Hello`) you can get i18n instance from

const i18n = getI18n()
  1. I followed your advice and used t(i18n) and i18n._(msg) with getI18n() to get the i18n instance.
  2. I cloned your example app from nextjs-lingui-rsc-poc and tested it, but I encountered the same issue. Neither t(i18n) nor i18n._(msg) worked in the client component.
  3. I implemented the files and provider exactly as they are in the PoC without making any changes.

Here is a snippet of my code:

"use client";
import React, { useRef, useState } from "react";
import { t } from "@lingui/macro";
import { getI18n } from "@/i18n/i18n";

export function LoginForm() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [errorMessage, setErrorMessage] = useState("");
  const [show2FA, setShow2FA] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  
  const i18n = getI18n();

  return (
    <form>
      <Input
        placeholder={t`Benutzername`}
        autoComplete="username"
        onChange={(e) => setUsername(e.target.value)}
      />
      <Input
        placeholder={t`Passwort`}
        type="password"
        autoComplete="current-password"
        onChange={(e) => setPassword(e.target.value)}
      />
      <Button type="submit">
        {i18n ? t(i18n)`Anmelden` : "Anmelden"}
        {/* This works correctly */}
        <Trans>Anmelden</Trans>
      </Button>
    </form>
  );
}

When I run this code, I get the following error in the VS Code terminal:

⨯ Error: Not implemented.
    at getI18n (./src/i18n/i18n.ts:14:12)
    at LoginForm (login.form.tsx:85:71)
digest: "4187124702"
GET /en 500 in 58ms

It appears there's an issue with getI18n() not being implemented correctly, which might be causing the translation to fail.

Here is my project folder structure for reference:

image

Could you provide further guidance on how to properly configure the provider or any other setup that might be missing to get t`` working both on client and server sides? Do I need to create a different provider specifically for managing t`` in client-side components, or is there a way to configure it to work correctly in both environments?

Thanks in advance for your help!

@thekip
Copy link
Collaborator Author

thekip commented May 30, 2024

getI18n(); is for RSC, it uses React.cache() which is not implemented for client components (by React team)

For client components (use client), you have to use:

function MyComponent() {
 const {_} = useLingui();

 const message = _(msg`Hello!`)
}

As it's written in the tutorial.

You also can check attached to this issue Pull Requests, there is an ongoing work for new documentation for RSC, you can read it from PR while it's not ready.

@imWildCat
Copy link

imWildCat commented Jun 4, 2024

I'm so shocked how terrible the dev experience in this next.js community.
I have many years of React experience. But it already took me 10+ hours and I still cannot enable Lingui for next.js 14 with App Router, in 2024.

I'm removing next.js for every project of my team. I feel sorry to say this but I can really get things done 10x faster with any other solutions like Rails/Nuxt/pure React or raw HTML.

@imWildCat
Copy link

@thekip How to make the t function work on both client and server? I tried this example does not work.

Two ways:

  1. Implement another t helper for client only when window !== undefined
  2. pass t from server to client components

both sounds silly but this is the reality of next.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet