Skip to content
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

Rich text formatting and translations #513

Closed
geekyme opened this issue Jun 30, 2016 · 31 comments
Closed

Rich text formatting and translations #513

geekyme opened this issue Jun 30, 2016 · 31 comments
Milestone

Comments

@geekyme
Copy link

geekyme commented Jun 30, 2016

Issue

Say i have a string:

To buy a shoe, 
<a class="external_link" target="_blank" href="https://www.shoe.com/">visit our website</a>
and
<strong class="important">eat a shoe</strong>

According to https://github.com/yahoo/react-intl/wiki/Components#formattedmessage, I need to format my text this way:

<FormattedMessage
    defaultMessage='To buy a shoe, { link } and { cta }'
    values={{
        link: <a class="external_link" target="_blank" href="https://www.shoe.com/">visit our website</a>,
        cta: <strong class="important">eat a shoe</strong>
    }}
/>

When doing string extraction with https://github.com/yahoo/babel-plugin-react-intl, I would get:

To buy a shoe, { link } and { cta } to be translated.

This isn't good because I lose the context and texts from the rich content for link and cta.

Suggestion

I think we should have some special delimiters to surround rich text content so translators can still translate the text as a whole:

To buy a shoe, @visit our website@ and @eat a shoe@

Thoughts?

@ericf
Copy link
Collaborator

ericf commented Jul 1, 2016

@geekyme can you explain more how this would work?

@geekyme
Copy link
Author

geekyme commented Jul 4, 2016

What I have in mind is FormattedMessage should look at the string presented in defaultMessage, and if it finds an opening and closing @, it takes the contents between them and look up a list of rich markups and interpolate the text inside the markup.

Eg. visit our website gets interpolated between <a class="external_link" target="_blank" href="https://www.shoe.com/"></a>. This can be done using React.cloneElement then providing the text as children. Same thing for eat a shoe

<FormattedMessage
    defaultMessage='To buy a shoe, @visit our website@ and @eat a shoe@'
   interpolate={[
       <a class="external_link" target="_blank" href="https://www.shoe.com/"></a>,
       <strong class="important">eat a shoe</strong>
   ]}
/>

@ericf
Copy link
Collaborator

ericf commented Jul 5, 2016

This sounds related to: #455 (comment)

Trying to make your example work can quickly break down, imagine if someone had:

<strong>eat a <em>shoe</em></strong>

@azmenak
Copy link

azmenak commented Aug 4, 2016

Came across a similar problem today, maybe there's a better way to do this that I'm not seeing at the moment.

If I want to "format" links in a message, the only option I see right now is passing an additional <FormattedMessage /> in the values of the parent. IE.

<FormattedMessage
  id="messageTextId"
  defaultMessage="Hello {name}, please go visit {link}"
  values={{
    name: <strong>{this.props.name}</strong>,
    link: (
      <Link to="/visit-me">
        <FormattedMessage
          id="messageTextID.linkText"
          defaultMessage="translated link title"
        />
      </Link>
    ),
  }}
/>

This works, but its quite a bit to add for each link inside a message (we have a lot of them). Maybe @ericf or someone else can shed some light on a better solution for this?

I kind of imagined defining a custom format for messages IE.

<FormattedMessage
  defaultMessage="Hello {name}, please go visit {linkKey, link, {href}}."
  values={{
    name: this.props.name,
    linkKey: "Text inside the link",
    href: "/some-page",
  }}
/>

but AFAIK, you can't define a custom format which isn't a number, date, or time.

@mxmzb
Copy link

mxmzb commented Sep 12, 2016

I am also currently running into this. I like the idea of nested placeholders like

<FormattedMessage
  defaultMessage="Hello {name}, please go visit {somekey(Text inside the link)}."
  values={{
    name: this.props.name,
    somekey: <Link to="/some-page">{someArbitraryPlaceholder}</Link>
  }}
/>

Even if it looks complicated, I think this would simplify the creation of natural messages files, where you wouldn't have to rip the link text out of the context of the parent message. And you wouldn't have to create a link builder into the template language, instead the use simply decides what the element around some text looks like.

@kamilio
Copy link

kamilio commented Oct 6, 2016

I needed some simple formatting in my messages so I just run it through markdown.

import React, { Component, PropTypes } from 'react';
import Markdown from 'react-remarkable';

class FormattedMessageMarkdown extends Component {
    render() {
        const { formatMessage } = this.context.intl;
        const { id, defaultMessage, values, description } = this.props;
        const messageDescriptor = { id, defaultMessage, values, description };
        const message = formatMessage(messageDescriptor);
        return (<Markdown source={message}/>);
    }
}

FormattedMessageMarkdown.contextTypes = {
    intl: PropTypes.object.isRequired
};

export default FormattedMessageMarkdown;

Works pretty well, highly recommended.

@ericf
Copy link
Collaborator

ericf commented Oct 6, 2016

Last week I had some time to start thinking about this problem. What I really want to make work is something like this:

<FormattedMessage id='email.sent'>
  Your <a href={message.url}>email</a> has been sent.
</FormattedMessage>

For the developer, id could be optional (See #612). And for the translator what they are need to see is the full text:

Your email has been sent.

The translator shouldn't need to worry about "email" is a hyperlink in the UI, and I don't want to limit support to just HTML tags like @kamilio's Markdown approach yields. If you're using React Router, then you have <Link> components (capital "L") that need to be executed with the React context. The key is something like what @thelamborghinistory showed above: rich-text can be modeled simply as a function call!

What I've landed on is using XML/JSX syntax within the ICU Message string that translators see. In my example above, it would compile to:

Your <x:0>email</x:0> has been sent.

The <x:0> is using x as the namespace, and 0 as the argument position. Named arguments could be used as well, eg. <x:link>. Now the translator can move around <x:0>email</x:0> as a unit — which I think will be easier for them than getting all the closing parenthesis correct.

To illustrate this better the following would render the same:

<FormattedMessage id='email.sent'>
  Your <a href='/sent'>email</a> has been sent.
</FormattedMessage>
<FormattedMessage
  id='email.sent'
  defaultMessage='Your <x:link>email</x:link> has been sent.'
  values={{
    link: (email) => <a href='/sent'>{email}</a>
  }}
/>

This shows how <x:link>email</x:link> ends up meaning:

  1. Lookup the link prop on values object.
  2. Assume link is a function and call it: values.link(translatedText).

I'd also like to support just providing JSX, but that would require marking the location of the placeholder which will be replaced by the translated text, here's an example of what that could look like:

<FormattedMessage
  id='email.sent'
  defaultMessage='Your <x:link>email</x:link> has been sent.'
  values={{
    link: <a href='/sent'><FormattedMessage.Placeholder/></a>
  }}
/>

It seems that this approach should scale up to even more complex messages like this:

<FormattedMessage id='emails.sent'>
  <a href='/sent'>
    <FormattedPlural
      value={emails.length}
      one={<span>Your <b>email</b> has been sent.</span>}
      other={<span>Your <b>emails</b> have been sent.</span>}
    />
  </a>
</FormattedMessage>

The above would be extracted using babel-plugin-react-intl into the following:

<x:0>
  {1, plural,
    one {<x:2>Your <x:3>email</x:3> has been sent.</x:2>}
    other {<x:4>Your <x:5>emails</x:5> have been sent.</x:4>}
  }
</x:0>

I'm working through all the various packages to make sure this is possible to implement in a backwards-compatible way. I also plan to write up a more formal RFC soon, but anyone subscribed or looking at this thread, let me know what you think!

@ptomasroos
Copy link
Contributor

Thats pretty epic @ericf !

@esquevin
Copy link

I'd love for react intl to support your syntax

<FormattedMessage [id] [values]>
     defaultMessage
</FormattedMessage>

Internally we have implemented something similar but a less advanced that already allows us to do:

const messages = defineMessages({
  main: {
    id: 'forbidden_text__link',
    defaultMessage: "You're not authorized to access this page.\n" +
                    'Please contact your admin or <Link>go back to homepage</Link>.',
  }
});

// [...]

<FormattedReact
        {...messages.main}
        components={{ Link: props => <Link to="/" {...props} /> }}
/>

It's a use case that should be natively handled by react intl and I can't wait for what you proposed to be released

@firaskrichi
Copy link

@ericf any progress on this? It would be awesome!!

@tricoder42
Copy link

tricoder42 commented Mar 4, 2017

If anyone interested, feel free to take a look at js-lingui. It supports any component inside translated messages:

// Developer is allowed to use any component inside <Trans>.
// All valid JSX is converted to ICU message format:
<Trans>See the <a href="/more">description</a> below.</Trans>

// Under the hood babel transforms component above to this:
<Trans id="See the <0>description</0> below." components=[<a href="/more" />] />

Pros:

  • supports any component (builtin, custom, pair, self-closing)
  • prop types of inner components doesn't affect translations (e.g: change of className doesn't require update of translations)
  • translator-friendly - only one message per component is generated (unlike current version of react-intl which require separate translations for inline components)
  • easy setup - doesn't require extra variable to store inline component. There's no difference between writing translated/untranslated text.

Cons:

  • requires extra babel plugin
  • translators might require information about what's the <0> tag (e.g: link, emphasis, etc)

I guess the babel plugin could be ported to support react-intl and then only a subtle change of <FormattedMessage /> API is required (and a bit more internally).

@bjbrewster
Copy link

bjbrewster commented Apr 24, 2017

Thanks @kamilio. Your markdown method works fantastic! Here is my updated version that works with simple interpolation values (formatMessage can't use values wrapped in React components) and ES2015.

import React from 'react'
import { intlShape } from 'react-intl'
import Markdown from 'react-remarkable'

const FormattedMarkdown = ({ id, defaultMessage, values }, { intl: { formatMessage } }) =>
  <Markdown source={formatMessage({ id, defaultMessage}, values)} />

FormattedMarkdown.contextTypes = {
  intl: intlShape
}

export default FormattedMarkdown

I use my own helper methods t and tm as I find FormattedMessage too verbose in most cases:

import React from 'react'
import { intlShape, FormattedMessage } from 'react-intl'
import FormattedMarkdown from './FormattedMarkdown'

export const t = (id, values = {}) =>
  <FormattedMessage id={id} values={values} defaultMessage={values.default} />

export const tm = (id, values = {}) =>
  <FormattedMarkdown id={id} values={values} defaultMessage={values.default} />

@eliseumds
Copy link

eliseumds commented Jul 21, 2017

@bjbrewster I guess babel-plugin-react-intl wouldn't be able to extract messages if you use factories like that. It would work if you use defineMessages, though:

export const t = (message, values) =>
  <FormattedMessage {...message} values={values} />

And then:

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  requiredFieldMsg: {
    id: 'form.errors.required_field',
    defaultMessage: 'Field {fieldName} is required'
  }
});

function SomeComponent() {
  return t(messages.requiredFieldMsg, { fieldName: 'Phone Number' });
}

@ephys
Copy link

ephys commented Jun 2, 2018

I made a version of FormattedMessage which maps XML tags to React Components:
https://github.com/ephys/react-intl-formatted-xml-message/

@joseph-galindo
Copy link

@ericf I think that proposal looks pretty cool...would definitely be a nicer way of allowing complex messages with components nested inside the message. For example, something that compiles to this:

<p>Please visit our helpdesk at <a href="https://foo.com">this link</a>, and please leave any feedback.</p>

Where the tag could be any arbitrary React component from an outside consumer, but that component needs to very specifically wrap the "this link" text.

@dan-lee
Copy link

dan-lee commented Mar 7, 2019

As we need this functionality right now, I created this package which tries to solve this in userland: react-intl-enhanced-message.

@stale
Copy link

stale bot commented May 30, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label May 30, 2019
@stale
Copy link

stale bot commented Jun 6, 2019

Issue was closed because of inactivity. If you think this is still a valid issue, please file a new issue with additional information.

@stale stale bot closed this as completed Jun 6, 2019
@priyajeet
Copy link

@box we are also experimenting with React components within the messages
https://github.com/box/box-ui-elements/tree/master/src/components/i18n

@longlho
Copy link
Member

longlho commented Jul 23, 2019

parser work: ad42f1f

@longlho
Copy link
Member

longlho commented Jul 24, 2019

This unfortunately cannot be done in the parser due to complexity in parsing XML. We'd have to embed a XML parser for this to work reliably in both DOM & Node. What other libraries do seem to be re-parsing the translated message as XML (not HTML), then do another round of formatting which might be the safest way to do it. Performance-wise it won't be great, but does provide better translation context.

@longlho
Copy link
Member

longlho commented Jul 25, 2019

Upstream change: #124

@longlho longlho added this to the v3.0.0 milestone Jul 25, 2019
@longlho
Copy link
Member

longlho commented Jul 29, 2019

3.0.0-beta.20 should have this already.

@longlho longlho closed this as completed Jul 29, 2019
@ephys
Copy link

ephys commented Jul 30, 2019

I'm so happy to see this being included natively, thank you for the hard work @longlho!

@ValeryVS
Copy link

ValeryVS commented Aug 7, 2019

@longlho
Should we add some example to documentation, or new docs are already in progress?
I made example here
https://codesandbox.io/s/charming-swartz-6bl13

@longlho
Copy link
Member

longlho commented Aug 7, 2019 via email

simonvomeyser added a commit to simonvomeyser/simonvomeyser.de-2020 that referenced this issue Feb 23, 2020
See formatjs/formatjs#513

This is not possbile within HTMLMessages, so split the paragraphs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests