lingui / js-lingui

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

Why React components instead of key or functions #15

Closed tricoder42 closed 5 years ago

tricoder42 commented 7 years ago

Just to make it clear, there're two i18n APIs:

Components vs. functions

React is all about rendering data. When data changes, React renders updates. Given example below:

<span>{i18n.t`Hello World`}</span>
<Trans>Hello World</Trans>

when language or message catalog changes, the span (or probably parent component) is responsible for update, while Trans take care of it itself. When parent component is optimized using shouldComponentUpdate, the Trans component will update the translated message even when parent skips update.

Reason 1: Using Trans component shifts responsibility from parent to Trans component itself. As a developer, you don't have to worry about skipped updates.

Second reason are inline components. Using Trans component you can go wild a do things like this:

<Trans>
   Any component is <strong className="green">valid</strong> here, even <Link>custom</Link> ones.
</Trans>

Example above will result in single entry in your message catalog: Any component is <0>valid</0> here, even <1>custom</1> ones.

This has several advantages:

  1. Translator doesn't care about html tags
  2. Props of inline components doesn't affect message
  3. You get only one message. This is obvious, but other libs (i18next, formatjs) translate content of inline components separately which is unfortunate for translators.

Reason 2: First-class support for inline components. As a developer, you can use JSX as you're used to. As a translator, you see the full message.

In future I'm planning further optimization, when message is rendered to React components tree and when props changes (like values, params) the message isn't parsed from scratch, but React handle update itself. Right now, message itself is cached, but inline components are rendered on each update.

Key vs. source language

// key
i18n.t`component.title`

// message in source language
i18n.t`Title`

Both approaches have pros & cons. I never settled to either solution. I'm trying to support both approaches:

// Key
<Trans id=`msg.hello`>Hello World</Trans>
// becomes {'msg.hello': 'Hello World'} for English

// Message
<Trans>Hello World</Trans>
// becomes {'Hello World': 'Hello World'} for English

I'm using second approach in examples because it's easier to start with.

davidfurlong commented 7 years ago

@tricoder42 I felt a bit bad hijacking the thread too, but I didn't know where to take the conversation. This is good.

Reason 1: Using Trans component shifts responsibility from parent to Trans component itself. As a developer, you don't have to worry about skipped updates.

Well you shouldn't be skipping changes to the HOC passed translation props anyways, so this shouldn't pose an issue.

Second reason are inline components. Using Trans component you can go wild a do things like this:

You could theoretically return a html styled string from the function served by the translation (and then parse it) (instead of just strings, have something like:

{t["numberOfUsers"](users.length)}
...
{
en: {
numberOfUsers: (n) => `<b>${n}</b> people`
},
de: {
numberOfUsers: (n) => `personen: <b>${n}</b>`
}
}

). This would have the advantage of moving flexibility into the individual translations, and minimizing API surface area.

davidfurlong commented 7 years ago

You get only one message. This is obvious, but other libs (i18next, formatjs) translate content of inline components separately which is unfortunate for translators.

What do you mean by this?

davidfurlong commented 7 years ago

Any component is <0>valid here, even <1>custom ones.

But the translators have no idea what the tags are?

davidfurlong commented 7 years ago

In future I'm planning further optimization, when message is rendered to React components tree and when props changes (like values, params) the message isn't parsed from scratch, but React handle update itself. Right now, message itself is cached, but inline components are rendered on each update.

could be solved by a memoized function

davidfurlong commented 7 years ago

/ Message <Trans>Hello World</Trans> // becomes {'Hello World': 'Hello World'} for English

I like the way the contents automatically become the key

davidfurlong commented 7 years ago

The method of using <Trans></Trans> everywhere encourages having big translation blocks. I'm not sure whether its best principles to keep them small or not, but I'd imagine so

tricoder42 commented 7 years ago

Well you shouldn't be skipping changes to the HOC passed translation props anyways, so this shouldn't pose an issue.

The component skipping update might be high above the HOC. It might not even be aware of some i18n going on down in tree. This is common bug in formatjs and I guess other libs too: You switch language, but not all translations are updated. So they recommend to reload the page, to avoid any problems, but it's unnecessary.

You could theoretically return a html styled string from the function served by the translation (and then parse it)

I'm working on sth similar. lingui-cli will have a compile method, which takes ICU message format and returns a function. In the app, you only pass a formatting function with all data in clojure, so you don't use messageformat parser in browser (slow, heavy). Then you can load compiled catalogs, save a few kb and gain perf.

You get only one message. This is obvious, but other libs (i18next, formatjs) translate content of inline components separately which is unfortunate for translators.

What do you mean by this?

This is how does in react-intl (taken from example in docs: https://github.com/yahoo/react-intl/wiki/Components#string-formatting-components):

<FormattedMessage
    id='app.greeting'
    description='Greeting to welcome the user to the app'
    defaultMessage='Hello, {name}!'
    values={{
        name: <b>Eric</b>
    }}
/>

They replace all inline components with variables and pass React component as a variable. So, either the Eric isn't translated (not problem in this usecase), or you can pass another FormattedMessage, but then you'll get two messages, instead of one:

<FormattedMessage
    id='app.greeting'
    defaultMessage='Read {more}'
    values={{
        more: <a href="/more"><FormattedMessage defaultMessage="more" /></a>
    }}
/>

Any component is <0>valid here, even <1>custom ones.

But the translators have no idea what the tags are?

That's my concert too! I was thinking about <0:strong> or <1:a>, so the translator knows a bit what tag is there, but props are still kept away. Everything after : is just a comment, the important is index of component. What do you think?

The method of using

everywhere encourages having big translation blocks. I'm not sure whether its best principles to keep them small or not, but I'd imagine so

This depends. I'm not a translator, but I think the smallest unit should be a paragraph. That's what holds the context. Some SaaS use sentence as the smallest unit. I'm not against it, but I guess that if you translate a paragraph, you only rarely do 1:1, sentence by sentence translation. In UI translatios it usually doesn't matter.

Definitely I don't what to translate Read <a>more</a> as two separate words :) Never break a sentence into words.

davidfurlong commented 7 years ago

The component skipping update might be high above the HOC. It might not even be aware of some i18n going on down in tree. This is common bug in formatjs and I guess other libs too: You switch language, but not all translations are updated. So they recommend to reload the page, to avoid any problems, but it's unnecessary.

I suppose this could be the case, but it seems you have written bad shouldComponentUpdate methods if you are stopping rerender propagation for the entire subtree when the change in props does need to propagate.

They replace all inline components with variables and pass React component as a variable. So, either the Eric isn't translated (not problem in this usecase), or you can pass another FormattedMessage, but then you'll get two messages, instead of one:

Ah I see. Yes this is an eye sore and I don't want this kind of crap in my codebase.

That's my concert too! I was thinking about <0:strong> or <1:a>, so the translator knows a bit what tag is there, but props are still kept away. Everything after : is just a comment, the important is index of component. What do you think?

Well thats committing each translation to using the same HTML elements. It also seems like its adding ugly complexity. Occasionally you do want to "yield" and nest another component or translation within another - think popover on hover on a word in a paragraph - but that word could be located anywhere - this is why intl libraries are so damn complex.

At this point I may just bite the bullet and use a complex intl library

Also wouldn't the below code from your readme generate a new translation key for each value of name passed to it? - or are you doing this before runtime and stringifying the Trans contents?

// Variables
        <Trans>Hello, my name is {name}</Trans>
davidfurlong commented 7 years ago

I threw together a little proof of concept

https://github.com/DeedMob/react-local-translations

Will add dynamic language asap

tricoder42 commented 7 years ago

I suppose this could be the case, but it seems you have written bad shouldComponentUpdate methods if you are stopping rerender propagation for the entire subtree when the change in props does need to propagate.

The problem is that language/messages are passed in context and only the last component receives it from HOC as props. Updates in context needs to be handled carefuly, as described here https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076#.mohyrubc7

Well thats committing each translation to using the same HTML elements. It also seems like its adding ugly complexity. Occasionally you do want to "yield" and nest another component or translation within another - think popover on hover on a word in a paragraph - but that word could be located anywhere - this is why intl libraries are so damn complex.

Yes, sometimes it makes sense and it's possible:

<Trans>
   Click <Popover
       header={i18n.t`Payment options`}
       body={
           <Trans>Please select payment options</Trans>
       }>here</Popover> to set payment options.
</Trans>

This will result in three messages:

  1. "Click <0>here to set payment options."
  2. "Payment options"
  3. "Please select payment options"

Each of them make sense on it's own. You can decouple messages from UI.

However, I never thought about a use case, where you use different components in different languages. Something like <0>Hello</0> in English, but <1>Hola</1> in Spanish, where 0 and 1 are different components. Technically it's possible with current solution, it's just not implemented yet.

Also wouldn't the below code from your readme generate a new translation key for each value of name passed to it? - or are you doing this before runtime and stringifying the Trans contents?

No, just one. You need to use babel-plugin-lingui-transform-react or babel-plugin-lingui-transform-js to make it happen. However, lingui-i18n is based on template strings which are ES6 feature and lingui-react is based on JSX, so you need babel anyway (in most cases).

davidfurlong commented 7 years ago

The problem is that language/messages are passed in context and only the last component receives it from HOC as props. Updates in context needs to be handled carefuly, as described here https://medium.com/@mweststrate/how-to-safely-use-react-context-b7e343eff076#.mohyrubc7

You're totally right. Thanks for telling me this, I haven't used React's contexts.

Yeah everyone will have babel anyways.

I have updated my mini localized components library to reflect this change and I'm quite happy with it. Demo @ https://deedmob.github.io/react-local-translations/example/index.html Of course I'm not sure how well it will perform, but it suits my needs quite well

tricoder42 commented 7 years ago

@davidfurlong That's a good stuff! I'm a bit busy this week with other projects, but I'm going to push custom formats this weekend. I'll take a look at local catalogs after that.

ivan7237d commented 7 years ago

Reg. user-provided versus autogenerated IDs: I'm wondering if by adding support for "description" prop you would end up with the best of both worlds. If you autogenerate IDs as a hash of description + default message, you will be able to avoid clashes between short default messages that mean different things in different contexts.

Also I wonder if it would make sense to create an "Argument" HOC that will have also have a description and which could be used to wrap child components - it would pass on its child but will add the description that will help translators understand what the child component is.

ivan7237d commented 7 years ago

Actually what would be even more fun is using the jsdoc comments as descriptions for messages and message arguments.

tricoder42 commented 7 years ago

Hello Ivan 👋

Actually what would be even more fun is using the jsdoc comments as descriptions for messages and message arguments.

Good point, I'm planning to add comments for translators, something like this:

const Label = () =>
  // i18n: Heading at registration page
  <h1><Trans>Registration</Trans></h1>

which would be extracted as:

{
  "Registration": {
    "description": "Heading at registration page",
    "translation": ""
  }
}

It could be probably possible to add description prop directly to <Trans> component, but it JS it would become more verbose: i18n.t('Registration', { description: "Heading at registration page" }). I'm open to any ideas and PRs here!

Also I wonder if it would make sense to create an "Argument" HOC that will have also have a description and which could be used to wrap child components - it would pass on its child but will add the description that will help translators understand what the child component is.

Could you please provide a short example of this?

Thank you!

ivan7237d commented 7 years ago

Hey Tomáš, yes, the comments approach sounds good, better than a prop. And as for arguments, if you have

<Trans>Abc<MyComponent /></Trans>

then as far as I understand, currently the translation file will just have a number for <MyComponent />, and ideally you'd want to let the translators know what <MyComponent /> is, so I was thinking of ways to achieve that. Here too, a comment would be nicer than a wrapper component with a prop, but JSX doesn't support comments, so I can't think of some good approach off the top of my head...

tricoder42 commented 7 years ago

I see. Maybe we could use description for it:

// i18n: <Link> - clickable element
<Trans>Link to <Link>documentation</Link></Trans>

and extracted description might be:

{
  "Link to <0>documentation</0>": {
    "description": "<0> - clickable element",
    "translation": ""
  }
}

What do you think?

ivan7237d commented 7 years ago

Hmm... I wonder if this is general enough. For example there could be multiple child components with the same name but different props or different children. Maybe this would work, looks a bit ugly but it's the usual way to add comments in JSX:

<Trans>Link to {/* i18n: Clickable element */}<Link>documentation</Link></Trans>
cjayyy commented 5 years ago

Reason 1: Using Trans component shifts responsibility from parent to Trans component itself. As a developer, you don't have to worry about skipped updates.

Second reason are inline components. Using Trans component you can go wild a do things like this:

<Trans>
   Any component is <strong className="green">valid</strong> here, even <Link>custom</Link> ones.
</Trans>

Example above will result in single entry in your message catalog: Any component is <0>valid</0> here, even <1>custom</1> ones.

This has several advantages:

  1. Translator doesn't care about html tags
  2. Props of inline components doesn't affect message
  3. You get only one message. This is obvious, but other libs (i18next, formatjs) translate content of inline components separately which is unfortunate for translators.

According to i18next docs this doesn't seem to be up-to-date. In their example sandbox complex message extraction results into one piece.

tricoder42 commented 5 years ago

@cJayyy Yeah, sorry, this issues was opened almost 2 years ago. Meanwhile, react-i18next implemented similar concept with few exceptions. The main one is that they process JSX children at runtime - because of that you have to use double curly braces for variables. Unfortunately, object as a child isn't a valid JSX so some linters might not like it.

I'm closing this issue, as it was mostly discussion and it's clearly outdated.