react-intl
Version:
Internationalize React apps. This library provides React components and an API to format dates, numbers, and strings, including pluralization and handling translations.
158 lines (133 loc) • 4.79 kB
JavaScript
/*
* Copyright 2015, Yahoo Inc.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
import {Component, createElement, isValidElement} from 'react';
import PropTypes from 'prop-types';
import IntlMessageFormat from 'intl-messageformat';
import memoizeIntlConstructor from 'intl-format-cache';
import {intlShape, messageDescriptorPropTypes} from '../types';
import {
invariantIntlContext,
shallowEquals,
shouldIntlComponentUpdate,
} from '../utils';
import {formatMessage as baseFormatMessage} from '../format';
const defaultFormatMessage = (descriptor, values) => {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry. Using default message as fallback.'
);
}
return baseFormatMessage(
{},
{getMessageFormat: memoizeIntlConstructor(IntlMessageFormat)},
descriptor,
values
);
};
export default class FormattedMessage extends Component {
static displayName = 'FormattedMessage';
static contextTypes = {
intl: intlShape,
};
static propTypes = {
...messageDescriptorPropTypes,
values: PropTypes.object,
tagName: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
children: PropTypes.func,
};
static defaultProps = {
values: {},
};
constructor(props, context) {
super(props, context);
if (!props.defaultMessage) {
invariantIntlContext(context);
}
}
shouldComponentUpdate(nextProps, ...next) {
const {values} = this.props;
const {values: nextValues} = nextProps;
if (!shallowEquals(nextValues, values)) {
return true;
}
// Since `values` has already been checked, we know they're not
// different, so the current `values` are carried over so the shallow
// equals comparison on the other props isn't affected by the `values`.
let nextPropsToCheck = {
...nextProps,
values,
};
return shouldIntlComponentUpdate(this, nextPropsToCheck, ...next);
}
render() {
const {formatMessage = defaultFormatMessage, textComponent: Text = 'span'} =
this.context.intl || {};
const {
id,
description,
defaultMessage,
values,
tagName: Component = Text,
children,
} = this.props;
let tokenDelimiter;
let tokenizedValues;
let elements;
let hasValues = values && Object.keys(values).length > 0;
if (hasValues) {
// Creates a token with a random UID that should not be guessable or
// conflict with other parts of the `message` string.
let uid = Math.floor(Math.random() * 0x10000000000).toString(16);
let generateToken = (() => {
let counter = 0;
return () => `ELEMENT-${uid}-${(counter += 1)}`;
})();
// Splitting with a delimiter to support IE8. When using a regex
// with a capture group IE8 does not include the capture group in
// the resulting array.
tokenDelimiter = `@__${uid}__@`;
tokenizedValues = {};
elements = {};
// Iterates over the `props` to keep track of any React Element
// values so they can be represented by the `token` as a placeholder
// when the `message` is formatted. This allows the formatted
// message to then be broken-up into parts with references to the
// React Elements inserted back in.
Object.keys(values).forEach(name => {
let value = values[name];
if (isValidElement(value)) {
let token = generateToken();
tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;
elements[token] = value;
} else {
tokenizedValues[name] = value;
}
});
}
let descriptor = {id, description, defaultMessage};
let formattedMessage = formatMessage(descriptor, tokenizedValues || values);
let nodes;
let hasElements = elements && Object.keys(elements).length > 0;
if (hasElements) {
// Split the message into parts so the React Element values captured
// above can be inserted back into the rendered message. This
// approach allows messages to render with React Elements while
// keeping React's virtual diffing working properly.
nodes = formattedMessage
.split(tokenDelimiter)
.filter(part => !!part)
.map(part => elements[part] || part);
} else {
nodes = [formattedMessage];
}
if (typeof children === 'function') {
return children(...nodes);
}
// Needs to use `createElement()` instead of JSX, otherwise React will
// warn about a missing `key` prop with rich-text message formatting.
return createElement(Component, null, ...nodes);
}
}