amannn / next-intl

🌐 Internationalization (i18n) for Next.js
https://next-intl-docs.vercel.app
MIT License
2.31k stars 210 forks source link

Fully serializable config #611

Open amannn opened 10 months ago

amannn commented 10 months ago

Is your feature request related to a problem? Please describe.

These three config options accept functions and can therefore not be serialized currently:

  1. defaultTranslationValues
  2. onError
  3. getMessageFallback

Due to this, we can't automatically pass them from an RSC render to the client side.

It might be worth investigating serializable alternatives, to avoid this.

Describe the solution you'd like

defaultTranslationValues

Maybe a better option could be to deprecate the option altogether and ask users to share values e.g. via a component:

function Markup() {
  const t = useTranslations('Markup');
  return <RichText>{components => t('markup', components)}</RichText>
}

function RichText({children}) {
  return children({
    b: (chunks: ReactNode) => <b>{chunks}</b>
  });
}

… or imports:

// defaultRichTextComponents.tsx

export default {
  b: (chunks: ReactNode) => <b>{chunks}</b>
}

// Markup.tsx

import defaultRichTextComponents from './defaultRichTextComponents';

function Markup() {
  const t = useTranslations('Markup');
  return t.rich('markup', defaultRichTextComponents);
}

As a nice side effect, this would allow us to statically type the arguments of a given ICU string (see https://github.com/amannn/next-intl/issues/410), since there are no potential global values floating around.

Another benefit could be to remove ambiguity between values that should be used for t.rich and t.markup (see https://github.com/amannn/next-intl/issues/1181).

onError

See https://github.com/amannn/next-intl/issues/1285

getMessageFallback

See https://github.com/amannn/next-intl/issues/1285

Alternatives:

Describe alternatives you've considered

NextIntlClientProvider can be added in a client-side module graph to configure the non-serializable config options, see the docs.

ovflowd commented 10 months ago

Note this doesn't handle the case where I want to pass the messages as an object from another component.

I.e.

const MyComponent = ({ translationValues }) => t.rich('translationKey', translatioNValues);

So IMHO the whole RichTranslations and MarkupTranslations should support non-functions.

amannn commented 10 months ago

You mean when translationValues contains functions, right? Can you share more about the practical use case?

ovflowd commented 10 months ago

You mean when translationValues contains functions, right? Can you share more about the practical use case?

Wdym by that? I should be able to pass translation values as props between any component if I wish. Shouldnt be blocked by an implementation detail of making them a function.

In other words, what if I have dynamic values and want to map them, I'd be unable due to next-intl requiring markup or rich text to be wrapped on functions...

amannn commented 10 months ago

I should be able to pass translation values as props between any component if I wish.

React defines the rules for which props are serializable.

next-intl provides an API that allows for rich text formatting where any custom component can be used:

t.rich('hint', {
  help: (chunks) => <Link href="/help">{chunks}</Link>
})

Theoretically, React elements themselves are serializable, so from an API perspective the value of help here is serializable:

t.rich('hint', {
  help: <Link href="/help" />
})

However, if the element that is passed to help comes from a Server Component and t.rich executes in a Client Component, then only the markup is transferred—not the component. This is quite the point of the RSC model. Therefore, practically this API doesn't work.


Long story short, I can't think of an API that has the same flexibility as the current t.rich that would accept a fully serializable config. Staying within either a server or client module graph for rich text elements solves the problem from a different angle. Due to the way the RSC model works, you can import rich text elements even from both from Server and Client Components.

My comment on top of this thread describes examples for sharing common rich text elements. I think it's helpful to discuss this based on concrete examples to be able to find good solutions.

In case you can think of a better pattern, let me know!

ovflowd commented 10 months ago

I think sharing this with you is self-explanatory.

Pretty much there's a translation key that we want to have a specific part of it to be surrounded with a "span" element (https://github.com/nodejs/nodejs.org/blob/main/i18n/locales/en.json#L84), that's it.

This was possible before, because react-intl allowed me to replace a variable, such as {something} (https://github.com/nodejs/nodejs.org/blob/c72dc046dffda7d8b056623602c80bac3dd6a8f4/i18n/locales/en.json#L19) with something else (https://github.com/nodejs/nodejs.org/blob/c72dc046dffda7d8b056623602c80bac3dd6a8f4/layouts/DocsLayout.tsx#L21)

I understand that this is unrelated to next-intl, and react-intl would face the same issues once I've used server components; Hence me indulging if it makes sense to support something else.

In the end, I just want to be able to share translation values from Component A to Component B. Right not it works because both components are client components, but I feel that maybe next-intl should support a way to transfer rich-text content independent if the component is RSC or CSC... 🤔

Not a big deal, but I feel that it'd be nice if it was supported. Either by something like:

const translationContext: RichTranslationValues = {
 graySpan: '<span className="small color-lightgray">{0}</span>'
}

Encapsulating the HTML code within a string and then eval'ing it. And the translation key be like:

"apiLts": "{fullLtsNodeVersion} API <graySpan>something</graySpan>",

I know this is just a poor example, and definitely a stretch.

ovflowd commented 10 months ago

Also sorry if I'm just crying wold here, @amannn, please feel more than free to just disregard my request!

amannn commented 10 months ago

This was possible before, because react-intl allowed me to replace a variable, such as {something} (https://github.com/nodejs/nodejs.org/blob/c72dc046dffda7d8b056623602c80bac3dd6a8f4/i18n/locales/en.json#L19) with something else (https://github.com/nodejs/nodejs.org/blob/c72dc046dffda7d8b056623602c80bac3dd6a8f4/layouts/DocsLayout.tsx#L21)

That's an interesting one, because you didn't need chunks previously (since "LTS" was hardcoded):

spanLts: <span className="small color-lightgray">LTS</span>,

next-intl could most likely support passing a React Element without chunks for rich text, by directly assigning the element to a value (without a function). In fact, I think it already does, we'd only have to update the types for such a call.

There's currently an expandable section in the rich text docs about self-closing tags. This could be updated accordingly as part of this change.

In regard to rich text content with chunks: I don't see eval'ing as a good direction here, to be honest. The provided elements can not just be HTML, but any React component.

To come back to your example: I think there could also be a question here about which parts of the component should be Server or Client Components. Briefly looking into the code, I'm wondering if the rendering of the sidebar could be achieved in RSC and only individual items that need to be highlighted if they're active could be Client Components. The next-intl App Router example does just that for the navigation. If you're staying within RSC for the rendering of the items, then this problem also disappears.

Hope this helps!

ovflowd commented 10 months ago

'm wondering if the rendering of the sidebar could be achieved in RSC and only individual items that need to be highlighted if they're active could be Client Components.

Hm, true, I think I can make it RSC compatible.

In regard to rich text content with chunks: I don't see eval'ing as a good direction here, to be honest. The provided elements can not just be HTML, but any React component.

Right, I'm also not a fan of this. But I believe at least for the case you mentioned above about without chunks, worth documenting it and updating the types. Can be useful for some folks :)

amannn commented 10 months ago

Right, I'm also not a fan of this. But I believe at least for the case you mentioned above about without chunks, worth documenting it and updating the types. Can be useful for some folks :)

Yep, I agree!

ovflowd commented 10 months ago

Aaand, found a way to make it RSC by removing the only Hook that was "client-dependent"

amannn commented 10 months ago

Awesome! 👏

amannn commented 2 weeks ago

I've added https://github.com/amannn/next-intl/issues/1285 to investigate a better way to achieve global error handling.