Bug 1450071 - Vendor fluent-react in devtools/client/shared;r=jryans,stas draft
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 28 May 2018 21:26:57 +0200
changeset 802592 549200589ee8f33a950cf28787ae170ee43229c1
parent 802528 42880a726964a0bd66e2f636931e8322eae86ef7
child 802593 31b6ccb2515425812770770417b0ed2f82a007f3
push id111921
push userjdescottes@mozilla.com
push dateFri, 01 Jun 2018 07:43:58 +0000
reviewersjryans, stas
bugs1450071
milestone62.0a1
Bug 1450071 - Vendor fluent-react in devtools/client/shared;r=jryans,stas Ultimately fluent.js should not be vendored here, since fluent-react only needs 2 methods from fluent.js. Work is currently ongoing to extract those dependencies to separate packages, once ready we will remove this vendored version of fluent.js. MozReview-Commit-ID: E5uwsCHQ7tj
devtools/client/shared/vendor/FLUENT_REACT_LICENSE
devtools/client/shared/vendor/FLUENT_REACT_UPGRADING
devtools/client/shared/vendor/fluent-react.js
devtools/client/shared/vendor/fluent.js
devtools/client/shared/vendor/moz.build
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/FLUENT_REACT_LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/FLUENT_REACT_UPGRADING
@@ -0,0 +1,17 @@
+Follow these steps when adding/upgrading the fluent-react.js module:
+
+1. git clone https://github.com/projectfluent/fluent.js - clone the repo
+2. checkout the tag for the version to update
+3. build bundles:
+  3.1. npm install
+  3.2. make deps
+  3.3. make
+4. cp fluent-react/fluent-react.js $DEST_DIR - copy fluent-react to Firefox source tree
+5. in fluent-react.js, prepend "devtools/client/shared/vendor/" to the require calls
+  require('fluent') should be require('devtools/client/shared/vendor/fluent')
+  require('react') should be require('devtools/client/shared/vendor/react')
+  require('react-prop-types') should be require('devtools/client/shared/vendor/react-prop-types')
+6. cp fluent-react/node_modules/fluent/fluent.js $DEST_DIR - copy fluent to Firefox source tree
+7. update the version below
+
+The current version is 0.7.0
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/fluent-react.js
@@ -0,0 +1,492 @@
+/* fluent-react@0.7.0 */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('devtools/client/shared/vendor/fluent'), require('devtools/client/shared/vendor/react'), require('devtools/client/shared/vendor/react-prop-types')) :
+  typeof define === 'function' && define.amd ? define('fluent-react', ['exports', 'fluent', 'react', 'prop-types'], factory) :
+  (factory((global.FluentReact = {}),global.Fluent,global.React,global.PropTypes));
+}(this, (function (exports,fluent,react,PropTypes) { 'use strict';
+
+  PropTypes = PropTypes && PropTypes.hasOwnProperty('default') ? PropTypes['default'] : PropTypes;
+
+  /*
+   * `ReactLocalization` handles translation formatting and fallback.
+   *
+   * The current negotiated fallback chain of languages is stored in the
+   * `ReactLocalization` instance in form of an iterable of `MessageContext`
+   * instances.  This iterable is used to find the best existing translation for
+   * a given identifier.
+   *
+   * `Localized` components must subscribe to the changes of the
+   * `ReactLocalization`'s fallback chain.  When the fallback chain changes (the
+   * `messages` iterable is set anew), all subscribed compontent must relocalize.
+   *
+   * The `ReactLocalization` class instances are exposed to `Localized` elements
+   * via the `LocalizationProvider` component.
+   */
+  class ReactLocalization {
+    constructor(messages) {
+      this.contexts = new fluent.CachedIterable(messages);
+      this.subs = new Set();
+    }
+
+    /*
+     * Subscribe a `Localized` component to changes of `messages`.
+     */
+    subscribe(comp) {
+      this.subs.add(comp);
+    }
+
+    /*
+     * Unsubscribe a `Localized` component from `messages` changes.
+     */
+    unsubscribe(comp) {
+      this.subs.delete(comp);
+    }
+
+    /*
+     * Set a new `messages` iterable and trigger the retranslation.
+     */
+    setMessages(messages) {
+      this.contexts = new fluent.CachedIterable(messages);
+
+      // Update all subscribed Localized components.
+      this.subs.forEach(comp => comp.relocalize());
+    }
+
+    getMessageContext(id) {
+      return fluent.mapContextSync(this.contexts, id);
+    }
+
+    formatCompound(mcx, msg, args) {
+      const value = mcx.format(msg, args);
+
+      if (msg.attrs) {
+        var attrs = {};
+        for (const name of Object.keys(msg.attrs)) {
+          attrs[name] = mcx.format(msg.attrs[name], args);
+        }
+      }
+
+      return { value, attrs };
+    }
+
+    /*
+     * Find a translation by `id` and format it to a string using `args`.
+     */
+    getString(id, args, fallback) {
+      const mcx = this.getMessageContext(id);
+
+      if (mcx === null) {
+        return fallback || id;
+      }
+
+      const msg = mcx.getMessage(id);
+      return mcx.format(msg, args);
+    }
+  }
+
+  function isReactLocalization(props, propName) {
+    const prop = props[propName];
+
+    if (prop instanceof ReactLocalization) {
+      return null;
+    }
+
+    return new Error(
+      `The ${propName} context field must be an instance of ReactLocalization.`
+    );
+  }
+
+  /*
+   * The Provider component for the `ReactLocalization` class.
+   *
+   * Exposes a `ReactLocalization` instance to all descendants via React's
+   * context feature.  It makes translations available to all localizable
+   * elements in the descendant's render tree without the need to pass them
+   * explicitly.
+   *
+   *     <LocalizationProvider messages={…}>
+   *         …
+   *     </LocalizationProvider>
+   *
+   * The `LocalizationProvider` component takes one prop: `messages`.  It should
+   * be an iterable of `MessageContext` instances in order of the user's
+   * preferred languages.  The `MessageContext` instances will be used by
+   * `ReactLocalization` to format translations.  If a translation is missing in
+   * one instance, `ReactLocalization` will fall back to the next one.
+   */
+  class LocalizationProvider extends react.Component {
+    constructor(props) {
+      super(props);
+      const { messages } = props;
+
+      if (messages === undefined) {
+        throw new Error("LocalizationProvider must receive the messages prop.");
+      }
+
+      if (!messages[Symbol.iterator]) {
+        throw new Error("The messages prop must be an iterable.");
+      }
+
+      this.l10n = new ReactLocalization(messages);
+    }
+
+    getChildContext() {
+      return {
+        l10n: this.l10n
+      };
+    }
+
+    componentWillReceiveProps(next) {
+      const { messages } = next;
+
+      if (messages !== this.props.messages) {
+        this.l10n.setMessages(messages);
+      }
+    }
+
+    render() {
+      return react.Children.only(this.props.children);
+    }
+  }
+
+  LocalizationProvider.childContextTypes = {
+    l10n: isReactLocalization
+  };
+
+  LocalizationProvider.propTypes = {
+    children: PropTypes.element.isRequired,
+    messages: isIterable
+  };
+
+  function isIterable(props, propName, componentName) {
+    const prop = props[propName];
+
+    if (Symbol.iterator in Object(prop)) {
+      return null;
+    }
+
+    return new Error(
+      `The ${propName} prop supplied to ${componentName} must be an iterable.`
+    );
+  }
+
+  function withLocalization(Inner) {
+    class WithLocalization extends react.Component {
+      componentDidMount() {
+        const { l10n } = this.context;
+
+        if (l10n) {
+          l10n.subscribe(this);
+        }
+      }
+
+      componentWillUnmount() {
+        const { l10n } = this.context;
+
+        if (l10n) {
+          l10n.unsubscribe(this);
+        }
+      }
+
+      /*
+       * Rerender this component in a new language.
+       */
+      relocalize() {
+        // When the `ReactLocalization`'s fallback chain changes, update the
+        // component.
+        this.forceUpdate();
+      }
+
+      /*
+       * Find a translation by `id` and format it to a string using `args`.
+       */
+      getString(id, args, fallback) {
+        const { l10n } = this.context;
+
+        if (!l10n) {
+          return fallback || id;
+        }
+
+        return l10n.getString(id, args, fallback);
+      }
+
+      render() {
+        return react.createElement(
+          Inner,
+          Object.assign(
+            // getString needs to be re-bound on updates to trigger a re-render
+            { getString: (...args) => this.getString(...args) },
+            this.props
+          )
+        );
+      }
+    }
+
+    WithLocalization.displayName = `WithLocalization(${displayName(Inner)})`;
+
+    WithLocalization.contextTypes = {
+      l10n: isReactLocalization
+    };
+
+    return WithLocalization;
+  }
+
+  function displayName(component) {
+    return component.displayName || component.name || "Component";
+  }
+
+  /* eslint-env browser */
+
+  const TEMPLATE = document.createElement("template");
+
+  function parseMarkup(str) {
+    TEMPLATE.innerHTML = str;
+    return TEMPLATE.content;
+  }
+
+  /**
+   * Copyright (c) 2013-present, Facebook, Inc.
+   *
+   * This source code is licensed under the MIT license found in the
+   * LICENSE file in this directory.
+   */
+
+  // For HTML, certain tags should omit their close tag. We keep a whitelist for
+  // those special-case tags.
+
+  var omittedCloseTags = {
+    area: true,
+    base: true,
+    br: true,
+    col: true,
+    embed: true,
+    hr: true,
+    img: true,
+    input: true,
+    keygen: true,
+    link: true,
+    meta: true,
+    param: true,
+    source: true,
+    track: true,
+    wbr: true,
+    // NOTE: menuitem's close tag should be omitted, but that causes problems.
+  };
+
+  /**
+   * Copyright (c) 2013-present, Facebook, Inc.
+   *
+   * This source code is licensed under the MIT license found in the
+   * LICENSE file in this directory.
+   */
+
+  // For HTML, certain tags cannot have children. This has the same purpose as
+  // `omittedCloseTags` except that `menuitem` should still have its closing tag.
+
+  var voidElementTags = {
+    menuitem: true,
+    ...omittedCloseTags,
+  };
+
+  // Match the opening angle bracket (<) in HTML tags, and HTML entities like
+  // &amp;, &#0038;, &#x0026;.
+  const reMarkup = /<|&#?\w+;/;
+
+  /*
+   * Prepare props passed to `Localized` for formatting.
+   */
+  function toArguments(props) {
+    const args = {};
+    const elems = {};
+
+    for (const [propname, propval] of Object.entries(props)) {
+      if (propname.startsWith("$")) {
+        const name = propname.substr(1);
+        args[name] = propval;
+      } else if (react.isValidElement(propval)) {
+        // We'll try to match localNames of elements found in the translation with
+        // names of elements passed as props. localNames are always lowercase.
+        const name = propname.toLowerCase();
+        elems[name] = propval;
+      }
+    }
+
+    return [args, elems];
+  }
+
+  /*
+   * The `Localized` class renders its child with translated props and children.
+   *
+   *     <Localized id="hello-world">
+   *         <p>{'Hello, world!'}</p>
+   *     </Localized>
+   *
+   * The `id` prop should be the unique identifier of the translation.  Any
+   * attributes found in the translation will be applied to the wrapped element.
+   *
+   * Arguments to the translation can be passed as `$`-prefixed props on
+   * `Localized`.
+   *
+   *     <Localized id="hello-world" $username={name}>
+   *         <p>{'Hello, { $username }!'}</p>
+   *     </Localized>
+   *
+   *  It's recommended that the contents of the wrapped component be a string
+   *  expression.  The string will be used as the ultimate fallback if no
+   *  translation is available.  It also makes it easy to grep for strings in the
+   *  source code.
+   */
+  class Localized extends react.Component {
+    componentDidMount() {
+      const { l10n } = this.context;
+
+      if (l10n) {
+        l10n.subscribe(this);
+      }
+    }
+
+    componentWillUnmount() {
+      const { l10n } = this.context;
+
+      if (l10n) {
+        l10n.unsubscribe(this);
+      }
+    }
+
+    /*
+     * Rerender this component in a new language.
+     */
+    relocalize() {
+      // When the `ReactLocalization`'s fallback chain changes, update the
+      // component.
+      this.forceUpdate();
+    }
+
+    render() {
+      const { l10n } = this.context;
+      const { id, attrs, children } = this.props;
+      const elem = react.Children.only(children);
+
+      if (!l10n) {
+        // Use the wrapped component as fallback.
+        return elem;
+      }
+
+      const mcx = l10n.getMessageContext(id);
+
+      if (mcx === null) {
+        // Use the wrapped component as fallback.
+        return elem;
+      }
+
+      const msg = mcx.getMessage(id);
+      const [args, elems] = toArguments(this.props);
+      const {
+        value: messageValue,
+        attrs: messageAttrs
+      } = l10n.formatCompound(mcx, msg, args);
+
+      // The default is to forbid all message attributes. If the attrs prop exists
+      // on the Localized instance, only set message attributes which have been
+      // explicitly allowed by the developer.
+      if (attrs && messageAttrs) {
+        var localizedProps = {};
+
+        for (const [name, value] of Object.entries(messageAttrs)) {
+          if (attrs[name]) {
+            localizedProps[name] = value;
+          }
+        }
+      }
+
+      // If the wrapped component is a known void element, explicitly dismiss the
+      // message value and do not pass it to cloneElement in order to avoid the
+      // "void element tags must neither have `children` nor use
+      // `dangerouslySetInnerHTML`" error.
+      if (elem.type in voidElementTags) {
+        return react.cloneElement(elem, localizedProps);
+      }
+
+      // If the message has a null value, we're only interested in its attributes.
+      // Do not pass the null value to cloneElement as it would nuke all children
+      // of the wrapped component.
+      if (messageValue === null) {
+        return react.cloneElement(elem, localizedProps);
+      }
+
+      // If the message value doesn't contain any markup nor any HTML entities,
+      // insert it as the only child of the wrapped component.
+      if (!reMarkup.test(messageValue)) {
+        return react.cloneElement(elem, localizedProps, messageValue);
+      }
+
+      // If the message contains markup, parse it and try to match the children
+      // found in the translation with the props passed to this Localized.
+      const translationNodes = Array.from(parseMarkup(messageValue).childNodes);
+      const translatedChildren = translationNodes.map(childNode => {
+        if (childNode.nodeType === childNode.TEXT_NODE) {
+          return childNode.textContent;
+        }
+
+        // If the child is not expected just take its textContent.
+        if (!elems.hasOwnProperty(childNode.localName)) {
+          return childNode.textContent;
+        }
+
+        const sourceChild = elems[childNode.localName];
+
+        // If the element passed as a prop to <Localized> is a known void element,
+        // explicitly dismiss any textContent which might have accidentally been
+        // defined in the translation to prevent the "void element tags must not
+        // have children" error.
+        if (sourceChild.type in voidElementTags) {
+          return sourceChild;
+        }
+
+        // TODO Protect contents of elements wrapped in <Localized>
+        // https://github.com/projectfluent/fluent.js/issues/184
+        // TODO  Control localizable attributes on elements passed as props
+        // https://github.com/projectfluent/fluent.js/issues/185
+        return react.cloneElement(sourceChild, null, childNode.textContent);
+      });
+
+      return react.cloneElement(elem, localizedProps, ...translatedChildren);
+    }
+  }
+
+  Localized.contextTypes = {
+    l10n: isReactLocalization
+  };
+
+  Localized.propTypes = {
+    children: PropTypes.element.isRequired,
+  };
+
+  /*
+   * @module fluent-react
+   * @overview
+   *
+
+   * `fluent-react` provides React bindings for Fluent.  It takes advantage of
+   * React's Components system and the virtual DOM.  Translations are exposed to
+   * components via the provider pattern.
+   *
+   *     <LocalizationProvider messages={…}>
+   *         <Localized id="hello-world">
+   *             <p>{'Hello, world!'}</p>
+   *         </Localized>
+   *     </LocalizationProvider>
+   *
+   * Consult the documentation of the `LocalizationProvider` and the `Localized`
+   * components for more information.
+   */
+
+  exports.LocalizationProvider = LocalizationProvider;
+  exports.withLocalization = withLocalization;
+  exports.Localized = Localized;
+  exports.ReactLocalization = ReactLocalization;
+  exports.isReactLocalization = isReactLocalization;
+
+  Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/fluent.js
@@ -0,0 +1,2075 @@
+/* fluent@0.6.4 */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+  typeof define === 'function' && define.amd ? define('fluent', ['exports'], factory) :
+  (factory((global.Fluent = {})));
+}(this, (function (exports) { 'use strict';
+
+/*  eslint no-magic-numbers: [0]  */
+
+const MAX_PLACEABLES = 100;
+
+const entryIdentifierRe = /-?[a-zA-Z][a-zA-Z0-9_-]*/y;
+const identifierRe = /[a-zA-Z][a-zA-Z0-9_-]*/y;
+const functionIdentifierRe = /^[A-Z][A-Z_?-]*$/;
+
+/**
+ * The `Parser` class is responsible for parsing FTL resources.
+ *
+ * It's only public method is `getResource(source)` which takes an FTL string
+ * and returns a two element Array with an Object of entries generated from the
+ * source as the first element and an array of SyntaxError objects as the
+ * second.
+ *
+ * This parser is optimized for runtime performance.
+ *
+ * There is an equivalent of this parser in syntax/parser which is
+ * generating full AST which is useful for FTL tools.
+ */
+class RuntimeParser {
+  /**
+   * Parse FTL code into entries formattable by the MessageContext.
+   *
+   * Given a string of FTL syntax, return a map of entries that can be passed
+   * to MessageContext.format and a list of errors encountered during parsing.
+   *
+   * @param {String} string
+   * @returns {Array<Object, Array>}
+   */
+  getResource(string) {
+    this._source = string;
+    this._index = 0;
+    this._length = string.length;
+    this.entries = {};
+
+    const errors = [];
+
+    this.skipWS();
+    while (this._index < this._length) {
+      try {
+        this.getEntry();
+      } catch (e) {
+        if (e instanceof SyntaxError) {
+          errors.push(e);
+
+          this.skipToNextEntryStart();
+        } else {
+          throw e;
+        }
+      }
+      this.skipWS();
+    }
+
+    return [this.entries, errors];
+  }
+
+  /**
+   * Parse the source string from the current index as an FTL entry
+   * and add it to object's entries property.
+   *
+   * @private
+   */
+  getEntry() {
+    // The index here should either be at the beginning of the file
+    // or right after new line.
+    if (this._index !== 0 &&
+        this._source[this._index - 1] !== "\n") {
+      throw this.error(`Expected an entry to start
+        at the beginning of the file or on a new line.`);
+    }
+
+    const ch = this._source[this._index];
+
+    // We don't care about comments or sections at runtime
+    if (ch === "/" ||
+      (ch === "#" &&
+        [" ", "#", "\n"].includes(this._source[this._index + 1]))) {
+      this.skipComment();
+      return;
+    }
+
+    if (ch === "[") {
+      this.skipSection();
+      return;
+    }
+
+    this.getMessage();
+  }
+
+  /**
+   * Skip the section entry from the current index.
+   *
+   * @private
+   */
+  skipSection() {
+    this._index += 1;
+    if (this._source[this._index] !== "[") {
+      throw this.error('Expected "[[" to open a section');
+    }
+
+    this._index += 1;
+
+    this.skipInlineWS();
+    this.getVariantName();
+    this.skipInlineWS();
+
+    if (this._source[this._index] !== "]" ||
+        this._source[this._index + 1] !== "]") {
+      throw this.error('Expected "]]" to close a section');
+    }
+
+    this._index += 2;
+  }
+
+  /**
+   * Parse the source string from the current index as an FTL message
+   * and add it to the entries property on the Parser.
+   *
+   * @private
+   */
+  getMessage() {
+    const id = this.getEntryIdentifier();
+
+    this.skipInlineWS();
+
+    if (this._source[this._index] === "=") {
+      this._index++;
+    }
+
+    this.skipInlineWS();
+
+    const val = this.getPattern();
+
+    if (id.startsWith("-") && val === null) {
+      throw this.error("Expected term to have a value");
+    }
+
+    let attrs = null;
+
+    if (this._source[this._index] === " ") {
+      const lineStart = this._index;
+      this.skipInlineWS();
+
+      if (this._source[this._index] === ".") {
+        this._index = lineStart;
+        attrs = this.getAttributes();
+      }
+    }
+
+    if (attrs === null && typeof val === "string") {
+      this.entries[id] = val;
+    } else {
+      if (val === null && attrs === null) {
+        throw this.error("Expected message to have a value or attributes");
+      }
+
+      this.entries[id] = {};
+
+      if (val !== null) {
+        this.entries[id].val = val;
+      }
+
+      if (attrs !== null) {
+        this.entries[id].attrs = attrs;
+      }
+    }
+  }
+
+  /**
+   * Skip whitespace.
+   *
+   * @private
+   */
+  skipWS() {
+    let ch = this._source[this._index];
+    while (ch === " " || ch === "\n" || ch === "\t" || ch === "\r") {
+      ch = this._source[++this._index];
+    }
+  }
+
+  /**
+   * Skip inline whitespace (space and \t).
+   *
+   * @private
+   */
+  skipInlineWS() {
+    let ch = this._source[this._index];
+    while (ch === " " || ch === "\t") {
+      ch = this._source[++this._index];
+    }
+  }
+
+  /**
+   * Skip blank lines.
+   *
+   * @private
+   */
+  skipBlankLines() {
+    while (true) {
+      const ptr = this._index;
+
+      this.skipInlineWS();
+
+      if (this._source[this._index] === "\n") {
+        this._index += 1;
+      } else {
+        this._index = ptr;
+        break;
+      }
+    }
+  }
+
+  /**
+   * Get identifier using the provided regex.
+   *
+   * By default this will get identifiers of public messages, attributes and
+   * external arguments (without the $).
+   *
+   * @returns {String}
+   * @private
+   */
+  getIdentifier(re = identifierRe) {
+    re.lastIndex = this._index;
+    const result = re.exec(this._source);
+
+    if (result === null) {
+      this._index += 1;
+      throw this.error(`Expected an identifier [${re.toString()}]`);
+    }
+
+    this._index = re.lastIndex;
+    return result[0];
+  }
+
+  /**
+   * Get identifier of a Message or a Term (staring with a dash).
+   *
+   * @returns {String}
+   * @private
+   */
+  getEntryIdentifier() {
+    return this.getIdentifier(entryIdentifierRe);
+  }
+
+  /**
+   * Get Variant name.
+   *
+   * @returns {Object}
+   * @private
+   */
+  getVariantName() {
+    let name = "";
+
+    const start = this._index;
+    let cc = this._source.charCodeAt(this._index);
+
+    if ((cc >= 97 && cc <= 122) || // a-z
+        (cc >= 65 && cc <= 90) || // A-Z
+        cc === 95 || cc === 32) { // _ <space>
+      cc = this._source.charCodeAt(++this._index);
+    } else {
+      throw this.error("Expected a keyword (starting with [a-zA-Z_])");
+    }
+
+    while ((cc >= 97 && cc <= 122) || // a-z
+           (cc >= 65 && cc <= 90) || // A-Z
+           (cc >= 48 && cc <= 57) || // 0-9
+           cc === 95 || cc === 45 || cc === 32) { // _- <space>
+      cc = this._source.charCodeAt(++this._index);
+    }
+
+    // If we encountered the end of name, we want to test if the last
+    // collected character is a space.
+    // If it is, we will backtrack to the last non-space character because
+    // the keyword cannot end with a space character.
+    while (this._source.charCodeAt(this._index - 1) === 32) {
+      this._index--;
+    }
+
+    name += this._source.slice(start, this._index);
+
+    return { type: "varname", name };
+  }
+
+  /**
+   * Get simple string argument enclosed in `"`.
+   *
+   * @returns {String}
+   * @private
+   */
+  getString() {
+    const start = this._index + 1;
+
+    while (++this._index < this._length) {
+      const ch = this._source[this._index];
+
+      if (ch === '"') {
+        break;
+      }
+
+      if (ch === "\n") {
+        throw this.error("Unterminated string expression");
+      }
+    }
+
+    return this._source.substring(start, this._index++);
+  }
+
+  /**
+   * Parses a Message pattern.
+   * Message Pattern may be a simple string or an array of strings
+   * and placeable expressions.
+   *
+   * @returns {String|Array}
+   * @private
+   */
+  getPattern() {
+    // We're going to first try to see if the pattern is simple.
+    // If it is we can just look for the end of the line and read the string.
+    //
+    // Then, if either the line contains a placeable opening `{` or the
+    // next line starts an indentation, we switch to complex pattern.
+    const start = this._index;
+    let eol = this._source.indexOf("\n", this._index);
+
+    if (eol === -1) {
+      eol = this._length;
+    }
+
+    const firstLineContent = start !== eol ?
+      this._source.slice(start, eol) : null;
+
+    if (firstLineContent && firstLineContent.includes("{")) {
+      return this.getComplexPattern();
+    }
+
+    this._index = eol + 1;
+
+    this.skipBlankLines();
+
+    if (this._source[this._index] !== " ") {
+      // No indentation means we're done with this message. Callers should check
+      // if the return value here is null. It may be OK for messages, but not OK
+      // for terms, attributes and variants.
+      return firstLineContent;
+    }
+
+    const lineStart = this._index;
+
+    this.skipInlineWS();
+
+    if (this._source[this._index] === ".") {
+      // The pattern is followed by an attribute. Rewind _index to the first
+      // column of the current line as expected by getAttributes.
+      this._index = lineStart;
+      return firstLineContent;
+    }
+
+    if (firstLineContent) {
+      // It's a multiline pattern which started on the same line as the
+      // identifier. Reparse the whole pattern to make sure we get all of it.
+      this._index = start;
+    }
+
+    return this.getComplexPattern();
+  }
+
+  /**
+   * Parses a complex Message pattern.
+   * This function is called by getPattern when the message is multiline,
+   * or contains escape chars or placeables.
+   * It does full parsing of complex patterns.
+   *
+   * @returns {Array}
+   * @private
+   */
+  /* eslint-disable complexity */
+  getComplexPattern() {
+    let buffer = "";
+    const content = [];
+    let placeables = 0;
+
+    let ch = this._source[this._index];
+
+    while (this._index < this._length) {
+      // This block handles multi-line strings combining strings separated
+      // by new line.
+      if (ch === "\n") {
+        this._index++;
+
+        // We want to capture the start and end pointers
+        // around blank lines and add them to the buffer
+        // but only if the blank lines are in the middle
+        // of the string.
+        const blankLinesStart = this._index;
+        this.skipBlankLines();
+        const blankLinesEnd = this._index;
+
+
+        if (this._source[this._index] !== " ") {
+          break;
+        }
+        this.skipInlineWS();
+
+        if (this._source[this._index] === "}" ||
+            this._source[this._index] === "[" ||
+            this._source[this._index] === "*" ||
+            this._source[this._index] === ".") {
+          this._index = blankLinesEnd;
+          break;
+        }
+
+        buffer += this._source.substring(blankLinesStart, blankLinesEnd);
+
+        if (buffer.length || content.length) {
+          buffer += "\n";
+        }
+        ch = this._source[this._index];
+        continue;
+      } else if (ch === "\\") {
+        const ch2 = this._source[this._index + 1];
+        if (ch2 === '"' || ch2 === "{" || ch2 === "\\") {
+          ch = ch2;
+          this._index++;
+        }
+      } else if (ch === "{") {
+        // Push the buffer to content array right before placeable
+        if (buffer.length) {
+          content.push(buffer);
+        }
+        if (placeables > MAX_PLACEABLES - 1) {
+          throw this.error(
+            `Too many placeables, maximum allowed is ${MAX_PLACEABLES}`);
+        }
+        buffer = "";
+        content.push(this.getPlaceable());
+
+        this._index++;
+
+        ch = this._source[this._index];
+        placeables++;
+        continue;
+      }
+
+      if (ch) {
+        buffer += ch;
+      }
+      this._index++;
+      ch = this._source[this._index];
+    }
+
+    if (content.length === 0) {
+      return buffer.length ? buffer : null;
+    }
+
+    if (buffer.length) {
+      content.push(buffer);
+    }
+
+    return content;
+  }
+  /* eslint-enable complexity */
+
+  /**
+   * Parses a single placeable in a Message pattern and returns its
+   * expression.
+   *
+   * @returns {Object}
+   * @private
+   */
+  getPlaceable() {
+    const start = ++this._index;
+
+    this.skipWS();
+
+    if (this._source[this._index] === "*" ||
+       (this._source[this._index] === "[" &&
+        this._source[this._index + 1] !== "]")) {
+      const variants = this.getVariants();
+
+      return {
+        type: "sel",
+        exp: null,
+        vars: variants[0],
+        def: variants[1]
+      };
+    }
+
+    // Rewind the index and only support in-line white-space now.
+    this._index = start;
+    this.skipInlineWS();
+
+    const selector = this.getSelectorExpression();
+
+    this.skipWS();
+
+    const ch = this._source[this._index];
+
+    if (ch === "}") {
+      if (selector.type === "attr" && selector.id.name.startsWith("-")) {
+        throw this.error(
+          "Attributes of private messages cannot be interpolated."
+        );
+      }
+
+      return selector;
+    }
+
+    if (ch !== "-" || this._source[this._index + 1] !== ">") {
+      throw this.error('Expected "}" or "->"');
+    }
+
+    if (selector.type === "ref") {
+      throw this.error("Message references cannot be used as selectors.");
+    }
+
+    if (selector.type === "var") {
+      throw this.error("Variants cannot be used as selectors.");
+    }
+
+    if (selector.type === "attr" && !selector.id.name.startsWith("-")) {
+      throw this.error(
+        "Attributes of public messages cannot be used as selectors."
+      );
+    }
+
+
+    this._index += 2; // ->
+
+    this.skipInlineWS();
+
+    if (this._source[this._index] !== "\n") {
+      throw this.error("Variants should be listed in a new line");
+    }
+
+    this.skipWS();
+
+    const variants = this.getVariants();
+
+    if (variants[0].length === 0) {
+      throw this.error("Expected members for the select expression");
+    }
+
+    return {
+      type: "sel",
+      exp: selector,
+      vars: variants[0],
+      def: variants[1]
+    };
+  }
+
+  /**
+   * Parses a selector expression.
+   *
+   * @returns {Object}
+   * @private
+   */
+  getSelectorExpression() {
+    const literal = this.getLiteral();
+
+    if (literal.type !== "ref") {
+      return literal;
+    }
+
+    if (this._source[this._index] === ".") {
+      this._index++;
+
+      const name = this.getIdentifier();
+      this._index++;
+      return {
+        type: "attr",
+        id: literal,
+        name
+      };
+    }
+
+    if (this._source[this._index] === "[") {
+      this._index++;
+
+      const key = this.getVariantKey();
+      this._index++;
+      return {
+        type: "var",
+        id: literal,
+        key
+      };
+    }
+
+    if (this._source[this._index] === "(") {
+      this._index++;
+      const args = this.getCallArgs();
+
+      if (!functionIdentifierRe.test(literal.name)) {
+        throw this.error("Function names must be all upper-case");
+      }
+
+      this._index++;
+
+      literal.type = "fun";
+
+      return {
+        type: "call",
+        fun: literal,
+        args
+      };
+    }
+
+    return literal;
+  }
+
+  /**
+   * Parses call arguments for a CallExpression.
+   *
+   * @returns {Array}
+   * @private
+   */
+  getCallArgs() {
+    const args = [];
+
+    while (this._index < this._length) {
+      this.skipInlineWS();
+
+      if (this._source[this._index] === ")") {
+        return args;
+      }
+
+      const exp = this.getSelectorExpression();
+
+      // MessageReference in this place may be an entity reference, like:
+      // `call(foo)`, or, if it's followed by `:` it will be a key-value pair.
+      if (exp.type !== "ref") {
+        args.push(exp);
+      } else {
+        this.skipInlineWS();
+
+        if (this._source[this._index] === ":") {
+          this._index++;
+          this.skipInlineWS();
+
+          const val = this.getSelectorExpression();
+
+          // If the expression returned as a value of the argument
+          // is not a quote delimited string or number, throw.
+          //
+          // We don't have to check here if the pattern is quote delimited
+          // because that's the only type of string allowed in expressions.
+          if (typeof val === "string" ||
+              Array.isArray(val) ||
+              val.type === "num") {
+            args.push({
+              type: "narg",
+              name: exp.name,
+              val
+            });
+          } else {
+            this._index = this._source.lastIndexOf(":", this._index) + 1;
+            throw this.error(
+              "Expected string in quotes, number.");
+          }
+
+        } else {
+          args.push(exp);
+        }
+      }
+
+      this.skipInlineWS();
+
+      if (this._source[this._index] === ")") {
+        break;
+      } else if (this._source[this._index] === ",") {
+        this._index++;
+      } else {
+        throw this.error('Expected "," or ")"');
+      }
+    }
+
+    return args;
+  }
+
+  /**
+   * Parses an FTL Number.
+   *
+   * @returns {Object}
+   * @private
+   */
+  getNumber() {
+    let num = "";
+    let cc = this._source.charCodeAt(this._index);
+
+    // The number literal may start with negative sign `-`.
+    if (cc === 45) {
+      num += "-";
+      cc = this._source.charCodeAt(++this._index);
+    }
+
+    // next, we expect at least one digit
+    if (cc < 48 || cc > 57) {
+      throw this.error(`Unknown literal "${num}"`);
+    }
+
+    // followed by potentially more digits
+    while (cc >= 48 && cc <= 57) {
+      num += this._source[this._index++];
+      cc = this._source.charCodeAt(this._index);
+    }
+
+    // followed by an optional decimal separator `.`
+    if (cc === 46) {
+      num += this._source[this._index++];
+      cc = this._source.charCodeAt(this._index);
+
+      // followed by at least one digit
+      if (cc < 48 || cc > 57) {
+        throw this.error(`Unknown literal "${num}"`);
+      }
+
+      // and optionally more digits
+      while (cc >= 48 && cc <= 57) {
+        num += this._source[this._index++];
+        cc = this._source.charCodeAt(this._index);
+      }
+    }
+
+    return {
+      type: "num",
+      val: num
+    };
+  }
+
+  /**
+   * Parses a list of Message attributes.
+   *
+   * @returns {Object}
+   * @private
+   */
+  getAttributes() {
+    const attrs = {};
+
+    while (this._index < this._length) {
+      if (this._source[this._index] !== " ") {
+        break;
+      }
+      this.skipInlineWS();
+
+      if (this._source[this._index] !== ".") {
+        break;
+      }
+      this._index++;
+
+      const key = this.getIdentifier();
+
+      this.skipInlineWS();
+
+      if (this._source[this._index] !== "=") {
+        throw this.error('Expected "="');
+      }
+      this._index++;
+
+      this.skipInlineWS();
+
+      const val = this.getPattern();
+
+      if (val === null) {
+        throw this.error("Expected attribute to have a value");
+      }
+
+      if (typeof val === "string") {
+        attrs[key] = val;
+      } else {
+        attrs[key] = {
+          val
+        };
+      }
+
+      this.skipBlankLines();
+    }
+
+    return attrs;
+  }
+
+  /**
+   * Parses a list of Selector variants.
+   *
+   * @returns {Array}
+   * @private
+   */
+  getVariants() {
+    const variants = [];
+    let index = 0;
+    let defaultIndex;
+
+    while (this._index < this._length) {
+      const ch = this._source[this._index];
+
+      if ((ch !== "[" || this._source[this._index + 1] === "[") &&
+          ch !== "*") {
+        break;
+      }
+      if (ch === "*") {
+        this._index++;
+        defaultIndex = index;
+      }
+
+      if (this._source[this._index] !== "[") {
+        throw this.error('Expected "["');
+      }
+
+      this._index++;
+
+      const key = this.getVariantKey();
+
+      this.skipInlineWS();
+
+      const val = this.getPattern();
+
+      if (val === null) {
+        throw this.error("Expected variant to have a value");
+      }
+
+      variants[index++] = {key, val};
+
+      this.skipWS();
+    }
+
+    return [variants, defaultIndex];
+  }
+
+  /**
+   * Parses a Variant key.
+   *
+   * @returns {String}
+   * @private
+   */
+  getVariantKey() {
+    // VariantKey may be a Keyword or Number
+
+    const cc = this._source.charCodeAt(this._index);
+    let literal;
+
+    if ((cc >= 48 && cc <= 57) || cc === 45) {
+      literal = this.getNumber();
+    } else {
+      literal = this.getVariantName();
+    }
+
+    if (this._source[this._index] !== "]") {
+      throw this.error('Expected "]"');
+    }
+
+    this._index++;
+    return literal;
+  }
+
+  /**
+   * Parses an FTL literal.
+   *
+   * @returns {Object}
+   * @private
+   */
+  getLiteral() {
+    const cc0 = this._source.charCodeAt(this._index);
+
+    if (cc0 === 36) { // $
+      this._index++;
+      return {
+        type: "ext",
+        name: this.getIdentifier()
+      };
+    }
+
+    const cc1 = cc0 === 45 // -
+      // Peek at the next character after the dash.
+      ? this._source.charCodeAt(this._index + 1)
+      // Or keep using the character at the current index.
+      : cc0;
+
+    if ((cc1 >= 97 && cc1 <= 122) || // a-z
+        (cc1 >= 65 && cc1 <= 90)) { // A-Z
+      return {
+        type: "ref",
+        name: this.getEntryIdentifier()
+      };
+    }
+
+    if ((cc1 >= 48 && cc1 <= 57)) { // 0-9
+      return this.getNumber();
+    }
+
+    if (cc0 === 34) { // "
+      return this.getString();
+    }
+
+    throw this.error("Expected literal");
+  }
+
+  /**
+   * Skips an FTL comment.
+   *
+   * @private
+   */
+  skipComment() {
+    // At runtime, we don't care about comments so we just have
+    // to parse them properly and skip their content.
+    let eol = this._source.indexOf("\n", this._index);
+
+    while (eol !== -1 &&
+      ((this._source[eol + 1] === "/" && this._source[eol + 2] === "/") ||
+       (this._source[eol + 1] === "#" &&
+         [" ", "#"].includes(this._source[eol + 2])))) {
+      this._index = eol + 3;
+
+      eol = this._source.indexOf("\n", this._index);
+
+      if (eol === -1) {
+        break;
+      }
+    }
+
+    if (eol === -1) {
+      this._index = this._length;
+    } else {
+      this._index = eol + 1;
+    }
+  }
+
+  /**
+   * Creates a new SyntaxError object with a given message.
+   *
+   * @param {String} message
+   * @returns {Object}
+   * @private
+   */
+  error(message) {
+    return new SyntaxError(message);
+  }
+
+  /**
+   * Skips to the beginning of a next entry after the current position.
+   * This is used to mark the boundary of junk entry in case of error,
+   * and recover from the returned position.
+   *
+   * @private
+   */
+  skipToNextEntryStart() {
+    let start = this._index;
+
+    while (true) {
+      if (start === 0 || this._source[start - 1] === "\n") {
+        const cc = this._source.charCodeAt(start);
+
+        if ((cc >= 97 && cc <= 122) || // a-z
+            (cc >= 65 && cc <= 90) || // A-Z
+             cc === 47 || cc === 91) { // /[
+          this._index = start;
+          return;
+        }
+      }
+
+      start = this._source.indexOf("\n", start);
+
+      if (start === -1) {
+        this._index = this._length;
+        return;
+      }
+      start++;
+    }
+  }
+}
+
+/**
+ * Parses an FTL string using RuntimeParser and returns the generated
+ * object with entries and a list of errors.
+ *
+ * @param {String} string
+ * @returns {Array<Object, Array>}
+ */
+function parse(string) {
+  const parser = new RuntimeParser();
+  return parser.getResource(string);
+}
+
+/* global Intl */
+
+/**
+ * The `FluentType` class is the base of Fluent's type system.
+ *
+ * Fluent types wrap JavaScript values and store additional configuration for
+ * them, which can then be used in the `toString` method together with a proper
+ * `Intl` formatter.
+ */
+class FluentType {
+
+  /**
+   * Create an `FluentType` instance.
+   *
+   * @param   {Any}    value - JavaScript value to wrap.
+   * @param   {Object} opts  - Configuration.
+   * @returns {FluentType}
+   */
+  constructor(value, opts) {
+    this.value = value;
+    this.opts = opts;
+  }
+
+  /**
+   * Unwrap the raw value stored by this `FluentType`.
+   *
+   * @returns {Any}
+   */
+  valueOf() {
+    return this.value;
+  }
+
+  /**
+   * Format this instance of `FluentType` to a string.
+   *
+   * Formatted values are suitable for use outside of the `MessageContext`.
+   * This method can use `Intl` formatters memoized by the `MessageContext`
+   * instance passed as an argument.
+   *
+   * @param   {MessageContext} [ctx]
+   * @returns {string}
+   */
+  toString() {
+    throw new Error("Subclasses of FluentType must implement toString.");
+  }
+}
+
+class FluentNone extends FluentType {
+  toString() {
+    return this.value || "???";
+  }
+}
+
+class FluentNumber extends FluentType {
+  constructor(value, opts) {
+    super(parseFloat(value), opts);
+  }
+
+  toString(ctx) {
+    try {
+      const nf = ctx._memoizeIntlObject(
+        Intl.NumberFormat, this.opts
+      );
+      return nf.format(this.value);
+    } catch (e) {
+      // XXX Report the error.
+      return this.value;
+    }
+  }
+
+  /**
+   * Compare the object with another instance of a FluentType.
+   *
+   * @param   {MessageContext} ctx
+   * @param   {FluentType}     other
+   * @returns {bool}
+   */
+  match(ctx, other) {
+    if (other instanceof FluentNumber) {
+      return this.value === other.value;
+    }
+    return false;
+  }
+}
+
+class FluentDateTime extends FluentType {
+  constructor(value, opts) {
+    super(new Date(value), opts);
+  }
+
+  toString(ctx) {
+    try {
+      const dtf = ctx._memoizeIntlObject(
+        Intl.DateTimeFormat, this.opts
+      );
+      return dtf.format(this.value);
+    } catch (e) {
+      // XXX Report the error.
+      return this.value;
+    }
+  }
+}
+
+class FluentSymbol extends FluentType {
+  toString() {
+    return this.value;
+  }
+
+  /**
+   * Compare the object with another instance of a FluentType.
+   *
+   * @param   {MessageContext} ctx
+   * @param   {FluentType}     other
+   * @returns {bool}
+   */
+  match(ctx, other) {
+    if (other instanceof FluentSymbol) {
+      return this.value === other.value;
+    } else if (typeof other === "string") {
+      return this.value === other;
+    } else if (other instanceof FluentNumber) {
+      const pr = ctx._memoizeIntlObject(
+        Intl.PluralRules, other.opts
+      );
+      return this.value === pr.select(other.value);
+    }
+    return false;
+  }
+}
+
+/**
+ * @overview
+ *
+ * The FTL resolver ships with a number of functions built-in.
+ *
+ * Each function take two arguments:
+ *   - args - an array of positional args
+ *   - opts - an object of key-value args
+ *
+ * Arguments to functions are guaranteed to already be instances of
+ * `FluentType`.  Functions must return `FluentType` objects as well.
+ */
+
+var builtins = {
+  "NUMBER": ([arg], opts) =>
+    new FluentNumber(arg.valueOf(), merge(arg.opts, opts)),
+  "DATETIME": ([arg], opts) =>
+    new FluentDateTime(arg.valueOf(), merge(arg.opts, opts)),
+};
+
+function merge(argopts, opts) {
+  return Object.assign({}, argopts, values(opts));
+}
+
+function values(opts) {
+  const unwrapped = {};
+  for (const [name, opt] of Object.entries(opts)) {
+    unwrapped[name] = opt.valueOf();
+  }
+  return unwrapped;
+}
+
+/**
+ * @overview
+ *
+ * The role of the Fluent resolver is to format a translation object to an
+ * instance of `FluentType` or an array of instances.
+ *
+ * Translations can contain references to other messages or external arguments,
+ * conditional logic in form of select expressions, traits which describe their
+ * grammatical features, and can use Fluent builtins which make use of the
+ * `Intl` formatters to format numbers, dates, lists and more into the
+ * context's language.  See the documentation of the Fluent syntax for more
+ * information.
+ *
+ * In case of errors the resolver will try to salvage as much of the
+ * translation as possible.  In rare situations where the resolver didn't know
+ * how to recover from an error it will return an instance of `FluentNone`.
+ *
+ * `MessageReference`, `VariantExpression`, `AttributeExpression` and
+ * `SelectExpression` resolve to raw Runtime Entries objects and the result of
+ * the resolution needs to be passed into `Type` to get their real value.
+ * This is useful for composing expressions.  Consider:
+ *
+ *     brand-name[nominative]
+ *
+ * which is a `VariantExpression` with properties `id: MessageReference` and
+ * `key: Keyword`.  If `MessageReference` was resolved eagerly, it would
+ * instantly resolve to the value of the `brand-name` message.  Instead, we
+ * want to get the message object and look for its `nominative` variant.
+ *
+ * All other expressions (except for `FunctionReference` which is only used in
+ * `CallExpression`) resolve to an instance of `FluentType`.  The caller should
+ * use the `toString` method to convert the instance to a native value.
+ *
+ *
+ * All functions in this file pass around a special object called `env`.
+ * This object stores a set of elements used by all resolve functions:
+ *
+ *  * {MessageContext} ctx
+ *      context for which the given resolution is happening
+ *  * {Object} args
+ *      list of developer provided arguments that can be used
+ *  * {Array} errors
+ *      list of errors collected while resolving
+ *  * {WeakSet} dirty
+ *      Set of patterns already encountered during this resolution.
+ *      This is used to prevent cyclic resolutions.
+ */
+
+// Prevent expansion of too long placeables.
+const MAX_PLACEABLE_LENGTH = 2500;
+
+// Unicode bidi isolation characters.
+const FSI = "\u2068";
+const PDI = "\u2069";
+
+
+/**
+ * Helper for choosing the default value from a set of members.
+ *
+ * Used in SelectExpressions and Type.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} members
+ *    Hash map of variants from which the default value is to be selected.
+ * @param   {Number} def
+ *    The index of the default variant.
+ * @returns {FluentType}
+ * @private
+ */
+function DefaultMember(env, members, def) {
+  if (members[def]) {
+    return members[def];
+  }
+
+  const { errors } = env;
+  errors.push(new RangeError("No default"));
+  return new FluentNone();
+}
+
+
+/**
+ * Resolve a reference to another message.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} id
+ *    The identifier of the message to be resolved.
+ * @param   {String} id.name
+ *    The name of the identifier.
+ * @returns {FluentType}
+ * @private
+ */
+function MessageReference(env, {name}) {
+  const { ctx, errors } = env;
+  const message = name.startsWith("-")
+    ? ctx._terms.get(name)
+    : ctx._messages.get(name);
+
+  if (!message) {
+    const err = name.startsWith("-")
+      ? new ReferenceError(`Unknown term: ${name}`)
+      : new ReferenceError(`Unknown message: ${name}`);
+    errors.push(err);
+    return new FluentNone(name);
+  }
+
+  return message;
+}
+
+/**
+ * Resolve a variant expression to the variant object.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression to be resolved.
+ * @param   {Object} expr.id
+ *    An Identifier of a message for which the variant is resolved.
+ * @param   {Object} expr.id.name
+ *    Name a message for which the variant is resolved.
+ * @param   {Object} expr.key
+ *    Variant key to be resolved.
+ * @returns {FluentType}
+ * @private
+ */
+function VariantExpression(env, {id, key}) {
+  const message = MessageReference(env, id);
+  if (message instanceof FluentNone) {
+    return message;
+  }
+
+  const { ctx, errors } = env;
+  const keyword = Type(env, key);
+
+  function isVariantList(node) {
+    return Array.isArray(node) &&
+      node[0].type === "sel" &&
+      node[0].exp === null;
+  }
+
+  if (isVariantList(message.val)) {
+    // Match the specified key against keys of each variant, in order.
+    for (const variant of message.val[0].vars) {
+      const variantKey = Type(env, variant.key);
+      if (keyword.match(ctx, variantKey)) {
+        return variant;
+      }
+    }
+  }
+
+  errors.push(new ReferenceError(`Unknown variant: ${keyword.toString(ctx)}`));
+  return Type(env, message);
+}
+
+
+/**
+ * Resolve an attribute expression to the attribute object.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression to be resolved.
+ * @param   {String} expr.id
+ *    An ID of a message for which the attribute is resolved.
+ * @param   {String} expr.name
+ *    Name of the attribute to be resolved.
+ * @returns {FluentType}
+ * @private
+ */
+function AttributeExpression(env, {id, name}) {
+  const message = MessageReference(env, id);
+  if (message instanceof FluentNone) {
+    return message;
+  }
+
+  if (message.attrs) {
+    // Match the specified name against keys of each attribute.
+    for (const attrName in message.attrs) {
+      if (name === attrName) {
+        return message.attrs[name];
+      }
+    }
+  }
+
+  const { errors } = env;
+  errors.push(new ReferenceError(`Unknown attribute: ${name}`));
+  return Type(env, message);
+}
+
+/**
+ * Resolve a select expression to the member object.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression to be resolved.
+ * @param   {String} expr.exp
+ *    Selector expression
+ * @param   {Array} expr.vars
+ *    List of variants for the select expression.
+ * @param   {Number} expr.def
+ *    Index of the default variant.
+ * @returns {FluentType}
+ * @private
+ */
+function SelectExpression(env, {exp, vars, def}) {
+  if (exp === null) {
+    return DefaultMember(env, vars, def);
+  }
+
+  const selector = Type(env, exp);
+  if (selector instanceof FluentNone) {
+    return DefaultMember(env, vars, def);
+  }
+
+  // Match the selector against keys of each variant, in order.
+  for (const variant of vars) {
+    const key = Type(env, variant.key);
+    const keyCanMatch =
+      key instanceof FluentNumber || key instanceof FluentSymbol;
+
+    if (!keyCanMatch) {
+      continue;
+    }
+
+    const { ctx } = env;
+
+    if (key.match(ctx, selector)) {
+      return variant;
+    }
+  }
+
+  return DefaultMember(env, vars, def);
+}
+
+
+/**
+ * Resolve expression to a Fluent type.
+ *
+ * JavaScript strings are a special case.  Since they natively have the
+ * `toString` method they can be used as if they were a Fluent type without
+ * paying the cost of creating a instance of one.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression object to be resolved into a Fluent type.
+ * @returns {FluentType}
+ * @private
+ */
+function Type(env, expr) {
+  // A fast-path for strings which are the most common case, and for
+  // `FluentNone` which doesn't require any additional logic.
+  if (typeof expr === "string" || expr instanceof FluentNone) {
+    return expr;
+  }
+
+  // The Runtime AST (Entries) encodes patterns (complex strings with
+  // placeables) as Arrays.
+  if (Array.isArray(expr)) {
+    return Pattern(env, expr);
+  }
+
+
+  switch (expr.type) {
+    case "varname":
+      return new FluentSymbol(expr.name);
+    case "num":
+      return new FluentNumber(expr.val);
+    case "ext":
+      return ExternalArgument(env, expr);
+    case "fun":
+      return FunctionReference(env, expr);
+    case "call":
+      return CallExpression(env, expr);
+    case "ref": {
+      const message = MessageReference(env, expr);
+      return Type(env, message);
+    }
+    case "attr": {
+      const attr = AttributeExpression(env, expr);
+      return Type(env, attr);
+    }
+    case "var": {
+      const variant = VariantExpression(env, expr);
+      return Type(env, variant);
+    }
+    case "sel": {
+      const member = SelectExpression(env, expr);
+      return Type(env, member);
+    }
+    case undefined: {
+      // If it's a node with a value, resolve the value.
+      if (expr.val !== null && expr.val !== undefined) {
+        return Type(env, expr.val);
+      }
+
+      const { errors } = env;
+      errors.push(new RangeError("No value"));
+      return new FluentNone();
+    }
+    default:
+      return new FluentNone();
+  }
+}
+
+/**
+ * Resolve a reference to an external argument.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression to be resolved.
+ * @param   {String} expr.name
+ *    Name of an argument to be returned.
+ * @returns {FluentType}
+ * @private
+ */
+function ExternalArgument(env, {name}) {
+  const { args, errors } = env;
+
+  if (!args || !args.hasOwnProperty(name)) {
+    errors.push(new ReferenceError(`Unknown external: ${name}`));
+    return new FluentNone(name);
+  }
+
+  const arg = args[name];
+
+  // Return early if the argument already is an instance of FluentType.
+  if (arg instanceof FluentType) {
+    return arg;
+  }
+
+  // Convert the argument to a Fluent type.
+  switch (typeof arg) {
+    case "string":
+      return arg;
+    case "number":
+      return new FluentNumber(arg);
+    case "object":
+      if (arg instanceof Date) {
+        return new FluentDateTime(arg);
+      }
+    default:
+      errors.push(
+        new TypeError(`Unsupported external type: ${name}, ${typeof arg}`)
+      );
+      return new FluentNone(name);
+  }
+}
+
+/**
+ * Resolve a reference to a function.
+ *
+ * @param   {Object}  env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression to be resolved.
+ * @param   {String} expr.name
+ *    Name of the function to be returned.
+ * @returns {Function}
+ * @private
+ */
+function FunctionReference(env, {name}) {
+  // Some functions are built-in.  Others may be provided by the runtime via
+  // the `MessageContext` constructor.
+  const { ctx: { _functions }, errors } = env;
+  const func = _functions[name] || builtins[name];
+
+  if (!func) {
+    errors.push(new ReferenceError(`Unknown function: ${name}()`));
+    return new FluentNone(`${name}()`);
+  }
+
+  if (typeof func !== "function") {
+    errors.push(new TypeError(`Function ${name}() is not callable`));
+    return new FluentNone(`${name}()`);
+  }
+
+  return func;
+}
+
+/**
+ * Resolve a call to a Function with positional and key-value arguments.
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Object} expr
+ *    An expression to be resolved.
+ * @param   {Object} expr.fun
+ *    FTL Function object.
+ * @param   {Array} expr.args
+ *    FTL Function argument list.
+ * @returns {FluentType}
+ * @private
+ */
+function CallExpression(env, {fun, args}) {
+  const callee = FunctionReference(env, fun);
+
+  if (callee instanceof FluentNone) {
+    return callee;
+  }
+
+  const posargs = [];
+  const keyargs = {};
+
+  for (const arg of args) {
+    if (arg.type === "narg") {
+      keyargs[arg.name] = Type(env, arg.val);
+    } else {
+      posargs.push(Type(env, arg));
+    }
+  }
+
+  try {
+    return callee(posargs, keyargs);
+  } catch (e) {
+    // XXX Report errors.
+    return new FluentNone();
+  }
+}
+
+/**
+ * Resolve a pattern (a complex string with placeables).
+ *
+ * @param   {Object} env
+ *    Resolver environment object.
+ * @param   {Array} ptn
+ *    Array of pattern elements.
+ * @returns {Array}
+ * @private
+ */
+function Pattern(env, ptn) {
+  const { ctx, dirty, errors } = env;
+
+  if (dirty.has(ptn)) {
+    errors.push(new RangeError("Cyclic reference"));
+    return new FluentNone();
+  }
+
+  // Tag the pattern as dirty for the purpose of the current resolution.
+  dirty.add(ptn);
+  const result = [];
+
+  // Wrap interpolations with Directional Isolate Formatting characters
+  // only when the pattern has more than one element.
+  const useIsolating = ctx._useIsolating && ptn.length > 1;
+
+  for (const elem of ptn) {
+    if (typeof elem === "string") {
+      result.push(elem);
+      continue;
+    }
+
+    const part = Type(env, elem).toString(ctx);
+
+    if (useIsolating) {
+      result.push(FSI);
+    }
+
+    if (part.length > MAX_PLACEABLE_LENGTH) {
+      errors.push(
+        new RangeError(
+          "Too many characters in placeable " +
+          `(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
+        )
+      );
+      result.push(part.slice(MAX_PLACEABLE_LENGTH));
+    } else {
+      result.push(part);
+    }
+
+    if (useIsolating) {
+      result.push(PDI);
+    }
+  }
+
+  dirty.delete(ptn);
+  return result.join("");
+}
+
+/**
+ * Format a translation into a string.
+ *
+ * @param   {MessageContext} ctx
+ *    A MessageContext instance which will be used to resolve the
+ *    contextual information of the message.
+ * @param   {Object}         args
+ *    List of arguments provided by the developer which can be accessed
+ *    from the message.
+ * @param   {Object}         message
+ *    An object with the Message to be resolved.
+ * @param   {Array}          errors
+ *    An error array that any encountered errors will be appended to.
+ * @returns {FluentType}
+ */
+function resolve(ctx, args, message, errors = []) {
+  const env = {
+    ctx, args, errors, dirty: new WeakSet()
+  };
+  return Type(env, message).toString(ctx);
+}
+
+/**
+ * Message contexts are single-language stores of translations.  They are
+ * responsible for parsing translation resources in the Fluent syntax and can
+ * format translation units (entities) to strings.
+ *
+ * Always use `MessageContext.format` to retrieve translation units from
+ * a context.  Translations can contain references to other entities or
+ * external arguments, conditional logic in form of select expressions, traits
+ * which describe their grammatical features, and can use Fluent builtins which
+ * make use of the `Intl` formatters to format numbers, dates, lists and more
+ * into the context's language.  See the documentation of the Fluent syntax for
+ * more information.
+ */
+class MessageContext {
+
+  /**
+   * Create an instance of `MessageContext`.
+   *
+   * The `locales` argument is used to instantiate `Intl` formatters used by
+   * translations.  The `options` object can be used to configure the context.
+   *
+   * Examples:
+   *
+   *     const ctx = new MessageContext(locales);
+   *
+   *     const ctx = new MessageContext(locales, { useIsolating: false });
+   *
+   *     const ctx = new MessageContext(locales, {
+   *       useIsolating: true,
+   *       functions: {
+   *         NODE_ENV: () => process.env.NODE_ENV
+   *       }
+   *     });
+   *
+   * Available options:
+   *
+   *   - `functions` - an object of additional functions available to
+   *                   translations as builtins.
+   *
+   *   - `useIsolating` - boolean specifying whether to use Unicode isolation
+   *                    marks (FSI, PDI) for bidi interpolations.
+   *
+   * @param   {string|Array<string>} locales - Locale or locales of the context
+   * @param   {Object} [options]
+   * @returns {MessageContext}
+   */
+  constructor(locales, { functions = {}, useIsolating = true } = {}) {
+    this.locales = Array.isArray(locales) ? locales : [locales];
+
+    this._terms = new Map();
+    this._messages = new Map();
+    this._functions = functions;
+    this._useIsolating = useIsolating;
+    this._intls = new WeakMap();
+  }
+
+  /*
+   * Return an iterator over public `[id, message]` pairs.
+   *
+   * @returns {Iterator}
+   */
+  get messages() {
+    return this._messages[Symbol.iterator]();
+  }
+
+  /*
+   * Check if a message is present in the context.
+   *
+   * @param {string} id - The identifier of the message to check.
+   * @returns {bool}
+   */
+  hasMessage(id) {
+    return this._messages.has(id);
+  }
+
+  /*
+   * Return the internal representation of a message.
+   *
+   * The internal representation should only be used as an argument to
+   * `MessageContext.format`.
+   *
+   * @param {string} id - The identifier of the message to check.
+   * @returns {Any}
+   */
+  getMessage(id) {
+    return this._messages.get(id);
+  }
+
+  /**
+   * Add a translation resource to the context.
+   *
+   * The translation resource must use the Fluent syntax.  It will be parsed by
+   * the context and each translation unit (message) will be available in the
+   * context by its identifier.
+   *
+   *     ctx.addMessages('foo = Foo');
+   *     ctx.getMessage('foo');
+   *
+   *     // Returns a raw representation of the 'foo' message.
+   *
+   * Parsed entities should be formatted with the `format` method in case they
+   * contain logic (references, select expressions etc.).
+   *
+   * @param   {string} source - Text resource with translations.
+   * @returns {Array<Error>}
+   */
+  addMessages(source) {
+    const [entries, errors] = parse(source);
+    for (const id in entries) {
+      if (id.startsWith("-")) {
+        // Identifiers starting with a dash (-) define terms. Terms are private
+        // and cannot be retrieved from MessageContext.
+        if (this._terms.has(id)) {
+          errors.push(`Attempt to override an existing term: "${id}"`);
+          continue;
+        }
+        this._terms.set(id, entries[id]);
+      } else {
+        if (this._messages.has(id)) {
+          errors.push(`Attempt to override an existing message: "${id}"`);
+          continue;
+        }
+        this._messages.set(id, entries[id]);
+      }
+    }
+
+    return errors;
+  }
+
+  /**
+   * Format a message to a string or null.
+   *
+   * Format a raw `message` from the context into a string (or a null if it has
+   * a null value).  `args` will be used to resolve references to external
+   * arguments inside of the translation.
+   *
+   * In case of errors `format` will try to salvage as much of the translation
+   * as possible and will still return a string.  For performance reasons, the
+   * encountered errors are not returned but instead are appended to the
+   * `errors` array passed as the third argument.
+   *
+   *     const errors = [];
+   *     ctx.addMessages('hello = Hello, { $name }!');
+   *     const hello = ctx.getMessage('hello');
+   *     ctx.format(hello, { name: 'Jane' }, errors);
+   *
+   *     // Returns 'Hello, Jane!' and `errors` is empty.
+   *
+   *     ctx.format(hello, undefined, errors);
+   *
+   *     // Returns 'Hello, name!' and `errors` is now:
+   *
+   *     [<ReferenceError: Unknown external: name>]
+   *
+   * @param   {Object | string}    message
+   * @param   {Object | undefined} args
+   * @param   {Array}              errors
+   * @returns {?string}
+   */
+  format(message, args, errors) {
+    // optimize entities which are simple strings with no attributes
+    if (typeof message === "string") {
+      return message;
+    }
+
+    // optimize simple-string entities with attributes
+    if (typeof message.val === "string") {
+      return message.val;
+    }
+
+    // optimize entities with null values
+    if (message.val === undefined) {
+      return null;
+    }
+
+    return resolve(this, args, message, errors);
+  }
+
+  _memoizeIntlObject(ctor, opts) {
+    const cache = this._intls.get(ctor) || {};
+    const id = JSON.stringify(opts);
+
+    if (!cache[id]) {
+      cache[id] = new ctor(this.locales, opts);
+      this._intls.set(ctor, cache);
+    }
+
+    return cache[id];
+  }
+}
+
+/*
+ * CachedIterable caches the elements yielded by an iterable.
+ *
+ * It can be used to iterate over an iterable many times without depleting the
+ * iterable.
+ */
+class CachedIterable {
+  /**
+   * Create an `CachedIterable` instance.
+   *
+   * @param {Iterable} iterable
+   * @returns {CachedIterable}
+   */
+  constructor(iterable) {
+    if (Symbol.asyncIterator in Object(iterable)) {
+      this.iterator = iterable[Symbol.asyncIterator]();
+    } else if (Symbol.iterator in Object(iterable)) {
+      this.iterator = iterable[Symbol.iterator]();
+    } else {
+      throw new TypeError("Argument must implement the iteration protocol.");
+    }
+
+    this.seen = [];
+  }
+
+  [Symbol.iterator]() {
+    const { seen, iterator } = this;
+    let cur = 0;
+
+    return {
+      next() {
+        if (seen.length <= cur) {
+          seen.push(iterator.next());
+        }
+        return seen[cur++];
+      }
+    };
+  }
+
+  [Symbol.asyncIterator]() {
+    const { seen, iterator } = this;
+    let cur = 0;
+
+    return {
+      async next() {
+        if (seen.length <= cur) {
+          seen.push(await iterator.next());
+        }
+        return seen[cur++];
+      }
+    };
+  }
+
+  /**
+   * This method allows user to consume the next element from the iterator
+   * into the cache.
+   */
+  touchNext() {
+    const { seen, iterator } = this;
+    if (seen.length === 0 || seen[seen.length - 1].done === false) {
+      seen.push(iterator.next());
+    }
+  }
+}
+
+/*
+ * @overview
+ *
+ * Functions for managing ordered sequences of MessageContexts.
+ *
+ * An ordered iterable of MessageContext instances can represent the current
+ * negotiated fallback chain of languages.  This iterable can be used to find
+ * the best existing translation for a given identifier.
+ *
+ * The mapContext* methods can be used to find the first MessageContext in the
+ * given iterable which contains the translation with the given identifier.  If
+ * the iterable is ordered according to the result of a language negotiation
+ * the returned MessageContext contains the best available translation.
+ *
+ * A simple function which formats translations based on the identifier might
+ * be implemented as follows:
+ *
+ *     formatString(id, args) {
+ *         const ctx = mapContextSync(contexts, id);
+ *
+ *         if (ctx === null) {
+ *             return id;
+ *         }
+ *
+ *         const msg = ctx.getMessage(id);
+ *         return ctx.format(msg, args);
+ *     }
+ *
+ * In order to pass an iterator to mapContext*, wrap it in CachedIterable.
+ * This allows multiple calls to mapContext* without advancing and eventually
+ * depleting the iterator.
+ *
+ *     function *generateMessages() {
+ *         // Some lazy logic for yielding MessageContexts.
+ *         yield *[ctx1, ctx2];
+ *     }
+ *
+ *     const contexts = new CachedIterable(generateMessages());
+ *     const ctx = mapContextSync(contexts, id);
+ *
+ */
+
+/*
+ * Synchronously map an identifier or an array of identifiers to the best
+ * `MessageContext` instance(s).
+ *
+ * @param {Iterable} iterable
+ * @param {string|Array<string>} ids
+ * @returns {MessageContext|Array<MessageContext>}
+ */
+function mapContextSync(iterable, ids) {
+  if (!Array.isArray(ids)) {
+    return getContextForId(iterable, ids);
+  }
+
+  return ids.map(
+    id => getContextForId(iterable, id)
+  );
+}
+
+/*
+ * Find the best `MessageContext` with the translation for `id`.
+ */
+function getContextForId(iterable, id) {
+  for (const context of iterable) {
+    if (context.hasMessage(id)) {
+      return context;
+    }
+  }
+
+  return null;
+}
+
+/*
+ * Asynchronously map an identifier or an array of identifiers to the best
+ * `MessageContext` instance(s).
+ *
+ * @param {AsyncIterable} iterable
+ * @param {string|Array<string>} ids
+ * @returns {Promise<MessageContext|Array<MessageContext>>}
+ */
+async function mapContextAsync(iterable, ids) {
+  if (!Array.isArray(ids)) {
+    for await (const context of iterable) {
+      if (context.hasMessage(ids)) {
+        return context;
+      }
+    }
+  }
+
+  let remainingCount = ids.length;
+  const foundContexts = new Array(remainingCount).fill(null);
+
+  for await (const context of iterable) {
+    // XXX Switch to const [index, id] of id.entries() when we move to Babel 7.
+    // See https://github.com/babel/babel/issues/5880.
+    for (let index = 0; index < ids.length; index++) {
+      const id = ids[index];
+      if (!foundContexts[index] && context.hasMessage(id)) {
+        foundContexts[index] = context;
+        remainingCount--;
+      }
+
+      // Return early when all ids have been mapped to contexts.
+      if (remainingCount === 0) {
+        return foundContexts;
+      }
+    }
+  }
+
+  return foundContexts;
+}
+
+function nonBlank(line) {
+  return !/^\s*$/.test(line);
+}
+
+function countIndent(line) {
+  const [indent] = line.match(/^\s*/);
+  return indent.length;
+}
+
+/**
+ * Template literal tag for dedenting FTL code.
+ *
+ * Strip the common indent of non-blank lines. Remove blank lines.
+ *
+ * @param {Array<string>} strings
+ */
+function ftl(strings) {
+  const [code] = strings;
+  const lines = code.split("\n").filter(nonBlank);
+  const indents = lines.map(countIndent);
+  const common = Math.min(...indents);
+  const indent = new RegExp(`^\\s{${common}}`);
+
+  return lines.map(
+    line => line.replace(indent, "")
+  ).join("\n");
+}
+
+/*
+ * @module fluent
+ * @overview
+ *
+ * `fluent` is a JavaScript implementation of Project Fluent, a localization
+ * framework designed to unleash the expressive power of the natural language.
+ *
+ */
+
+exports._parse = parse;
+exports.MessageContext = MessageContext;
+exports.MessageArgument = FluentType;
+exports.MessageNumberArgument = FluentNumber;
+exports.MessageDateTimeArgument = FluentDateTime;
+exports.CachedIterable = CachedIterable;
+exports.mapContextSync = mapContextSync;
+exports.mapContextAsync = mapContextAsync;
+exports.ftl = ftl;
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
--- a/devtools/client/shared/vendor/moz.build
+++ b/devtools/client/shared/vendor/moz.build
@@ -4,16 +4,18 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'stringvalidator',
 ]
 
 DevToolsModules(
+    'fluent-react.js',
+    'fluent.js',
     'immutable.js',
     'jsol.js',
     'jszip.js',
     'lodash.js',
     'react-dom-factories.js',
     'react-dom-server.js',
     'react-dom-test-utils.js',
     'react-dom.js',