Skip to content

More robust icu-specific Trans babel macro #1869

@cellog

Description

@cellog

🚀 Feature Proposal

A new Trans component that does not rely upon React.children under the hood will allow for more future-compatible work. The current component doesn't actually work with deeply nested trees like this:

    return (
      <div>
        <Trans i18nKey="complex-trans">
          <strong>exciting!</strong>
          {plural`${count},
          =0 { hi there ${(<strong>friend</strong>)} }
          other { woweee even supports nested ${number`${numbers}`} } `}{" "}
          and
          {select`${selectInput},
           thing { another nested ${(
             <Box style={{ color: "red" }}>
               with regular text and a date: <code>{date`${now}`}</code>
             </Box>
           )} other {and the fallback <ul><li>one</li><li>two</li></ul>}} `}
        </Trans>
      </div>
    );

This renders as:

Image

I wrote a proof-of-concept new Trans component, and the same JSX renders correctly as:

Image

The new component is slightly less ergonomic to use. The existing macro compiles to this API:

<Trans
  i18nKey="complex-trans"
defaults="<0>exciting!</0>{count, plural, =0 { hi there <1>friend</1> } other { woweee even supports nested {numbers, number} } } and{selectInput, select, thing { another nested <2>with regular text and a date: <0>{now, date}</0></2> } other { fallback }}"
components={[<strong>exciting!</strong>, <strong>friend</strong>, <Box style={{
  color: \\"red\\"
}}>
       with regular text and a date: <code>{date`${now}`}</code>
     </Component>]} values={{
  count,
  numbers,
  selectInput,
  now
}} />

Note that it doesn't correctly discover the internal <code>.

The new API looks like this (I used InternalTrans as the component name, but we can bikeshed that, perhaps ICUTrans or something like that):

<InternalTrans
  i18nKey="key"
  defaultTranslation="<0>exciting!</0>{count, plural, =0 { hi there <1>friend</1> } other { woweee even supports nested {numbers, number} } } and{selectInput, select, thing { another nested <2>with regular text and a date: <0>{now, date}</0></2> } other { fallback }}"
content={[
  {
    type: "strong"
  }, {
    type: "strong"
  }, {
    type: Component,
    props: {
      style: {
        color: "red"
      },
      children: [{
        type: "pre"
      }]
    }
  }, {
    type: "ul",
    props: {
      children: [
        { type: "li" },
        { type: "li" },
      ],
    },
  },
]}
values={{
  count,
  numbers,
  selectInput,
  now
}} />

This much more closely resembles the values passed to React.createElement, and in fact that's how it works under the hood.

The new API does the following:

  • get t from useTranslation, and take the values and translation key, and translate the function (passing in defaultTranslation as a fallback if the translation does not exist in the specified locale)
  • parse the generated translation for the <0> tags, and map them to the contents, recursively setting up the tree of content to render
  • traverse that tree recursively to get the actual calls to createElement
  • return that

In my testing, not only is this cleaner, but it actually works with react-compiler as well. It has the added benefit that all of the actual logic can be in external pure functions, so Jest can be used to test the range of possible values very efficiently.

Note that this component can also be directly rendered in a human-friendly fashion:

    return (
      <div>
        <InternalTrans
          i18nKey="complex-trans"
          defaultTranslation="<0>exciting!</0>{count, plural, =0 { hi there <1>friend</1> } other { woweee even supports nested {numbers, number} } } and {selectInput, select, thing { another nested thing <2>with regular text and a date: <0>{now, date}</0></2> } other {and the fallback <3><0>one</0><1>two</1></3>}}"
          content={[
            { type: "strong" }, // <0>exciting!</0>
            { type: "strong" }, // <1>friend</1> (inside plural)
            {
              type: Box,
              props: {
                style: { color: "red" },
                children: [
                  { type: "code" }, // <0> inside Box - date display
                ],
              },
            }, // <2> (inside select)
            {
              type: "ul",
              props: {
                children: [
                  { type: "li" }, // <0> inside ul
                  { type: "li" }, // <1> inside ul
                ],
              },
            }, // <3> (inside select fallback)
          ]}
          values={{ count, numbers, selectInput, now }}
        />
      </div>

rendered output is the same as the screenshot above.

Motivation

The 1.0 release of react-compiler just happened last week. At my company, we use the Trans component from react-i18next/icu-macro. When building with react-compiler, it caused some components to simply disappear from the component tree.

I have already built a working version of this for internal use at my company, and so there is no urgency, but would like to contribute back for the larger community.

Example

Examples are inline above. The API for import would be identical. Consumers of icu.macro would get the API above for direct calls to Trans.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions