Rich text formatting and translations
geekyme opened this issue ยท 31 comments
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?
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>
]}
/>
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>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.
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.
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.
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:
- Lookup the
linkprop onvaluesobject. - Assume
linkis 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!
Thats pretty epic @ericf !
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
@ericf any progress on this? It would be awesome!!
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-intlwhich 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).
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} />
@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' });
}@eliseumds Thanks for the tip. I don't use the extract messages babel plugin but I am keen to try it out. I currently have one json locale file with all the message text for my app that is getting unwieldy. React Components really deserve putting their locale text in the Component's file/folder so if you delete the component, its locale text is automatically cleaned up. I have worked on too many projects with monolithic string tables with MANY unused strings.
@ericf Curious if you have any ideas of how soon (or not) is your proposed implementation might make it?
(The current solution is breaking down for us if say we have two or three inline links in a paragraph and are using defineMessages method - some context is shifted to the react component while other stays in the messages file)
@kamilio In your markdown example, what is your process for going from markdown to the source json?
Hi, what is the status of the proposed solutions to this?
If anyone is interested (/cc @Hurtak), I made a component that maps any XML in the message to react components similar to what was proposed in this thread.
https://gist.github.com/Ephys/e70c35ed597f08509a9016368d2ae3ab
(I'll publish a more polished, isomorphic, version on NPM if there is interest)
I made a version of FormattedMessage which maps XML tags to React Components:
https://github.com/ephys/react-intl-formatted-xml-message/
@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.
As we need this functionality right now, I created this package which tries to solve this in userland: react-intl-enhanced-message.
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.
Issue was closed because of inactivity. If you think this is still a valid issue, please file a new issue with additional information.
@box we are also experimenting with React components within the messages
https://github.com/box/box-ui-elements/tree/master/src/components/i18n
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.
3.0.0-beta.20 should have this already.
I'm so happy to see this being included natively, thank you for the hard work @longlho!
@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