Code

2017年5月7日日曜日

How to use React-Intl to format multiple message id

 Recently, I am working on some project using React-Intl to format strings. One of the bad things about it is, the FormattedMessage component only support one id and a long message. When you want to combine several short messages together, it's kind of hard.
You have to use something like this (please refer react-intl document):

<FormattedMessage
    id="welcome"
    defaultMessage={`Hello {name}, you have {unreadCount, number} {unreadCount, plural,
    one {message}
    other {messages}
    }`}
    values={{name: <b>{name}</b>, unreadCount}}
/>

When you want to do something like format aria-label for a button, you have to wrap a function:

<FormattedMessage id="aria-label-id" defaultMessage="label">
    { (label) =>
          <button
            type="button"
            className="test"
            aria-expanded={expanded}
            aria-label={label} />
    }
</FormattedMessage>

In fact it's quite hard to use, also the format makes it difficult to read if you have a lot of buttons to render.

An easy way to avoid this is to use it's injectIntl function to wrap your component, like this:

import {injectIntl, intlShape} from 'react-intl';

const ButtonComponent = ({className, intl}) => (
    <button className={className} aria-label={intl.formatMessage({id: "aria-label", defaultMessage="test"})}>
        <span>Button Icon</span>
    </button>
);

ButtonComponent = {
    date: PropTypes.any.isRequired,
    intl: intlShape.isRequired,
};

export default injectIntl(ButtonComponent);

 injectIntl will create a wrapper component for the ButtonComponent, which will inject a childContext with locale and message information. Also it inject the intl prop for using.
By this intl prop, you can use the APIs like formatMessage (which is used inside the FormattedMessage component).

 When you want to use multiple messages to combine together to be a string, currently, there is no good way to do it, as far as I read the documents and looking into the examples.
So I created something to do it:

class Formatter extends React.Component {
    static defaultProps = {
        as: 'span'
    };

    constructor(props) {
        super(props);
        this.getFormattedMsg = this.getFormattedMsg.bind(this);
        this.formatMessage = this.formatMessage.bind(this);
    }

    getFormattedMsgWithValue(match, p1) {
        const { intl, defaultMessage } = this.props;
        return intl.formatMessage({ id: p1, defaultMessage });
    }

    formatMessage(msg) {
        return msg.replace(/@{([\w]+)}/g, this.getFormattedMsg);
    }

    render() {
        const { intl, as, ariaLabel, children, ...others } = this.props;

        if (children) {
            if (typeof children === 'string') {
                others.childre = this.formatMessage(children);
            } else {
                others.children = children;
            }
        }

        if (ariaLabel) {
            others['aria-label'] = this.formatMessage(ariaLabel);
        }

        return React.createElement(as, others);
    }
}

export default injectIntl(Formatter);
 
Then you can use this Formatter function like this:

<Formatter as="button" className="btn" aria-expanded={expanded}>
    {`@{label-id} ${otherParams} @{another-label-id} ${others}`}
<Formatter>

Or you can just wrap some children like this:

<Formatter as="button" className="btn" aria-label={`{@{aria-label-id} others`}>
    <span className="btn-icon arrow"></span>
<Formatter>

If you want to format with values, just change getFormmatedMsg like the following:

    getFormattedMsgWithValue(match, p1) {
        const { intl, defaultMessage, values } = this.props;
        const targetValue = (values && values[p1]) || {};
        return intl.formatMessage({ id: p1, defaultMessage }, targetValue);
    }

Will raise a pull request for the FormmattedMessage component in react-intl later, when I got enough time to make it properly.