lingui / js-lingui

🌍 📖 A readable, automated, and optimized (3 kb) internationalization for JavaScript
https://lingui.dev
MIT License
4.64k stars 382 forks source link

<Trans> breaks React JSX semantics with regards to booleans #1904

Closed gaearon closed 7 months ago

gaearon commented 7 months ago

Describe the bug

From:

https://lingui.dev/tutorials/react#jsx-to-messageformat-transformations

Obviously, you can also shoot yourself in the foot. Some expressions are valid and won't throw any error, yet it doesn't make any sense to write:

// Oh, seriously?
<Trans>{isOpen && <Modal />}</Trans>

If in doubt, imagine how the final message should look like.

Setting aside the unnecessarily mocking tone of the docs, this behavior seems quite problematic because it changes the React JSX semantics. Booleans are supposed to be ignored in the output, but Lingui just turns them to strings.

Even if you "know" that's what it does, it's still super easy to miss <Trans> outside of some JSX, and to change that JSX according to React rules. It's then a significant pitfall that it doesn't work. Especially considering Lingui reconstructs the entire tree below so it breaks not just one level deep, but all the way down. This is highly non-obvious.

To Reproduce Steps to reproduce the behavior, possibly with minimal code sample, e.g:

import { Trans } from "@lingui/react"

export default function App() {
   return (
      <Text>
        <Trans>
          foo
          <span>
            {false && 'lol'}
            {true && 'wut'}
          </span>
          bar
        </Trans>
      </Text>
   )
}

Expected behavior

<Text>foowutbar</Text>

Actual behavior

<Text>foofalsewutbar</Text>
timofei-iatsenko commented 7 months ago

Thanks for report. I Actually have no idea how proper React code after transform should looks like. Currently your example would be transformed into:

<Text>
  <_Trans
    id={"ULdFKt"}
    message={"foo<0>{0}{1}</0>bar"}
    values={{
      0: false && "lol",
      1: true && "wut",
    }}
    components={{
      0: <span />,
    }}
  />
</Text>;

And these differences in semantics came from

values={{
      0: false && "lol",
      1: true && "wut",
    }}

Which are simply interpolated into the string like so:

`${false && "lol"}` // -≥ will print false
timofei-iatsenko commented 7 months ago

I probably found a solution:

If every value would be wrapped into JSX Fragment, than lingui/react's formatElements will treat them as JSX components and inline as children, thus JSX semantics would be preserved

values={{
-      0: false && "lol",
+      0: <>{false && "lol"}</>,
    }}
gaearon commented 7 months ago

I think maybe it would be enough to do this for values that aren’t strings and numbers?