Bug 1426054 - Update Fluent in Gecko to 0.6. r?pike draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Fri, 26 Jan 2018 14:01:34 -0800
changeset 749719 a6dbcd5b2bd89fb6d4dac88109447c663a7b3a6d
parent 748205 a6c753e77345e968954e8d61e3871c5883015ede
push id97479
push userbmo:gandalf@aviary.pl
push dateWed, 31 Jan 2018 21:59:28 +0000
reviewerspike
bugs1426054
milestone60.0a1
Bug 1426054 - Update Fluent in Gecko to 0.6. r?pike This upstream uses fluent.js revision ae1b55a. MozReview-Commit-ID: 1IynCPWWN14
intl/l10n/DOMLocalization.jsm
intl/l10n/Localization.jsm
intl/l10n/MessageContext.jsm
intl/l10n/README
intl/l10n/fluent.js.patch
intl/l10n/test/test_messagecontext.js
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -11,44 +11,44 @@
  * 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.
  */
 
 
-/* fluent@0.4.1 */
+/* fluent@0.6.0 */
 
 const { Localization } =
   Components.utils.import("resource://gre/modules/Localization.jsm", {});
 
 // Match the opening angle bracket (<) in HTML tags, and HTML entities like
 // &amp;, &#0038;, &#x0026;.
 const reOverlay = /<|&#?\w+;/;
 
 /**
  * The list of elements that are allowed to be inserted into a localization.
  *
  * Source: https://www.w3.org/TR/html5/text-level-semantics.html
  */
-const ALLOWED_ELEMENTS = {
+const LOCALIZABLE_ELEMENTS = {
   'http://www.w3.org/1999/xhtml': [
     'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data',
     'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u',
     'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr'
   ],
 };
 
-const ALLOWED_ATTRIBUTES = {
+const LOCALIZABLE_ATTRIBUTES = {
   'http://www.w3.org/1999/xhtml': {
     global: ['title', 'aria-label', 'aria-valuetext', 'aria-moz-hint'],
     a: ['download'],
     area: ['download', 'alt'],
-    // value is special-cased in isAttrNameAllowed
+    // value is special-cased in isAttrNameLocalizable
     input: ['alt', 'placeholder'],
     menuitem: ['label'],
     menu: ['label'],
     optgroup: ['label'],
     option: ['label'],
     track: ['label'],
     img: ['alt'],
     textarea: ['placeholder'],
@@ -87,28 +87,34 @@ function overlayElement(targetElement, t
       templateElement.innerHTML = value;
       targetElement.appendChild(
         // The targetElement will be cleared at the end of sanitization.
         sanitizeUsing(templateElement.content, targetElement)
       );
     }
   }
 
-  if (translation.attrs === null) {
-    return;
-  }
-
   const explicitlyAllowed = targetElement.hasAttribute('data-l10n-attrs')
     ? targetElement.getAttribute('data-l10n-attrs')
       .split(',').map(i => i.trim())
     : null;
 
-  for (const [name, val] of translation.attrs) {
-    if (isAttrNameAllowed(name, targetElement, explicitlyAllowed)) {
-      targetElement.setAttribute(name, val);
+  // Remove localizable attributes which may have been set by a previous
+  // translation.
+  for (const attr of Array.from(targetElement.attributes)) {
+    if (isAttrNameLocalizable(attr.name, targetElement, explicitlyAllowed)) {
+      targetElement.removeAttribute(attr.name);
+    }
+  }
+
+  if (translation.attrs) {
+    for (const [name, val] of translation.attrs) {
+      if (isAttrNameLocalizable(name, targetElement, explicitlyAllowed)) {
+        targetElement.setAttribute(name, val);
+      }
     }
   }
 }
 
 /**
  * Sanitize `translationFragment` using `sourceElement` to add functional
  * HTML attributes to children.  `sourceElement` will have all its child nodes
  * removed.
@@ -126,79 +132,105 @@ function overlayElement(targetElement, t
  * The overlay logic is subject to the following limitations:
  *
  *   - Children are always cloned.  Event handlers attached to them are lost.
  *   - Nested HTML in source and in translations is not supported.
  *   - Multiple children of the same type will be matched in order.
  *
  * @param {DocumentFragment} translationFragment
  * @param {Element} sourceElement
+ * @returns {DocumentFragment}
  * @private
  */
 function sanitizeUsing(translationFragment, sourceElement) {
+  const ownerDocument = translationFragment.ownerDocument;
   // Take one node from translationFragment at a time and check it against
   // the allowed list or try to match it with a corresponding element
   // in the source.
   for (const childNode of translationFragment.childNodes) {
 
     if (childNode.nodeType === childNode.TEXT_NODE) {
       continue;
     }
 
     // If the child is forbidden just take its textContent.
-    if (!isElementAllowed(childNode)) {
-      const text = translationFragment.ownerDocument.createTextNode(
-        childNode.textContent
-      );
+    if (!isElementLocalizable(childNode)) {
+      const text = ownerDocument.createTextNode(childNode.textContent);
       translationFragment.replaceChild(text, childNode);
       continue;
     }
 
-
-    // If a child of the same type exists in sourceElement, use it as the base
-    // for the resultChild.  This also removes the child from sourceElement.
-    const sourceChild = shiftNamedElement(sourceElement, childNode.localName);
-
-    const mergedChild = sourceChild
-      // Shallow-clone the sourceChild to remove all childNodes.
-      ? sourceChild.cloneNode(false)
-      // Create a fresh element as a way to remove all forbidden attributes.
-      : childNode.ownerDocument.createElement(childNode.localName);
+    // Start the sanitization with an empty element.
+    const mergedChild = ownerDocument.createElement(childNode.localName);
 
     // Explicitly discard nested HTML by serializing childNode to a TextNode.
     mergedChild.textContent = childNode.textContent;
 
-    for (const attr of Array.from(childNode.attributes)) {
-      if (isAttrNameAllowed(attr.name, childNode)) {
-        mergedChild.setAttribute(attr.name, attr.value);
-      }
+    // If a child of the same type exists in sourceElement, take its functional
+    // (i.e. non-localizable) attributes. This also removes the child from
+    // sourceElement.
+    const sourceChild = shiftNamedElement(sourceElement, childNode.localName);
+
+    // Find the union of all safe attributes: localizable attributes from
+    // childNode and functional attributes from sourceChild.
+    const safeAttributes = sanitizeAttrsUsing(childNode, sourceChild);
+
+    for (const attr of safeAttributes) {
+      mergedChild.setAttribute(attr.name, attr.value);
     }
 
     translationFragment.replaceChild(mergedChild, childNode);
   }
 
   // SourceElement might have been already modified by shiftNamedElement.
   // Let's clear it to make sure other code doesn't rely on random leftovers.
   sourceElement.textContent = '';
 
   return translationFragment;
 }
 
 /**
+ * Sanitize and merge attributes.
+ *
+ * Only localizable attributes from the translated child element and only
+ * functional attributes from the source child element are considered safe.
+ *
+ * @param {Element} translatedElement
+ * @param {Element} sourceElement
+ * @returns {Array<Attr>}
+ * @private
+ */
+function sanitizeAttrsUsing(translatedElement, sourceElement) {
+  const localizedAttrs = Array.from(translatedElement.attributes).filter(
+    attr => isAttrNameLocalizable(attr.name, translatedElement)
+  );
+
+  if (!sourceElement) {
+    return localizedAttrs;
+  }
+
+  const functionalAttrs = Array.from(sourceElement.attributes).filter(
+    attr => !isAttrNameLocalizable(attr.name, sourceElement)
+  );
+
+  return localizedAttrs.concat(functionalAttrs);
+}
+
+/**
  * Check if element is allowed in the translation.
  *
  * This method is used by the sanitizer when the translation markup contains
  * an element which is not present in the source code.
  *
  * @param   {Element} element
  * @returns {boolean}
  * @private
  */
-function isElementAllowed(element) {
-  const allowed = ALLOWED_ELEMENTS[element.namespaceURI];
+function isElementLocalizable(element) {
+  const allowed = LOCALIZABLE_ELEMENTS[element.namespaceURI];
   return allowed && allowed.includes(element.localName);
 }
 
 /**
  * Check if attribute is allowed for the given element.
  *
  * This method is used by the sanitizer when the translation markup contains
  * DOM attributes, or when the translation has traits which map to DOM
@@ -208,22 +240,22 @@ function isElementAllowed(element) {
  * allowed on this element.
  *
  * @param   {string}         name
  * @param   {Element}        element
  * @param   {Array}          explicitlyAllowed
  * @returns {boolean}
  * @private
  */
-function isAttrNameAllowed(name, element, explicitlyAllowed = null) {
+function isAttrNameLocalizable(name, element, explicitlyAllowed = null) {
   if (explicitlyAllowed && explicitlyAllowed.includes(name)) {
     return true;
   }
 
-  const allowed = ALLOWED_ATTRIBUTES[element.namespaceURI];
+  const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI];
   if (!allowed) {
     return false;
   }
 
   const attrName = name.toLowerCase();
   const elemName = element.localName;
 
   // Is it a globally safe attribute?
@@ -470,41 +502,42 @@ class DOMLocalization extends Localizati
       switch (mutation.type) {
         case 'attributes':
           this.pendingElements.add(mutation.target);
           break;
         case 'childList':
           for (const addedNode of mutation.addedNodes) {
             if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
               if (addedNode.childElementCount) {
-                for (let element of this.getTranslatables(addedNode)) {
+                for (const element of this.getTranslatables(addedNode)) {
                   this.pendingElements.add(element);
                 }
               } else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) {
                 this.pendingElements.add(addedNode);
               }
             }
           }
           break;
       }
     }
 
-    // This fragment allows us to coalesce all pending translations into a single
-    // requestAnimationFrame.
+    // This fragment allows us to coalesce all pending translations
+    // into a single requestAnimationFrame.
     if (this.pendingElements.size > 0) {
       if (this.pendingrAF === null) {
         this.pendingrAF = this.windowElement.requestAnimationFrame(() => {
           this.translateElements(Array.from(this.pendingElements));
           this.pendingElements.clear();
           this.pendingrAF = null;
         });
       }
     }
   }
 
+
   /**
    * Translate a DOM element or fragment asynchronously using this
    * `DOMLocalization` object.
    *
    * Manually trigger the translation (or re-translation) of a DOM fragment.
    * Use the `data-l10n-id` and `data-l10n-args` attributes to mark up the DOM
    * with information about which translations to use.
    *
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -10,45 +10,69 @@
  *
  * 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.
  */
 
-/* fluent@0.4.1 */
+
+/* fluent@0.6.0 */
 
 /* eslint no-console: ["error", { allow: ["warn", "error"] }] */
 /* global console */
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 const { L10nRegistry } = Cu.import("resource://gre/modules/L10nRegistry.jsm", {});
 const LocaleService = Cc["@mozilla.org/intl/localeservice;1"].getService(Ci.mozILocaleService);
 const ObserverService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
 
-/**
+/*
  * 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))) {
-      throw new TypeError('Argument must implement the async iteration protocol.');
+    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.iterator = iterable[Symbol.asyncIterator]();
     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());
@@ -83,17 +107,17 @@ class CachedIterable {
 class L10nError extends Error {
   constructor(message) {
     super();
     this.name = 'L10nError';
     this.message = message;
   }
 }
 
-/**
+ /**
  * The default localization strategy for Gecko. It comabines locales
  * available in L10nRegistry, with locales requested by the user to
  * generate the iterator over MessageContexts.
  *
  * In the future, we may want to allow certain modules to override this
  * with a different negotitation strategy to allow for the module to
  * be localized into a different language - for example DevTools.
  */
@@ -112,17 +136,17 @@ function defaultGenerateMessages(resourc
  * The `Localization` class is a central high-level API for vanilla
  * JavaScript use of Fluent.
  * It combines language negotiation, MessageContext and I/O to
  * provide a scriptable API to format translations.
  */
 class Localization {
   /**
    * @param {Array<String>} resourceIds      - List of resource IDs
-   * @param {Function}      generateMessages - Function that returns the
+   * @param {Function}      generateMessages - Function that returns a
    *                                           generator over MessageContexts
    *
    * @returns {Localization}
    */
   constructor(resourceIds, generateMessages = defaultGenerateMessages) {
     this.resourceIds = resourceIds;
     this.generateMessages = generateMessages;
     this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds));
@@ -181,16 +205,19 @@ class Localization {
    */
   formatMessages(keys) {
     return this.formatWithFallback(keys, messageFromContext);
   }
 
   /**
    * Retrieve translations corresponding to the passed keys.
    *
+   * A generalized version of `DOMLocalization.formatValue`. Keys can
+   * either be simple string identifiers or `[id, args]` arrays.
+   *
    *     docL10n.formatValues([
    *       ['hello', { who: 'Mary' }],
    *       ['hello', { who: 'John' }],
    *       ['welcome']
    *     ]).then(console.log);
    *
    *     // ['Hello, Mary!', 'Hello, John!', 'Welcome!']
    *
--- a/intl/l10n/MessageContext.jsm
+++ b/intl/l10n/MessageContext.jsm
@@ -11,23 +11,25 @@
  * 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.
  */
 
 
-/* fluent@0.4.1 */
+/* fluent@0.6.0 */
 
 /*  eslint no-magic-numbers: [0]  */
 
 const MAX_PLACEABLES = 100;
 
-const identifierRe = new RegExp('[a-zA-Z_][a-zA-Z0-9_-]*', 'y');
+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.
@@ -87,17 +89,18 @@ class RuntimeParser {
         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 === '/') {
+    if (ch === '/' ||
+      (ch === '#' && [' ', '#'].includes(this._source[this._index + 1]))) {
       this.skipComment();
       return;
     }
 
     if (ch === '[') {
       this.skipSection();
       return;
     }
@@ -114,17 +117,17 @@ class RuntimeParser {
     this._index += 1;
     if (this._source[this._index] !== '[') {
       throw this.error('Expected "[[" to open a section');
     }
 
     this._index += 1;
 
     this.skipInlineWS();
-    this.getSymbol();
+    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;
@@ -132,72 +135,60 @@ class RuntimeParser {
 
   /**
    * 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.getIdentifier();
-    let attrs = null;
-    let tags = null;
+    const id = this.getEntryIdentifier();
+
+    this.skipInlineWS();
+
+    if (this._source[this._index] === '=') {
+      this._index++;
+    }
 
     this.skipInlineWS();
 
-    let ch = this._source[this._index];
-
-    let val;
-
-    if (ch === '=') {
-      this._index++;
-
-      this.skipInlineWS();
+    const val = this.getPattern();
 
-      val = this.getPattern();
-    } else {
-      this.skipWS();
-    }
-
-    ch = this._source[this._index];
-
-    if (ch === '\n') {
-      this._index++;
-      this.skipInlineWS();
-      ch = this._source[this._index];
+    if (id.startsWith('-') && val === null) {
+      throw this.error('Expected term to have a value');
     }
 
-    if (ch === '.') {
-      attrs = this.getAttributes();
+    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 (ch === '#') {
-      if (attrs !== null) {
-        throw this.error('Tags cannot be added to a message with attributes.');
-      }
-      tags = this.getTags();
-    }
-
-    if (tags === null && attrs === null && typeof val === 'string') {
+    if (attrs === null && typeof val === 'string') {
       this.entries[id] = val;
     } else {
-      if (val === undefined) {
-        if (tags === null && attrs === null) {
-          throw this.error(`Expected a value (like: " = value") or
-            an attribute (like: ".key = value")`);
-        }
+      if (val === null && attrs === null) {
+        throw this.error('Expected message to have a value or attributes');
       }
 
-      this.entries[id] = { val };
-      if (attrs) {
+      this.entries[id] = {};
+
+      if (val !== null) {
+        this.entries[id].val = val;
+      }
+
+      if (attrs !== null) {
         this.entries[id].attrs = attrs;
       }
-      if (tags) {
-        this.entries[id].tags = tags;
-      }
     }
   }
 
   /**
    * Skip whitespace.
    *
    * @private
    */
@@ -216,73 +207,105 @@ class RuntimeParser {
   skipInlineWS() {
     let ch = this._source[this._index];
     while (ch === ' ' || ch === '\t') {
       ch = this._source[++this._index];
     }
   }
 
   /**
-   * Get Message identifier.
+   * 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() {
-    identifierRe.lastIndex = this._index;
-
-    const result = identifierRe.exec(this._source);
+  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 (starting with [a-zA-Z_])');
+      throw this.error(`Expected an identifier [${re.toString()}]`);
     }
 
-    this._index = identifierRe.lastIndex;
+    this._index = re.lastIndex;
     return result[0];
   }
 
   /**
-   * Get Symbol.
+   * 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
    */
-  getSymbol() {
+  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 >= 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 >= 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: 'sym', name };
+    return { type: 'varname', name };
   }
 
   /**
    * Get simple string argument enclosed in `"`.
    *
    * @returns {String}
    * @private
    */
@@ -292,17 +315,17 @@ class RuntimeParser {
     while (++this._index < this._length) {
       const ch = this._source[this._index];
 
       if (ch === '"') {
         break;
       }
 
       if (ch === '\n') {
-        break;
+        throw this.error('Unterminated string expression');
       }
     }
 
     return this._source.substring(start, this._index++);
   }
 
   /**
    * Parses a Message pattern.
@@ -320,31 +343,50 @@ class RuntimeParser {
     // 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 line = start !== eol ?
-      this._source.slice(start, eol) : undefined;
+    const firstLineContent = start !== eol ?
+      this._source.slice(start, eol) : null;
 
-    if (line !== undefined && line.includes('{')) {
+    if (firstLineContent && firstLineContent.includes('{')) {
       return this.getComplexPattern();
     }
 
     this._index = eol + 1;
 
-    if (this._source[this._index] === ' ') {
-      this._index = start;
-      return this.getComplexPattern();
+    this.skipBlankLines();
+
+    if (this._source[this._index] !== ' ') {
+      // No indentation means we're done with this message.
+      return firstLineContent;
     }
 
-    return line;
+    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.
    *
@@ -356,32 +398,44 @@ class RuntimeParser {
     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 and `|` character at the beginning of the next one.
+      // 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._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 === '\\') {
@@ -410,17 +464,17 @@ class RuntimeParser {
       if (ch) {
         buffer += ch;
       }
       this._index++;
       ch = this._source[this._index];
     }
 
     if (content.length === 0) {
-      return buffer.length ? buffer : undefined;
+      return buffer.length ? buffer : null;
     }
 
     if (buffer.length) {
       content.push(buffer);
     }
 
     return content;
   }
@@ -457,23 +511,44 @@ class RuntimeParser {
 
     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');
     }
 
@@ -529,16 +604,20 @@ class RuntimeParser {
         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
@@ -552,29 +631,28 @@ class RuntimeParser {
    * Parses call arguments for a CallExpression.
    *
    * @returns {Array}
    * @private
    */
   getCallArgs() {
     const args = [];
 
-    if (this._source[this._index] === ')') {
-      return 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' ||
-         exp.namespace !== undefined) {
+      if (exp.type !== 'ref') {
         args.push(exp);
       } else {
         this.skipInlineWS();
 
         if (this._source[this._index] === ':') {
           this._index++;
           this.skipInlineWS();
 
@@ -673,75 +751,54 @@ class RuntimeParser {
    *
    * @returns {Object}
    * @private
    */
   getAttributes() {
     const attrs = {};
 
     while (this._index < this._length) {
-      const ch = this._source[this._index];
+      if (this._source[this._index] !== ' ') {
+        break;
+      }
+      this.skipInlineWS();
 
-      if (ch !== '.') {
+      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 (typeof val === 'string') {
         attrs[key] = val;
       } else {
         attrs[key] = {
           val
         };
       }
 
-      this.skipWS();
+      this.skipBlankLines();
     }
 
     return attrs;
   }
 
   /**
-   * Parses a list of Message tags.
-   *
-   * @returns {Array}
-   * @private
-   */
-  getTags() {
-    const tags = [];
-
-    while (this._index < this._length) {
-      const ch = this._source[this._index];
-
-      if (ch !== '#') {
-        break;
-      }
-      this._index++;
-
-      const symbol = this.getSymbol();
-
-      tags.push(symbol.name);
-
-      this.skipWS();
-    }
-
-    return tags;
-  }
-
-  /**
    * Parses a list of Selector variants.
    *
    * @returns {Array}
    * @private
    */
   getVariants() {
     const variants = [];
     let index = 0;
@@ -791,17 +848,17 @@ class RuntimeParser {
     // 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.getSymbol();
+      literal = this.getVariantName();
     }
 
     if (this._source[this._index] !== ']') {
       throw this.error('Expected "]"');
     }
 
     this._index++;
     return literal;
@@ -809,47 +866,65 @@ class RuntimeParser {
 
   /**
    * Parses an FTL literal.
    *
    * @returns {Object}
    * @private
    */
   getLiteral() {
-    const cc = this._source.charCodeAt(this._index);
-    if ((cc >= 48 && cc <= 57) || cc === 45) {
-      return this.getNumber();
-    } else if (cc === 34) { // "
-      return this.getString();
-    } else if (cc === 36) { // $
+    const cc0 = this._source.charCodeAt(this._index);
+
+    if (cc0 === 36) { // $
       this._index++;
       return {
         type: 'ext',
         name: this.getIdentifier()
       };
     }
 
-    return {
-      type: 'ref',
-      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] === '/' && 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;
       }
     }
@@ -882,18 +957,18 @@ class RuntimeParser {
   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 === 95 || cc === 47 || cc === 91) {  // _/[
+            (cc >= 65 && cc <= 90) || // A-Z
+             cc === 47 || cc === 91) { // /[
           this._index = start;
           return;
         }
       }
 
       start = this._source.indexOf('\n', start);
 
       if (start === -1) {
@@ -918,17 +993,17 @@ function parse(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 `valueOf` method together with a proper
+ * 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.
@@ -936,52 +1011,60 @@ class FluentType {
    * @returns {FluentType}
    */
   constructor(value, opts) {
     this.value = value;
     this.opts = opts;
   }
 
   /**
-   * Unwrap the instance of `FluentType`.
+   * Unwrap the raw value stored by this `FluentType`.
    *
-   * Unwrapped values are suitable for use outside of the `MessageContext`.
+   * @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.
    *
-   * In most cases, valueOf returns a string, but it can be overriden
-   * and there are use cases, where the return type is not a string.
-   *
-   * An example is fluent-react which implements a custom `FluentType`
-   * to represent React elements passed as arguments to format().
-   *
    * @param   {MessageContext} [ctx]
    * @returns {string}
    */
-  valueOf() {
-    throw new Error('Subclasses of FluentType must implement valueOf.');
+  toString() {
+    throw new Error('Subclasses of FluentType must implement toString.');
   }
 }
 
 class FluentNone extends FluentType {
-  valueOf() {
+  toString() {
     return this.value || '???';
   }
 }
 
 class FluentNumber extends FluentType {
   constructor(value, opts) {
     super(parseFloat(value), opts);
   }
 
-  valueOf(ctx) {
-    const nf = ctx._memoizeIntlObject(
-      Intl.NumberFormat, this.opts
-    );
-    return nf.format(this.value);
+  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}
@@ -994,26 +1077,31 @@ class FluentNumber extends FluentType {
   }
 }
 
 class FluentDateTime extends FluentType {
   constructor(value, opts) {
     super(new Date(value), opts);
   }
 
-  valueOf(ctx) {
-    const dtf = ctx._memoizeIntlObject(
-      Intl.DateTimeFormat, this.opts
-    );
-    return dtf.format(this.value);
+  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 {
-  valueOf() {
+  toString() {
     return this.value;
   }
 
   /**
    * Compare the object with another instance of a FluentType.
    *
    * @param   {MessageContext} ctx
    * @param   {FluentType}     other
@@ -1024,19 +1112,16 @@ class FluentSymbol extends FluentType {
       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);
-    } else if (Array.isArray(other)) {
-      const values = other.map(symbol => symbol.value);
-      return values.includes(this.value);
     }
     return false;
   }
 }
 
 /**
  * @overview
  *
@@ -1047,29 +1132,29 @@ class FluentSymbol extends FluentType {
  *   - 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.
  */
 
 const builtins = {
   'NUMBER': ([arg], opts) =>
-    new FluentNumber(arg.value, merge(arg.opts, opts)),
+    new FluentNumber(arg.valueOf(), merge(arg.opts, opts)),
   'DATETIME': ([arg], opts) =>
-    new FluentDateTime(arg.value, merge(arg.opts, 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 of Object.keys(opts)) {
-    unwrapped[name] = opts[name].value;
+  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
@@ -1095,17 +1180,17 @@ function values(opts) {
  *
  * 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 `valueOf` method to convert the instance to a native value.
+ * 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
@@ -1122,37 +1207,16 @@ function values(opts) {
 const MAX_PLACEABLE_LENGTH = 2500;
 
 // Unicode bidi isolation characters.
 const FSI = '\u2068';
 const PDI = '\u2069';
 
 
 /**
- * Helper for computing the total character length of a placeable.
- *
- * Used in Pattern.
- *
- * @param   {Object} env
- *    Resolver environment object.
- * @param   {Array}  parts
- *    List of parts of a placeable.
- * @returns {Number}
- * @private
- */
-function PlaceableLength(env, parts) {
-  const { ctx } = env;
-  return parts.reduce(
-    (sum, part) => sum + part.valueOf(ctx).length,
-    0
-  );
-}
-
-
-/**
  * 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.
@@ -1181,59 +1245,32 @@ function DefaultMember(env, members, def
  *    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 = ctx.getMessage(name);
+  const message = name.startsWith('-')
+    ? ctx._terms.get(name)
+    : ctx._messages.get(name);
 
   if (!message) {
-    errors.push(new ReferenceError(`Unknown message: ${name}`));
+    const err = name.startsWith('-')
+      ? new ReferenceError(`Unknown term: ${name}`)
+      : new ReferenceError(`Unknown message: ${name}`);
+    errors.push(err);
     return new FluentNone(name);
   }
 
   return message;
 }
 
 /**
- * Resolve an array of tags.
- *
- * @param   {Object} env
- *    Resolver environment object.
- * @param   {Object} id
- *    The identifier of the message with tags.
- * @param   {String} id.name
- *    The name of the identifier.
- * @returns {Array}
- * @private
- */
-function Tags(env, {name}) {
-  const { ctx, errors } = env;
-  const message = ctx.getMessage(name);
-
-  if (!message) {
-    errors.push(new ReferenceError(`Unknown message: ${name}`));
-    return new FluentNone(name);
-  }
-
-  if (!message.tags) {
-    errors.push(new RangeError(`No tags in message "${name}"`));
-    return new FluentNone(name);
-  }
-
-  return message.tags.map(
-    tag => new FluentSymbol(tag)
-  );
-}
-
-
-/**
  * 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.
@@ -1264,17 +1301,17 @@ function VariantExpression(env, {id, key
     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.valueOf(ctx)}`));
+  errors.push(new ReferenceError(`Unknown variant: ${keyword.toString(ctx)}`));
   return Type(env, message);
 }
 
 
 /**
  * Resolve an attribute expression to the attribute object.
  *
  * @param   {Object} env
@@ -1324,19 +1361,17 @@ function AttributeExpression(env, {id, n
  * @returns {FluentType}
  * @private
  */
 function SelectExpression(env, {exp, vars, def}) {
   if (exp === null) {
     return DefaultMember(env, vars, def);
   }
 
-  const selector = exp.type === 'ref'
-    ? Tags(env, exp)
-    : Type(env, exp);
+  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 =
@@ -1356,17 +1391,17 @@ function SelectExpression(env, {exp, var
   return DefaultMember(env, vars, def);
 }
 
 
 /**
  * Resolve expression to a Fluent type.
  *
  * JavaScript strings are a special case.  Since they natively have the
- * `valueOf` method they can be used as if they were a Fluent type without
+ * `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
@@ -1381,17 +1416,17 @@ function Type(env, 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 'sym':
+    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':
@@ -1409,17 +1444,17 @@ function Type(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 !== undefined) {
+      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:
@@ -1444,16 +1479,17 @@ function ExternalArgument(env, {name}) {
 
   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;
@@ -1519,28 +1555,32 @@ function FunctionReference(env, {name}) 
 function CallExpression(env, {fun, args}) {
   const callee = FunctionReference(env, fun);
 
   if (callee instanceof FluentNone) {
     return callee;
   }
 
   const posargs = [];
-  const keyargs = [];
+  const keyargs = {};
 
   for (const arg of args) {
     if (arg.type === 'narg') {
       keyargs[arg.name] = Type(env, arg.val);
     } else {
       posargs.push(Type(env, arg));
     }
   }
 
-  // XXX functions should also report errors
-  return callee(posargs, keyargs);
+  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
@@ -1561,53 +1601,45 @@ function Pattern(env, ptn) {
   const result = [];
 
   for (const elem of ptn) {
     if (typeof elem === 'string') {
       result.push(elem);
       continue;
     }
 
-    const part = Type(env, elem);
+    const part = Type(env, elem).toString(ctx);
 
     if (ctx._useIsolating) {
       result.push(FSI);
     }
 
-    if (Array.isArray(part)) {
-      const len = PlaceableLength(env, part);
-
-      if (len > MAX_PLACEABLE_LENGTH) {
-        errors.push(
-          new RangeError(
-            'Too many characters in placeable ' +
-            `(${len}, max allowed is ${MAX_PLACEABLE_LENGTH})`
-          )
-        );
-        result.push(new FluentNone());
-      } else {
-        result.push(...part);
-      }
+    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 (ctx._useIsolating) {
       result.push(PDI);
     }
   }
 
   dirty.delete(ptn);
-  return result;
+  return result.join('');
 }
 
 /**
- * Format a translation into an `FluentType`.
- *
- * The return value must be unwrapped via `valueOf` by the caller.
+ * 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
@@ -1615,17 +1647,17 @@ function Pattern(env, ptn) {
  * @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);
+  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
@@ -1667,24 +1699,25 @@ class MessageContext {
    *
    * @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 `[id, message]` pairs.
+   * Return an iterator over public `[id, message]` pairs.
    *
    * @returns {Iterator}
    */
   get messages() {
     return this._messages[Symbol.iterator]();
   }
 
   /*
@@ -1696,17 +1729,17 @@ class MessageContext {
   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` and `MessageContext.formatToParts`.
+   * `MessageContext.format`.
    *
    * @param {string} id - The identifier of the message to check.
    * @returns {Any}
    */
   getMessage(id) {
     return this._messages.get(id);
   }
 
@@ -1726,78 +1759,29 @@ class MessageContext {
    * 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) {
-      this._messages.set(id, entries[id]);
+      if (id.startsWith('-')) {
+        // Identifiers starting with a dash (-) define terms. Terms are private
+        // and cannot be retrieved from MessageContext.
+        this._terms.set(id, entries[id]);
+      } else {
+        this._messages.set(id, entries[id]);
+      }
     }
 
     return errors;
   }
 
   /**
-   * Format a message to an array of `FluentTypes` or null.
-   *
-   * Format a raw `message` from the context into an array of `FluentType`
-   * instances which may be used to build the final result.  It may also return
-   * `null` if it has a null value.  `args` will be used to resolve references
-   * to external arguments inside of the translation.
-   *
-   * See the documentation of {@link MessageContext#format} for more
-   * information about error handling.
-   *
-   * 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.
-   *
-   *     ctx.addMessages('hello = Hello, { $name }!');
-   *     const hello = ctx.getMessage('hello');
-   *     ctx.formatToParts(hello, { name: 'Jane' }, []);
-   *     // → ['Hello, ', '\u2068', 'Jane', '\u2069']
-   *
-   * The returned parts need to be formatted via `valueOf` before they can be
-   * used further.  This will ensure all values are correctly formatted
-   * according to the `MessageContext`'s locale.
-   *
-   *     const parts = ctx.formatToParts(hello, { name: 'Jane' }, []);
-   *     const str = parts.map(part => part.valueOf(ctx)).join('');
-   *
-   * @see MessageContext#format
-   * @param   {Object | string}    message
-   * @param   {Object | undefined} args
-   * @param   {Array}              errors
-   * @returns {?Array<FluentType>}
-   */
-  formatToParts(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;
-    }
-
-    const result = resolve(this, args, message, errors);
-
-    return result instanceof FluentNone ? null : result;
-  }
-
-  /**
    * 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
@@ -1833,23 +1817,17 @@ class MessageContext {
       return message.val;
     }
 
     // optimize entities with null values
     if (message.val === undefined) {
       return null;
     }
 
-    const result = resolve(this, args, message, errors);
-
-    if (result instanceof FluentNone) {
-      return null;
-    }
-
-    return result.map(part => part.valueOf(this)).join('');
+    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);
new file mode 100644
--- /dev/null
+++ b/intl/l10n/README
@@ -0,0 +1,17 @@
+The content of this directory is partially sourced from the fluent.js project.
+
+The following files are affected:
+ - MessageContext.jsm
+ - Localization.jsm
+ - DOMLocalization.jsm
+ - l10n.js
+
+At the moment, the tool used to produce those files in fluent.js repository, doesn't
+fully align with how the code is structured here, so we perform a manual adjustments
+mostly around header and footer.
+
+The result difference is stored in `./fluent.js.patch` file which can be used to
+approximate the changes needed to be applied on the output of the 
+fluent.js/fluent-gecko's make.
+
+In b.m.o. bug 1434434 we will try to reduce this difference to zero. 
new file mode 100644
--- /dev/null
+++ b/intl/l10n/fluent.js.patch
@@ -0,0 +1,492 @@
+diff -uNr ./dist/DOMLocalization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm
+--- ./dist/DOMLocalization.jsm	2018-01-30 13:46:58.589811108 -0800
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm	2018-01-30 13:46:13.613146435 -0800
+@@ -18,7 +18,8 @@
+ 
+ /* fluent@0.6.0 */
+ 
+-import Localization from '../../fluent-dom/src/localization.js';
++const { Localization } =
++  Components.utils.import("resource://gre/modules/Localization.jsm", {});
+ 
+ // Match the opening angle bracket (<) in HTML tags, and HTML entities like
+ // &amp;, &#0038;, &#x0026;.
+@@ -623,36 +624,5 @@
+   }
+ }
+ 
+-/* global L10nRegistry, Services */
+-/**
+- * The default localization strategy for Gecko. It comabines locales
+- * available in L10nRegistry, with locales requested by the user to
+- * generate the iterator over MessageContexts.
+- *
+- * In the future, we may want to allow certain modules to override this
+- * with a different negotitation strategy to allow for the module to
+- * be localized into a different language - for example DevTools.
+- */
+-function defaultGenerateMessages(resourceIds) {
+-  const requestedLocales = Services.locale.getRequestedLocales();
+-  const availableLocales = L10nRegistry.getAvailableLocales();
+-  const defaultLocale = Services.locale.defaultLocale;
+-  const locales = Services.locale.negotiateLanguages(
+-    requestedLocales, availableLocales, defaultLocale,
+-  );
+-  return L10nRegistry.generateContexts(locales, resourceIds);
+-}
+-
+-
+-class GeckoDOMLocalization extends DOMLocalization {
+-  constructor(
+-    windowElement,
+-    resourceIds,
+-    generateMessages = defaultGenerateMessages
+-  ) {
+-    super(windowElement, resourceIds, generateMessages);
+-  }
+-}
+-
+-this.DOMLocalization = GeckoDOMLocalization;
++this.DOMLocalization = DOMLocalization;
+ this.EXPORTED_SYMBOLS = ['DOMLocalization'];
+diff -uNr ./dist/l10n.js /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js
+--- ./dist/l10n.js	2018-01-30 13:46:58.749811101 -0800
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js	2018-01-26 20:52:09.106650798 -0800
+@@ -1,7 +1,6 @@
+-/* global Components, document, window */
+ {
+   const { DOMLocalization } =
+-    Components.utils.import('resource://gre/modules/DOMLocalization.jsm');
++    Components.utils.import("resource://gre/modules/DOMLocalization.jsm");
+ 
+   /**
+    * Polyfill for document.ready polyfill.
+diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm
+--- ./dist/Localization.jsm	2018-01-30 13:46:58.393144450 -0800
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm	2018-01-30 13:46:04.593146834 -0800
+@@ -18,92 +18,16 @@
+ 
+ /* fluent@0.6.0 */
+ 
+-/*  eslint no-magic-numbers: [0]  */
+-
+-/* 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.
+- */
+-
+-/**
+- * @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.
+- */
++/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
++/* global console */
+ 
+-/**
+- * @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.
+- */
++const Cu = Components.utils;
++const Cc = Components.classes;
++const Ci = Components.interfaces;
+ 
+-/**
+- * 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.
+- */
++const { L10nRegistry } = Cu.import("resource://gre/modules/L10nRegistry.jsm", {});
++const LocaleService = Cc["@mozilla.org/intl/localeservice;1"].getService(Ci.mozILocaleService);
++const ObserverService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+ 
+ /*
+  * CachedIterable caches the elements yielded by an iterable.
+@@ -170,87 +94,6 @@
+   }
+ }
+ 
+-/*
+- * @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>}
+- */
+-
+-
+-/*
+- * 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>>}
+- */
+-
+-/**
+- * Template literal tag for dedenting FTL code.
+- *
+- * Strip the common indent of non-blank lines. Remove blank lines.
+- *
+- * @param {Array<string>} strings
+- */
+-
+-/*
+- * @module fluent
+- * @overview
+- *
+- * `fluent` is a JavaScript implementation of Project Fluent, a localization
+- * framework designed to unleash the expressive power of the natural language.
+- *
+- */
+-
+-/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
+-/* global console */
+-
+ /**
+  * Specialized version of an Error used to indicate errors that are result
+  * of a problem during the localization process.
+@@ -269,6 +112,26 @@
+   }
+ }
+ 
++ /**
++ * The default localization strategy for Gecko. It comabines locales
++ * available in L10nRegistry, with locales requested by the user to
++ * generate the iterator over MessageContexts.
++ *
++ * In the future, we may want to allow certain modules to override this
++ * with a different negotitation strategy to allow for the module to
++ * be localized into a different language - for example DevTools.
++ */
++function defaultGenerateMessages(resourceIds) {
++  const availableLocales = L10nRegistry.getAvailableLocales();
++
++  const requestedLocales = LocaleService.getRequestedLocales();
++  const defaultLocale = LocaleService.defaultLocale;
++  const locales = LocaleService.negotiateLanguages(
++    requestedLocales, availableLocales, defaultLocale,
++  );
++  return L10nRegistry.generateContexts(locales, resourceIds);
++}
++
+ /**
+  * The `Localization` class is a central high-level API for vanilla
+  * JavaScript use of Fluent.
+@@ -283,7 +146,7 @@
+    *
+    * @returns {Localization}
+    */
+-  constructor(resourceIds, generateMessages) {
++  constructor(resourceIds, generateMessages = defaultGenerateMessages) {
+     this.resourceIds = resourceIds;
+     this.generateMessages = generateMessages;
+     this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds));
+@@ -303,7 +166,7 @@
+    */
+   async formatWithFallback(keys, method) {
+     const translations = [];
+-    for (let ctx of this.ctxs) {
++    for await (let ctx of this.ctxs) {
+       // This can operate on synchronous and asynchronous
+       // contexts coming from the iterator.
+       if (typeof ctx.then === 'function') {
+@@ -394,8 +257,38 @@
+     return val;
+   }
+ 
+-  handleEvent() {
+-    this.onLanguageChange();
++  /**
++   * Register observers on events that will trigger cache invalidation
++   */
++  registerObservers() {
++    ObserverService.addObserver(this, 'l10n:available-locales-changed', false);
++    ObserverService.addObserver(this, 'intl:requested-locales-changed', false);
++  }
++
++  /**
++   * Unregister observers on events that will trigger cache invalidation
++   */
++  unregisterObservers() {
++    ObserverService.removeObserver(this, 'l10n:available-locales-changed');
++    ObserverService.removeObserver(this, 'intl:requested-locales-changed');
++  }
++
++  /**
++   * Default observer handler method.
++   *
++   * @param {String} subject
++   * @param {String} topic
++   * @param {Object} data
++   */
++  observe(subject, topic, data) {
++    switch (topic) {
++      case 'l10n:available-locales-changed':
++      case 'intl:requested-locales-changed':
++        this.onLanguageChange();
++        break;
++      default:
++        break;
++    }
+   }
+ 
+   /**
+@@ -538,7 +431,8 @@
+       hasErrors = true;
+     }
+ 
+-    if (messageErrors.length && typeof console !== 'undefined') {
++    if (messageErrors.length) {
++      const { console } = Cu.import("resource://gre/modules/Console.jsm", {});
+       messageErrors.forEach(error => console.warn(error));
+     }
+   });
+@@ -546,45 +440,5 @@
+   return hasErrors;
+ }
+ 
+-/* global Components */
+-/* eslint no-unused-vars: 0 */
+-
+-const Cu = Components.utils;
+-const Cc = Components.classes;
+-const Ci = Components.interfaces;
+-
+-const { L10nRegistry } =
+-  Cu.import('resource://gre/modules/L10nRegistry.jsm', {});
+-const ObserverService =
+-  Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
+-const { Services } =
+-  Cu.import('resource://gre/modules/Services.jsm', {});
+-
+-
+-/**
+- * The default localization strategy for Gecko. It comabines locales
+- * available in L10nRegistry, with locales requested by the user to
+- * generate the iterator over MessageContexts.
+- *
+- * In the future, we may want to allow certain modules to override this
+- * with a different negotitation strategy to allow for the module to
+- * be localized into a different language - for example DevTools.
+- */
+-function defaultGenerateMessages(resourceIds) {
+-  const requestedLocales = Services.locale.getRequestedLocales();
+-  const availableLocales = L10nRegistry.getAvailableLocales();
+-  const defaultLocale = Services.locale.defaultLocale;
+-  const locales = Services.locale.negotiateLanguages(
+-    requestedLocales, availableLocales, defaultLocale,
+-  );
+-  return L10nRegistry.generateContexts(locales, resourceIds);
+-}
+-
+-class GeckoLocalization extends Localization {
+-  constructor(resourceIds, generateMessages = defaultGenerateMessages) {
+-    super(resourceIds, generateMessages);
+-  }
+-}
+-
+-this.Localization = GeckoLocalization;
++this.Localization = Localization;
+ this.EXPORTED_SYMBOLS = ['Localization'];
+diff -uNr ./dist/MessageContext.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm
+--- ./dist/MessageContext.jsm	2018-01-30 13:46:58.119811129 -0800
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm	2018-01-30 13:53:23.036460739 -0800
+@@ -1838,90 +1838,5 @@
+   }
+ }
+ 
+-/*
+- * CachedIterable caches the elements yielded by an iterable.
+- *
+- * It can be used to iterate over an iterable many times without depleting the
+- * iterable.
+- */
+-
+-/*
+- * @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>}
+- */
+-
+-
+-/*
+- * 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>>}
+- */
+-
+-/**
+- * Template literal tag for dedenting FTL code.
+- *
+- * Strip the common indent of non-blank lines. Remove blank lines.
+- *
+- * @param {Array<string>} strings
+- */
+-
+-/*
+- * @module fluent
+- * @overview
+- *
+- * `fluent` is a JavaScript implementation of Project Fluent, a localization
+- * framework designed to unleash the expressive power of the natural language.
+- *
+- */
+-
+ this.MessageContext = MessageContext;
+ this.EXPORTED_SYMBOLS = ['MessageContext'];
--- a/intl/l10n/test/test_messagecontext.js
+++ b/intl/l10n/test/test_messagecontext.js
@@ -9,31 +9,25 @@ function run_test() {
 
   ok(true);
 }
 
 function test_methods_presence(MessageContext) {
   const ctx = new MessageContext(["en-US", "pl"]);
   equal(typeof ctx.addMessages, "function");
   equal(typeof ctx.format, "function");
-  equal(typeof ctx.formatToParts, "function");
 }
 
 function test_methods_calling(MessageContext) {
   const ctx = new MessageContext(["en-US", "pl"], {
     useIsolating: false
   });
   ctx.addMessages("key = Value");
 
   const msg = ctx.getMessage("key");
   equal(ctx.format(msg), "Value");
-  deepEqual(ctx.formatToParts(msg), ["Value"]);
 
   ctx.addMessages("key2 = Hello { $name }");
 
   const msg2 = ctx.getMessage("key2");
   equal(ctx.format(msg2, { name: "Amy" }), "Hello Amy");
-  deepEqual(ctx.formatToParts(msg2), ["Hello ", {
-    value: "name",
-    opts: undefined
-  }]);
   ok(true);
 }