-
Notifications
You must be signed in to change notification settings - Fork 383
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
Comments
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 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 :
Then in a page, I've logged the i18n object and my messages were stored in i18n.messages. So I just wrote in a RSC page :
It seems to work ! 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? |
Extractor will not extract messages from your custom Trans components. But approach in the right direction. I wrote it as
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 |
I don't know much about babel / swc, but couldn't these plugins read the file, see if it contains the string Would be better for the user to write Also, your RSC example would be great but are you sure it's necessary to do |
Typescript users would not be happy. Explicit is always better than implicit.
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 |
I've dug through @amannn's next-intl package and I found their approach interesting, here's how I (think ?) it works:
react provides this capability with the 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>
}
|
I dug into the |
@raphaelbadia Your summary from above looks correct to me! I found that 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! 🙌 |
Thanks for investigation.
We still need to create an i18n instance per request with specific locale. I don't see how this 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'));
}) |
I believe React.cache is a cache per request. |
It is ! |
What's the progress? |
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:
// 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()
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:
|
Hi @thekip, I love your solution but I am encountering a small problem that maybe you could help me with. ProblemI'm using the Provider as you have in your repo where you pass 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 My current solutionWhat I was trying was reusing the
// ... 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:
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)? |
Hi all, since lingui@4.4.1 there is no need to copy |
@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 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. |
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". |
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. |
Hi guys, the fix is already available in v4.5.0 |
@thekip How to make the |
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... |
@marcmarcet could you create a PR with improvements you mentioned in this repo? |
@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 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 Also notice I added an extra layout in app/[locale], so the LinguiProvider can be shared with all client components, regardless of the page. |
Hi guys. want to ask. Is there a guide on how to set up Lingui App router RSC support from the beginning? |
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. |
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 |
What you mean by saying TransNoContext is not working? Check my PoC above, there are examples of how to use TransNoContext and extrator. btw, |
I might be not be seeing this correctly but... @thekip said
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... |
Thank you @thekip, after some efforts with the plugin, I did get this to work in server components. |
@mhfaust can you share how you managed to make it work in server components? |
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 When I use Lingui with the app dir everything works great except that I need to add
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 // 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 |
@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. |
@mhfaust I understand, but this is also part of the point I am making. If you just add 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 |
@fromthemills, well, good point. it's a 1-line change, so I just put in a PR for it: #1899. ...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. |
You can do this with ease if you use macro. You can create your own runtime AFAIK, changing |
@thekip Thank you for the Your assessment that marking |
Just released a new version that adds the |
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 In my setup, the text inside
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:
It appears there's an issue with Here is my project folder structure for reference: Could you provide further guidance on how to properly configure the provider or any other setup that might be missing to get Thanks in advance for your help! |
For client components ( 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. |
I'm so shocked how terrible the dev experience in this next.js community. 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. |
Two ways:
both sounds silly but this is the reality of next.js |
Actually i remember that import {useLingui} from '@lingui/react';
import {msg, t} from '@lingui/macro';
function MyComponent() {
const {_, i18n} = useLingui();
_(msg`Hello!`)
// or
t(i18n)`Hello`
} |
Thank you!!! I found a complete example here:
🫡 |
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:
<Trans>
directly in RSC.For the point number one there are few solutions:
t
ori18n._
directly in server components. While that would work, it is hard to translate messages which may have other JSX elements inside.<TransWithoutContext>
component (and macro as well), which will not use context and context would be passed as props directly to component.Checklist:
The text was updated successfully, but these errors were encountered: