/* * Copyright 2015, Yahoo Inc. * Copyrights licensed under the New BSD License. * See the accompanying LICENSE file for terms. */ import {Component, Children} from 'react'; import PropTypes from 'prop-types'; import IntlMessageFormat from 'intl-messageformat'; import IntlRelativeFormat from 'intl-relativeformat'; import IntlPluralFormat from '../plural'; import memoizeIntlConstructor from 'intl-format-cache'; import invariant from 'invariant'; import { createError, defaultErrorHandler, shouldIntlComponentUpdate, filterProps, } from '../utils'; import {intlConfigPropTypes, intlFormatPropTypes, intlShape} from '../types'; import * as format from '../format'; import {hasLocaleData} from '../locale-data-registry'; const intlConfigPropNames = Object.keys(intlConfigPropTypes); const intlFormatPropNames = Object.keys(intlFormatPropTypes); // These are not a static property on the `IntlProvider` class so the intl // config values can be inherited from an ancestor. const defaultProps = { formats: {}, messages: {}, timeZone: null, textComponent: 'span', defaultLocale: 'en', defaultFormats: {}, onError: defaultErrorHandler, }; export default class IntlProvider extends Component { static displayName = 'IntlProvider'; static contextTypes = { intl: intlShape, }; static childContextTypes = { intl: intlShape.isRequired, }; static propTypes = { ...intlConfigPropTypes, children: PropTypes.element.isRequired, initialNow: PropTypes.any, }; constructor(props, context = {}) { super(props, context); invariant( typeof Intl !== 'undefined', '[React Intl] The `Intl` APIs must be available in the runtime, ' + 'and do not appear to be built-in. An `Intl` polyfill should be loaded.\n' + 'See: http://formatjs.io/guides/runtime-environments/' ); const {intl: intlContext} = context; // Used to stabilize time when performing an initial rendering so that // all relative times use the same reference "now" time. let initialNow; if (isFinite(props.initialNow)) { initialNow = Number(props.initialNow); } else { // When an `initialNow` isn't provided via `props`, look to see an // exists in the ancestry and call its `now()` // function to propagate its value for "now". initialNow = intlContext ? intlContext.now() : Date.now(); } // Creating `Intl*` formatters is expensive. If there's a parent // ``, then its formatters will be used. Otherwise, this // memoize the `Intl*` constructors and cache them for the lifecycle of // this IntlProvider instance. const { formatters = { getDateTimeFormat: memoizeIntlConstructor(Intl.DateTimeFormat), getNumberFormat: memoizeIntlConstructor(Intl.NumberFormat), getMessageFormat: memoizeIntlConstructor(IntlMessageFormat), getRelativeFormat: memoizeIntlConstructor(IntlRelativeFormat), getPluralFormat: memoizeIntlConstructor(IntlPluralFormat), }, } = intlContext || {}; this.state = { ...formatters, // Wrapper to provide stable "now" time for initial render. now: () => { return this._didDisplay ? Date.now() : initialNow; }, }; } getConfig() { const {intl: intlContext} = this.context; // Build a whitelisted config object from `props`, defaults, and // `context.intl`, if an exists in the ancestry. let config = filterProps(this.props, intlConfigPropNames, intlContext); // Apply default props. This must be applied last after the props have // been resolved and inherited from any in the ancestry. // This matches how React resolves `defaultProps`. for (let propName in defaultProps) { if (config[propName] === undefined) { config[propName] = defaultProps[propName]; } } if (!hasLocaleData(config.locale)) { const {locale, defaultLocale, defaultFormats, onError} = config; onError( createError( `Missing locale data for locale: "${locale}". ` + `Using default locale: "${defaultLocale}" as fallback.` ) ); // Since there's no registered locale data for `locale`, this will // fallback to the `defaultLocale` to make sure things can render. // The `messages` are overridden to the `defaultProps` empty object // to maintain referential equality across re-renders. It's assumed // each contains a `defaultMessage` prop. config = { ...config, locale: defaultLocale, formats: defaultFormats, messages: defaultProps.messages, }; } return config; } getBoundFormatFns(config, state) { return intlFormatPropNames.reduce((boundFormatFns, name) => { boundFormatFns[name] = format[name].bind(null, config, state); return boundFormatFns; }, {}); } getChildContext() { const config = this.getConfig(); // Bind intl factories and current config to the format functions. const boundFormatFns = this.getBoundFormatFns(config, this.state); const {now, ...formatters} = this.state; return { intl: { ...config, ...boundFormatFns, formatters, now, }, }; } shouldComponentUpdate(...next) { return shouldIntlComponentUpdate(this, ...next); } componentDidMount() { this._didDisplay = true; } render() { return Children.only(this.props.children); } }