Bug 1480881 - Upgrade Gecko to Fluent 0.6. r=stas
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 07 Aug 2018 00:08:29 +0000
changeset 430292 7140a25f020cb41ac724148bc768ba01e4933c69
parent 430291 d6d39130d3c9c355e2656941ea39703fc2265f1e
child 430293 b3b9e6ed5f380b690fd2bec5dbff991a6d893c9c
push id67405
push userzbraniecki@mozilla.com
push dateTue, 07 Aug 2018 00:10:28 +0000
treeherderautoland@7140a25f020c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersstas
bugs1480881
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1480881 - Upgrade Gecko to Fluent 0.6. r=stas Upgrade Gecko to Fluent 0.6. Differential Revision: https://phabricator.services.mozilla.com/D2740
intl/l10n/DOMLocalization.jsm
intl/l10n/Localization.jsm
intl/l10n/MessageContext.jsm
intl/l10n/fluent.js.patch
intl/l10n/test/dom/test_domloc.xul
intl/l10n/test/dom/test_domloc_translateElements.html
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -11,17 +11,17 @@
  * 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-dom@aa95b1f (July 10, 2018) */
+/* fluent-dom@cab517f (July 31, 2018) */
 
 const { Localization } =
   ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
 const { Services } =
   ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
 // Match the opening angle bracket (<) in HTML tags, and HTML entities like
 // &amp;, &#0038;, &#x0026;.
@@ -55,20 +55,20 @@ const LOCALIZABLE_ATTRIBUTES = {
     optgroup: ["label"],
     option: ["label"],
     track: ["label"],
     img: ["alt"],
     textarea: ["placeholder"],
     th: ["abbr"]
   },
   "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": {
-    description: ["value"],
     global: [
       "accesskey", "aria-label", "aria-valuetext", "aria-moz-hint", "label"
     ],
+    description: ["value"],
     key: ["key", "keycode"],
     label: ["value"],
     textbox: ["placeholder"],
     toolbarbutton: ["tooltiptext"],
   }
 };
 
 
@@ -518,17 +518,17 @@ class DOMLocalization extends Localizati
           newRoot.contains(root)) {
         throw new Error("Cannot add a root that overlaps with existing root.");
       }
     }
 
     if (this.windowElement) {
       if (this.windowElement !== newRoot.ownerGlobal) {
         throw new Error(`Cannot connect a root:
-          DOMLocalization already has a root from a different window`);
+          DOMLocalization already has a root from a different window.`);
       }
     } else {
       this.windowElement = newRoot.ownerGlobal;
       this.mutationObserver = new this.windowElement.MutationObserver(
         mutations => this.translateMutations(mutations)
       );
     }
 
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -11,78 +11,122 @@
  * 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-dom@aa95b1f (July 10, 2018) */
+/* fluent-dom@cab517f (July 31, 2018) */
 
 /* eslint no-console: ["error", { allow: ["warn", "error"] }] */
 /* global console */
 
 const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
 
-/*
- * CachedAsyncIterable caches the elements yielded by an iterable.
- *
- * It can be used to iterate over an iterable many times without depleting the
- * iterable.
- */
-class CachedAsyncIterable {
+class CachedIterable extends Array {
+  /**
+   * Create a `CachedIterable` instance from an iterable or, if another
+   * instance of `CachedIterable` is passed, return it without any
+   * modifications.
+   *
+   * @param {Iterable} iterable
+   * @returns {CachedIterable}
+   */
+  static from(iterable) {
+    if (iterable instanceof this) {
+      return iterable;
+    }
+
+    return new this(iterable);
+  }
+}
+
+class CachedAsyncIterable extends CachedIterable {
   /**
    * Create an `CachedAsyncIterable` instance.
    *
    * @param {Iterable} iterable
    * @returns {CachedAsyncIterable}
    */
   constructor(iterable) {
+    super();
+
     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 = [];
   }
 
+  /**
+   * Synchronous iterator over the cached elements.
+   *
+   * Return a generator object implementing the iterator protocol over the
+   * cached elements of the original (async or sync) iterable.
+   */
+  [Symbol.iterator]() {
+    const cached = this;
+    let cur = 0;
+
+    return {
+      next() {
+        if (cached.length === cur) {
+          return {value: undefined, done: true};
+        }
+        return cached[cur++];
+      }
+    };
+  }
+
+  /**
+   * Asynchronous iterator caching the yielded elements.
+   *
+   * Elements yielded by the original iterable will be cached and available
+   * synchronously. Returns an async generator object implementing the
+   * iterator protocol over the elements of the original (async or sync)
+   * iterable.
+   */
   [Symbol.asyncIterator]() {
-    const { seen, iterator } = this;
+    const cached = this;
     let cur = 0;
 
     return {
       async next() {
-        if (seen.length <= cur) {
-          seen.push(await iterator.next());
+        if (cached.length <= cur) {
+          cached.push(await cached.iterator.next());
         }
-        return seen[cur++];
+        return cached[cur++];
       }
     };
   }
 
   /**
    * This method allows user to consume the next element from the iterator
    * into the cache.
    *
    * @param {number} count - number of elements to consume
    */
   async touchNext(count = 1) {
-    const { seen, iterator } = this;
     let idx = 0;
     while (idx++ < count) {
-      if (seen.length === 0 || seen[seen.length - 1].done === false) {
-        seen.push(await iterator.next());
+      const last = this[this.length - 1];
+      if (last && last.done) {
+        break;
       }
+      this.push(await this.iterator.next());
     }
+    // Return the last cached {value, done} object to allow the calling
+    // code to decide if it needs to call touchNext again.
+    return this[this.length - 1];
   }
 }
 
 /**
  * The default localization strategy for Gecko. It comabines locales
  * available in L10nRegistry, with locales requested by the user to
  * generate the iterator over MessageContexts.
  *
@@ -107,18 +151,18 @@ class Localization {
    * @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 CachedAsyncIterable(this.generateMessages(this.resourceIds));
+    this.ctxs = CachedAsyncIterable.from(
+      this.generateMessages(this.resourceIds));
   }
 
   addResourceIds(resourceIds) {
     this.resourceIds.push(...resourceIds);
     this.onChange();
     return this.resourceIds.length;
   }
 
@@ -271,18 +315,18 @@ class Localization {
     }
   }
 
   /**
    * This method should be called when there's a reason to believe
    * that language negotiation or available resources changed.
    */
   onChange() {
-    this.ctxs =
-      new CachedAsyncIterable(this.generateMessages(this.resourceIds));
+    this.ctxs = CachedAsyncIterable.from(
+      this.generateMessages(this.resourceIds));
     this.ctxs.touchNext(2);
   }
 }
 
 Localization.prototype.QueryInterface = ChromeUtils.generateQI([
   Ci.nsISupportsWeakReference
 ]);
 
--- a/intl/l10n/MessageContext.jsm
+++ b/intl/l10n/MessageContext.jsm
@@ -11,25 +11,28 @@
  * 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@aa95b1f (July 10, 2018) */
+/* fluent@0.7.0 */
 
 /*  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_?-]*$/;
+const unicodeEscapeRe = /^[a-fA-F0-9]{4}$/;
+const trailingWSRe = /[ \t\n\r]+$/;
+
 
 /**
  * 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.
@@ -89,69 +92,40 @@ 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 === "/" ||
-      (ch === "#" &&
-        [" ", "#", "\n"].includes(this._source[this._index + 1]))) {
+    if (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++;
+    } else {
+      throw this.error("Expected \"=\" after the identifier");
     }
 
     this.skipInlineWS();
 
     const val = this.getPattern();
 
     if (id.startsWith("-") && val === null) {
       throw this.error("Expected term to have a value");
@@ -231,17 +205,17 @@ class RuntimeParser {
       }
     }
   }
 
   /**
    * Get identifier using the provided regex.
    *
    * By default this will get identifiers of public messages, attributes and
-   * external arguments (without the $).
+   * variables (without the $).
    *
    * @returns {String}
    * @private
    */
   getIdentifier(re = identifierRe) {
     re.lastIndex = this._index;
     const result = re.exec(this._source);
 
@@ -306,31 +280,40 @@ class RuntimeParser {
 
   /**
    * Get simple string argument enclosed in `"`.
    *
    * @returns {String}
    * @private
    */
   getString() {
-    const start = this._index + 1;
+    let value = "";
+    this._index++;
 
-    while (++this._index < this._length) {
+    while (this._index < this._length) {
       const ch = this._source[this._index];
 
       if (ch === '"') {
+        this._index++;
         break;
       }
 
       if (ch === "\n") {
         throw this.error("Unterminated string expression");
       }
+
+      if (ch === "\\") {
+        value += this.getEscapedCharacter(["{", "\\", "\""]);
+      } else {
+        this._index++;
+        value += ch;
+      }
     }
 
-    return this._source.substring(start, this._index++);
+    return value;
   }
 
   /**
    * Parses a Message pattern.
    * Message Pattern may be a simple string or an array of strings
    * and placeable expressions.
    *
    * @returns {String|Array}
@@ -344,20 +327,27 @@ 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 firstLineContent = start !== eol ?
-      this._source.slice(start, eol) : null;
+    // If there's any text between the = and the EOL, store it for now. The next
+    // non-empty line will decide what to do with it.
+    const firstLineContent = start !== eol
+      // Trim the trailing whitespace in case this is a single-line pattern.
+      // Multiline patterns are parsed anew by getComplexPattern.
+      ? this._source.slice(start, eol).replace(trailingWSRe, "")
+      : null;
 
-    if (firstLineContent && firstLineContent.includes("{")) {
+    if (firstLineContent
+      && (firstLineContent.includes("{")
+        || firstLineContent.includes("\\"))) {
       return this.getComplexPattern();
     }
 
     this._index = eol + 1;
 
     this.skipBlankLines();
 
     if (this._source[this._index] !== " ") {
@@ -434,61 +424,91 @@ class RuntimeParser {
 
         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 === "{") {
+      }
+
+      if (ch === undefined) {
+        break;
+      }
+
+      if (ch === "\\") {
+        buffer += this.getEscapedCharacter();
+        ch = this._source[this._index];
+        continue;
+      }
+
+      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];
+        ch = this._source[++this._index];
         placeables++;
         continue;
       }
 
-      if (ch) {
-        buffer += ch;
-      }
-      this._index++;
-      ch = this._source[this._index];
+      buffer += ch;
+      ch = this._source[++this._index];
     }
 
     if (content.length === 0) {
       return buffer.length ? buffer : null;
     }
 
     if (buffer.length) {
-      content.push(buffer);
+      // Trim trailing whitespace, too.
+      content.push(buffer.replace(trailingWSRe, ""));
     }
 
     return content;
   }
   /* eslint-enable complexity */
 
   /**
+   * Parse an escape sequence and return the unescaped character.
+   *
+   * @returns {string}
+   * @private
+   */
+  getEscapedCharacter(specials = ["{", "\\"]) {
+    this._index++;
+    const next = this._source[this._index];
+
+    if (specials.includes(next)) {
+      this._index++;
+      return next;
+    }
+
+    if (next === "u") {
+      const sequence = this._source.slice(this._index + 1, this._index + 5);
+      if (unicodeEscapeRe.test(sequence)) {
+        this._index += 5;
+        return String.fromCodePoint(parseInt(sequence, 16));
+      }
+
+      throw this.error(`Invalid Unicode escape sequence: \\u${sequence}`);
+    }
+
+    throw this.error(`Unknown escape sequence: \\${next}`);
+  }
+
+  /**
    * Parses a single placeable in a Message pattern and returns its
    * expression.
    *
    * @returns {Object}
    * @private
    */
   getPlaceable() {
     const start = ++this._index;
@@ -514,38 +534,38 @@ class RuntimeParser {
 
     const selector = this.getSelectorExpression();
 
     this.skipWS();
 
     const ch = this._source[this._index];
 
     if (ch === "}") {
-      if (selector.type === "attr" && selector.id.name.startsWith("-")) {
+      if (selector.type === "getattr" && 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") {
+    if (selector.type === "getvar") {
       throw this.error("Variants cannot be used as selectors.");
     }
 
-    if (selector.type === "attr" && !selector.id.name.startsWith("-")) {
+    if (selector.type === "getattr" && !selector.id.name.startsWith("-")) {
       throw this.error(
         "Attributes of public messages cannot be used as selectors."
       );
     }
 
 
     this._index += 2; // ->
 
@@ -573,41 +593,45 @@ class RuntimeParser {
 
   /**
    * Parses a selector expression.
    *
    * @returns {Object}
    * @private
    */
   getSelectorExpression() {
+    if (this._source[this._index] === "{") {
+      return this.getPlaceable();
+    }
+
     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",
+        type: "getattr",
         id: literal,
         name
       };
     }
 
     if (this._source[this._index] === "[") {
       this._index++;
 
       const key = this.getVariantKey();
       this._index++;
       return {
-        type: "var",
+        type: "getvar",
         id: literal,
         key
       };
     }
 
     if (this._source[this._index] === "(") {
       this._index++;
       const args = this.getCallArgs();
@@ -635,34 +659,34 @@ class RuntimeParser {
    *
    * @returns {Array}
    * @private
    */
   getCallArgs() {
     const args = [];
 
     while (this._index < this._length) {
-      this.skipInlineWS();
+      this.skipWS();
 
       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();
+          this.skipWS();
 
           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.
@@ -680,17 +704,17 @@ class RuntimeParser {
               "Expected string in quotes, number.");
           }
 
         } else {
           args.push(exp);
         }
       }
 
-      this.skipInlineWS();
+      this.skipWS();
 
       if (this._source[this._index] === ")") {
         break;
       } else if (this._source[this._index] === ",") {
         this._index++;
       } else {
         throw this.error('Expected "," or ")"');
       }
@@ -880,17 +904,17 @@ class RuntimeParser {
    * @private
    */
   getLiteral() {
     const cc0 = this._source.charCodeAt(this._index);
 
     if (cc0 === 36) { // $
       this._index++;
       return {
-        type: "ext",
+        type: "var",
         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.
@@ -920,22 +944,21 @@ class RuntimeParser {
    *
    * @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])))) {
+    while (eol !== -1
+      && 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) {
@@ -967,17 +990,17 @@ class RuntimeParser {
     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) { // /[
+             cc === 45) { // -
           this._index = start;
           return;
         }
       }
 
       start = this._source.indexOf("\n", start);
 
       if (start === -1) {
@@ -1164,21 +1187,21 @@ function values(opts) {
 }
 
 /**
  * @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,
+ * Translations can contain references to other messages or variables,
  * 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
+ * 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
@@ -1431,31 +1454,31 @@ function Type(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 "var":
+      return VariableReference(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": {
+    case "getattr": {
       const attr = AttributeExpression(env, expr);
       return Type(env, attr);
     }
-    case "var": {
+    case "getvar": {
       const variant = VariantExpression(env, expr);
       return Type(env, variant);
     }
     case "sel": {
       const member = SelectExpression(env, expr);
       return Type(env, member);
     }
     case undefined: {
@@ -1469,32 +1492,32 @@ function Type(env, expr) {
       return new FluentNone();
     }
     default:
       return new FluentNone();
   }
 }
 
 /**
- * Resolve a reference to an external argument.
+ * Resolve a reference to a variable.
  *
  * @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}) {
+function VariableReference(env, {name}) {
   const { args, errors } = env;
 
   if (!args || !args.hasOwnProperty(name)) {
-    errors.push(new ReferenceError(`Unknown external: ${name}`));
+    errors.push(new ReferenceError(`Unknown variable: ${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;
@@ -1507,17 +1530,17 @@ function ExternalArgument(env, {name}) {
     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}`)
+        new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
       );
       return new FluentNone(name);
   }
 }
 
 /**
  * Resolve a reference to a function.
  *
@@ -1686,23 +1709,23 @@ class FluentResource extends Map {
   }
 }
 
 /**
  * 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.
+ * Always use `MessageContext.format` to retrieve translation units from a
+ * context. Translations can contain references to other entities or variables,
+ * 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.
@@ -1844,18 +1867,18 @@ class MessageContext {
 
     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.
+   * a null value).  `args` will be used to resolve references to variables
+   * passed as arguments to 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 }!');
@@ -1863,17 +1886,17 @@ class MessageContext {
    *     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>]
+   *     [<ReferenceError: Unknown variable: 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
--- a/intl/l10n/fluent.js.patch
+++ b/intl/l10n/fluent.js.patch
@@ -1,585 +1,566 @@
-diff -uNr ./dist/DOMLocalization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm
---- ./dist/DOMLocalization.jsm	2018-04-13 08:25:21.143138950 -0700
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm	2018-04-13 08:27:11.658083766 -0700
-@@ -18,10 +18,8 @@
+diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/DOMLocalization.jsm /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/DOMLocalization.jsm
+--- ./intl/l10n/DOMLocalization.jsm	2018-08-03 13:25:20.275840905 -0700
++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/DOMLocalization.jsm	2018-08-01 09:15:58.916763182 -0700
+@@ -16,10 +16,12 @@
+  */
  
- /* fluent-dom@0.2.0 */
  
--import Localization from '../../fluent-dom/src/localization.js';
--
--/* eslint no-console: ["error", {allow: ["warn"]}] */
--/* global console */
-+const { Localization } =
-+  ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
+-/* fluent-dom@cab517f (July 31, 2018) */
++/* fluent-dom@0.3.0 */
+ 
+-const { Localization } =
+-  ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
++import Localization from '../../fluent-dom/src/localization.js';
++
++/* eslint no-console: ["error", {allow: ["warn"]}] */
++/* global console */
  
  // Match the opening angle bracket (<) in HTML tags, and HTML entities like
  // &amp;, &#0038;, &#x0026;.
-@@ -96,6 +94,7 @@
+@@ -61,9 +63,7 @@
+     global: [
+       "accesskey", "aria-label", "aria-valuetext", "aria-moz-hint", "label"
+     ],
+-    description: ["value"],
+     key: ["key", "keycode"],
+-    label: ["value"],
+     textbox: ["placeholder"],
+     toolbarbutton: ["tooltiptext"],
+   }
+@@ -96,7 +96,6 @@
        const templateElement = element.ownerDocument.createElementNS(
          "http://www.w3.org/1999/xhtml", "template"
        );
-+      // eslint-disable-next-line no-unsanitized/property
+-      // eslint-disable-next-line no-unsanitized/property
        templateElement.innerHTML = value;
        overlayChildNodes(templateElement.content, element);
      }
-@@ -323,6 +322,46 @@
+@@ -350,46 +349,6 @@
    return toElement;
  }
  
-+/**
-+ * Sanitizes a translation before passing them to Node.localize API.
-+ *
-+ * It returns `false` if the translation contains DOM Overlays and should
-+ * not go into Node.localize.
-+ *
-+ * Note: There's a third item of work that JS DOM Overlays do - removal
-+ * of attributes from the previous translation.
-+ * This is not trivial to implement for Node.localize scenario, so
-+ * at the moment it is not supported.
-+ *
-+ * @param {{
-+ *          localName: string,
-+ *          namespaceURI: string,
-+ *          type: string || null
-+ *          l10nId: string,
-+ *          l10nArgs: Array<Object> || null,
-+ *          l10nAttrs: string ||null,
-+ *        }}                                     l10nItems
-+ * @param {{value: string, attrs: Object}} translations
-+ * @returns boolean
-+ * @private
-+ */
-+function sanitizeTranslationForNodeLocalize(l10nItem, translation) {
-+  if (reOverlay.test(translation.value)) {
-+    return false;
-+  }
-+
-+  if (translation.attributes) {
-+    const explicitlyAllowed = l10nItem.l10nAttrs === null ? null :
-+      l10nItem.l10nAttrs.split(",").map(i => i.trim());
-+    for (const [j, {name}] of translation.attributes.entries()) {
-+      if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) {
-+        translation.attributes.splice(j, 1);
-+      }
-+    }
-+  }
-+  return true;
-+}
-+
+-/**
+- * Sanitizes a translation before passing them to Node.localize API.
+- *
+- * It returns `false` if the translation contains DOM Overlays and should
+- * not go into Node.localize.
+- *
+- * Note: There's a third item of work that JS DOM Overlays do - removal
+- * of attributes from the previous translation.
+- * This is not trivial to implement for Node.localize scenario, so
+- * at the moment it is not supported.
+- *
+- * @param {{
+- *          localName: string,
+- *          namespaceURI: string,
+- *          type: string || null
+- *          l10nId: string,
+- *          l10nArgs: Array<Object> || null,
+- *          l10nAttrs: string ||null,
+- *        }}                                     l10nItems
+- * @param {{value: string, attrs: Object}} translations
+- * @returns boolean
+- * @private
+- */
+-function sanitizeTranslationForNodeLocalize(l10nItem, translation) {
+-  if (reOverlay.test(translation.value)) {
+-    return false;
+-  }
+-
+-  if (translation.attributes) {
+-    const explicitlyAllowed = l10nItem.l10nAttrs === null ? null :
+-      l10nItem.l10nAttrs.split(",").map(i => i.trim());
+-    for (const [j, {name}] of translation.attributes.entries()) {
+-      if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) {
+-        translation.attributes.splice(j, 1);
+-      }
+-    }
+-  }
+-  return true;
+-}
+-
  const L10NID_ATTR_NAME = "data-l10n-id";
  const L10NARGS_ATTR_NAME = "data-l10n-args";
  
-@@ -568,6 +607,59 @@
+@@ -530,6 +489,7 @@
+       );
+     }
+ 
++
+     this.roots.add(newRoot);
+     this.mutationObserver.observe(newRoot, this.observerConfig);
+   }
+@@ -639,10 +599,7 @@
+     if (this.pendingElements.size > 0) {
+       if (this.pendingrAF === null) {
+         this.pendingrAF = this.windowElement.requestAnimationFrame(() => {
+-          // We need to filter for elements that lost their l10n-id while
+-          // waiting for the animation frame.
+-          this.translateElements(Array.from(this.pendingElements)
+-            .filter(elem => elem.hasAttribute("data-l10n-id")));
++          this.translateElements(Array.from(this.pendingElements));
+           this.pendingElements.clear();
+           this.pendingrAF = null;
+         });
+@@ -664,63 +621,6 @@
     * @returns {Promise}
     */
    translateFragment(frag) {
-+    if (frag.localize) {
-+      // This is a temporary fast-path offered by Gecko to workaround performance
-+      // issues coming from Fluent and XBL+Stylo performing unnecesary
-+      // operations during startup.
-+      // For details see bug 1441037, bug 1442262, and bug 1363862.
-+
-+      // A sparse array which will store translations separated out from
-+      // all translations that is needed for DOM Overlay.
-+      const overlayTranslations = [];
-+
-+      const getTranslationsForItems = async l10nItems => {
-+        const keys = l10nItems.map(l10nItem => [l10nItem.l10nId, l10nItem.l10nArgs]);
-+        const translations = await this.formatMessages(keys);
-+
-+        // Here we want to separate out elements that require DOM Overlays.
-+        // Those elements will have to be translated using our JS
-+        // implementation, while everything else is going to use the fast-path.
-+        for (const [i, translation] of translations.entries()) {
-+          if (translation === undefined) {
-+            continue;
-+          }
-+
-+          const hasOnlyText =
-+            sanitizeTranslationForNodeLocalize(l10nItems[i], translation);
-+          if (!hasOnlyText) {
-+            // Removing from translations to make Node.localize skip it.
-+            // We will translate it below using JS DOM Overlays.
-+            overlayTranslations[i] = translations[i];
-+            translations[i] = undefined;
-+          }
-+        }
-+
-+        // We pause translation observing here because Node.localize
-+        // will translate the whole DOM next, using the `translations`.
-+        //
-+        // The observer will be resumed after DOM Overlays are localized
-+        // in the next microtask.
-+        this.pauseObserving();
-+        return translations;
-+      };
-+
-+      return frag.localize(getTranslationsForItems.bind(this))
-+        .then(untranslatedElements => {
-+          for (let i = 0; i < overlayTranslations.length; i++) {
-+            if (overlayTranslations[i] !== undefined &&
-+                untranslatedElements[i] !== undefined) {
-+              translateElement(untranslatedElements[i], overlayTranslations[i]);
-+            }
-+          }
-+          this.resumeObserving();
-+        })
-+        .catch(() => this.resumeObserving());
-+    }
+-    if (frag.localize) {
+-      // This is a temporary fast-path offered by Gecko to workaround performance
+-      // issues coming from Fluent and XBL+Stylo performing unnecesary
+-      // operations during startup.
+-      // For details see bug 1441037, bug 1442262, and bug 1363862.
+-
+-      // A sparse array which will store translations separated out from
+-      // all translations that is needed for DOM Overlay.
+-      const overlayTranslations = [];
+-
+-      const getTranslationsForItems = async l10nItems => {
+-        const keys = l10nItems.map(
+-          l10nItem => ({id: l10nItem.l10nId, args: l10nItem.l10nArgs}));
+-        const translations = await this.formatMessages(keys);
+-
+-        // Here we want to separate out elements that require DOM Overlays.
+-        // Those elements will have to be translated using our JS
+-        // implementation, while everything else is going to use the fast-path.
+-        for (const [i, translation] of translations.entries()) {
+-          if (translation === undefined) {
+-            continue;
+-          }
+-
+-          const hasOnlyText =
+-            sanitizeTranslationForNodeLocalize(l10nItems[i], translation);
+-          if (!hasOnlyText) {
+-            // Removing from translations to make Node.localize skip it.
+-            // We will translate it below using JS DOM Overlays.
+-            overlayTranslations[i] = translations[i];
+-            translations[i] = undefined;
+-          }
+-        }
+-
+-        // We pause translation observing here because Node.localize
+-        // will translate the whole DOM next, using the `translations`.
+-        //
+-        // The observer will be resumed after DOM Overlays are localized
+-        // in the next microtask.
+-        this.pauseObserving();
+-        return translations;
+-      };
+-
+-      return frag.localize(getTranslationsForItems.bind(this))
+-        .then(untranslatedElements => {
+-          for (let i = 0; i < overlayTranslations.length; i++) {
+-            if (overlayTranslations[i] !== undefined &&
+-                untranslatedElements[i] !== undefined) {
+-              translateElement(untranslatedElements[i], overlayTranslations[i]);
+-            }
+-          }
+-          this.resumeObserving();
+-        })
+-        .catch(e => {
+-          this.resumeObserving();
+-          throw e;
+-        });
+-    }
      return this.translateElements(this.getTranslatables(frag));
    }
  
-@@ -647,37 +739,5 @@
+@@ -800,5 +700,37 @@
    }
  }
  
--/* global L10nRegistry, Services */
+-this.DOMLocalization = DOMLocalization;
+-var EXPORTED_SYMBOLS = ["DOMLocalization"];
++/* 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.EXPORTED_SYMBOLS = ["DOMLocalization"];
+diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/l10n.js /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/l10n.js
+--- ./intl/l10n/l10n.js	2018-08-03 13:26:42.691527746 -0700
++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/l10n.js	2018-08-01 09:15:59.253432348 -0700
+@@ -1,6 +1,7 @@
++/* global Components, document, window */
+ {
+   const { DOMLocalization } =
+-    ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
++    Components.utils.import("resource://gre/modules/DOMLocalization.jsm");
+ 
+   /**
+    * Polyfill for document.ready polyfill.
+@@ -44,13 +45,16 @@
+ 
+   const resourceIds = getResourceLinks(document.head || document);
+ 
+-  document.l10n = new DOMLocalization(resourceIds);
++  document.l10n = new DOMLocalization(window, resourceIds);
+ 
+-  // Trigger the first two contexts to be loaded eagerly.
+-  document.l10n.ctxs.touchNext(2);
++  // trigger first context to be fetched eagerly
++  document.l10n.ctxs.touchNext();
+ 
+   document.l10n.ready = documentReady().then(() => {
+     document.l10n.registerObservers();
++    window.addEventListener("unload", () => {
++      document.l10n.unregisterObservers();
++    });
+     document.l10n.connectRoot(document.documentElement);
+     return document.l10n.translateRoots();
+   });
+diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/Localization.jsm /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/Localization.jsm
+--- ./intl/l10n/Localization.jsm	2018-08-03 13:20:57.417703171 -0700
++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/Localization.jsm	2018-08-01 09:15:58.546760435 -0700
+@@ -16,128 +16,11 @@
+  */
+ 
+ 
+-/* fluent-dom@cab517f (July 31, 2018) */
++/* fluent-dom@0.3.0 */
+ 
+-/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
+-/* global console */
+-
+-const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
+-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
+-const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
++import { CachedAsyncIterable } from 'cached-iterable';
+ 
+-class CachedIterable extends Array {
+-  /**
+-   * Create a `CachedIterable` instance from an iterable or, if another
+-   * instance of `CachedIterable` is passed, return it without any
+-   * modifications.
+-   *
+-   * @param {Iterable} iterable
+-   * @returns {CachedIterable}
+-   */
+-  static from(iterable) {
+-    if (iterable instanceof this) {
+-      return iterable;
+-    }
+-
+-    return new this(iterable);
+-  }
+-}
+-
+-class CachedAsyncIterable extends CachedIterable {
+-  /**
+-   * Create an `CachedAsyncIterable` instance.
+-   *
+-   * @param {Iterable} iterable
+-   * @returns {CachedAsyncIterable}
+-   */
+-  constructor(iterable) {
+-    super();
+-
+-    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.");
+-    }
+-  }
+-
+-  /**
+-   * Synchronous iterator over the cached elements.
+-   *
+-   * Return a generator object implementing the iterator protocol over the
+-   * cached elements of the original (async or sync) iterable.
+-   */
+-  [Symbol.iterator]() {
+-    const cached = this;
+-    let cur = 0;
+-
+-    return {
+-      next() {
+-        if (cached.length === cur) {
+-          return {value: undefined, done: true};
+-        }
+-        return cached[cur++];
+-      }
+-    };
+-  }
+-
+-  /**
+-   * Asynchronous iterator caching the yielded elements.
+-   *
+-   * Elements yielded by the original iterable will be cached and available
+-   * synchronously. Returns an async generator object implementing the
+-   * iterator protocol over the elements of the original (async or sync)
+-   * iterable.
+-   */
+-  [Symbol.asyncIterator]() {
+-    const cached = this;
+-    let cur = 0;
+-
+-    return {
+-      async next() {
+-        if (cached.length <= cur) {
+-          cached.push(await cached.iterator.next());
+-        }
+-        return cached[cur++];
+-      }
+-    };
+-  }
+-
+-  /**
+-   * This method allows user to consume the next element from the iterator
+-   * into the cache.
+-   *
+-   * @param {number} count - number of elements to consume
+-   */
+-  async touchNext(count = 1) {
+-    let idx = 0;
+-    while (idx++ < count) {
+-      const last = this[this.length - 1];
+-      if (last && last.done) {
+-        break;
+-      }
+-      this.push(await this.iterator.next());
+-    }
+-    // Return the last cached {value, done} object to allow the calling
+-    // code to decide if it needs to call touchNext again.
+-    return this[this.length - 1];
+-  }
+-}
 -
 -/**
 - * 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);
+-  const appLocales = Services.locale.getAppLocalesAsBCP47();
+-  return L10nRegistry.generateContexts(appLocales, resourceIds);
 -}
--
++/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
+ 
+ /**
+  * The `Localization` class is a central high-level API for vanilla
+@@ -153,7 +36,7 @@
+    *
+    * @returns {Localization}
+    */
+-  constructor(resourceIds = [], generateMessages = defaultGenerateMessages) {
++  constructor(resourceIds = [], generateMessages) {
+     this.resourceIds = resourceIds;
+     this.generateMessages = generateMessages;
+     this.ctxs = CachedAsyncIterable.from(
+@@ -194,12 +77,9 @@
+         break;
+       }
+ 
+-      if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) {
++      if (typeof console !== "undefined") {
+         const locale = ctx.locales[0];
+         const ids = Array.from(missingIds).join(", ");
+-        if (Cu.isInAutomation) {
+-          throw new Error(`Missing translations in ${locale}: ${ids}`);
+-        }
+         console.warn(`Missing translations in ${locale}: ${ids}`);
+       }
+     }
+@@ -284,35 +164,8 @@
+     return val;
+   }
+ 
+-  /**
+-   * Register weak observers on events that will trigger cache invalidation
+-   */
+-  registerObservers() {
+-    Services.obs.addObserver(this, "intl:app-locales-changed", true);
+-    Services.prefs.addObserver("intl.l10n.pseudo", this, true);
+-  }
 -
--class GeckoDOMLocalization extends DOMLocalization {
--  constructor(
--    windowElement,
--    resourceIds,
--    generateMessages = defaultGenerateMessages
--  ) {
--    super(windowElement, resourceIds, generateMessages);
--  }
--}
--
--this.DOMLocalization = GeckoDOMLocalization;
--this.EXPORTED_SYMBOLS = ["DOMLocalization"];
-+this.DOMLocalization = DOMLocalization;
-+var EXPORTED_SYMBOLS = ["DOMLocalization"];
-diff -uNr ./dist/l10n.js /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js
---- ./dist/l10n.js	2018-04-13 08:25:21.307139138 -0700
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js	2018-04-13 08:27:25.230296529 -0700
-@@ -1,20 +1,26 @@
--/* global Components, document, window */
- {
-   const { DOMLocalization } =
--    Components.utils.import("resource://gre/modules/DOMLocalization.jsm");
-+    ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
+-  /**
+-   * Default observer handler method.
+-   *
+-   * @param {String} subject
+-   * @param {String} topic
+-   * @param {Object} data
+-   */
+-  observe(subject, topic, data) {
+-    switch (topic) {
+-      case "intl:app-locales-changed":
+-        this.onChange();
+-        break;
+-      case "nsPref:changed":
+-        switch (data) {
+-          case "intl.l10n.pseudo":
+-            this.onChange();
+-        }
+-        break;
+-      default:
+-        break;
+-    }
++  handleEvent() {
++    this.onChange();
+   }
  
    /**
-    * Polyfill for document.ready polyfill.
-    * See: https://github.com/whatwg/html/issues/127 for details.
-    *
-+   * XXX: The callback is a temporary workaround for bug 1193394. Once Promises in Gecko
-+   *      start beeing a microtask and stop pushing translation post-layout, we can
-+   *      remove it and start using the returned Promise again.
-+   *
-+   * @param {Function} callback - function to be called when the document is ready.
-    * @returns {Promise}
-    */
--  function documentReady() {
-+  function documentReady(callback) {
-     if (document.contentType === "application/vnd.mozilla.xul+xml") {
-       // XUL
-       return new Promise(
-         resolve => document.addEventListener(
--          "MozBeforeInitialXULLayout", resolve, { once: true }
-+          "MozBeforeInitialXULLayout", () => {
-+            resolve(callback());
-+          }, { once: true }
-         )
-       );
-     }
-@@ -22,11 +28,13 @@
-     // HTML
-     const rs = document.readyState;
-     if (rs === "interactive" || rs === "completed") {
--      return Promise.resolve();
-+      return Promise.resolve(callback);
-     }
-     return new Promise(
-       resolve => document.addEventListener(
--        "readystatechange", resolve, { once: true }
-+        "readystatechange", () => {
-+          resolve(callback());
-+        }, { once: true }
-       )
-     );
-   }
-@@ -50,11 +58,8 @@
-   // trigger first context to be fetched eagerly
-   document.l10n.ctxs.touchNext();
- 
--  document.l10n.ready = documentReady().then(() => {
-+  document.l10n.ready = documentReady(() => {
-     document.l10n.registerObservers();
--    window.addEventListener("unload", () => {
--      document.l10n.unregisterObservers();
--    });
-     document.l10n.connectRoot(document.documentElement);
-     return document.l10n.translateRoots();
-   });
-diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm
---- ./dist/Localization.jsm	2018-04-13 08:25:20.946138732 -0700
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm	2018-04-13 08:27:16.396155987 -0700
-@@ -18,70 +18,13 @@
- 
- /* fluent-dom@0.2.0 */
- 
--/*  eslint no-magic-numbers: [0]  */
--
--/* global Intl */
--
--/**
-- * @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 { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
-+const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
-+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
-+const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
- 
- /*
-  * CachedIterable caches the elements yielded by an iterable.
-@@ -148,58 +91,19 @@
+@@ -326,10 +179,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);
-- *
-- */
+-Localization.prototype.QueryInterface = ChromeUtils.generateQI([
+-  Ci.nsISupportsWeakReference
+-]);
 -
--/*
-- * @module fluent
-- * @overview
-- *
-- * `fluent` is a JavaScript implementation of Project Fluent, a localization
-- * framework designed to unleash the expressive power of the natural language.
+ /**
+  * Format the value of a message into a string.
+  *
+@@ -449,5 +298,44 @@
+   return missingIds;
+ }
+ 
+-this.Localization = Localization;
+-var EXPORTED_SYMBOLS = ["Localization"];
++/* 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.
-  */
--
--/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
++ */
 +function defaultGenerateMessages(resourceIds) {
-+  const appLocales = Services.locale.getAppLocalesAsLangTags();
-+  return L10nRegistry.generateContexts(appLocales, 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);
 +}
- 
- /**
-  * The `Localization` class is a central high-level API for vanilla
-@@ -215,7 +119,7 @@
-    *
-    * @returns {Localization}
-    */
--  constructor(resourceIds, generateMessages) {
-+  constructor(resourceIds, generateMessages = defaultGenerateMessages) {
-     this.resourceIds = resourceIds;
-     this.generateMessages = generateMessages;
-     this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds));
-@@ -236,7 +140,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") {
-@@ -248,7 +152,7 @@
-         break;
-       }
- 
--      if (typeof console !== "undefined") {
-+      if (AppConstants.NIGHTLY_BUILD) {
-         const locale = ctx.locales[0];
-         const ids = Array.from(missingIds).join(", ");
-         console.warn(`Missing translations in ${locale}: ${ids}`);
-@@ -335,8 +239,28 @@
-     return val;
-   }
- 
--  handleEvent() {
--    this.onLanguageChange();
-+  /**
-+   * Register weak observers on events that will trigger cache invalidation
-+   */
-+  registerObservers() {
-+    Services.obs.addObserver(this, "intl:app-locales-changed", true);
-+  }
 +
-+  /**
-+   * Default observer handler method.
-+   *
-+   * @param {String} subject
-+   * @param {String} topic
-+   * @param {Object} data
-+   */
-+  observe(subject, topic, data) {
-+    switch (topic) {
-+      case "intl:app-locales-changed":
-+        this.onLanguageChange();
-+        break;
-+      default:
-+        break;
-+    }
-   }
- 
-   /**
-@@ -348,6 +272,10 @@
-   }
- }
- 
-+Localization.prototype.QueryInterface = XPCOMUtils.generateQI([
-+  Ci.nsISupportsWeakReference
-+]);
-+
- /**
-  * Format the value of a message into a string.
-  *
-@@ -368,6 +296,7 @@
-  */
- function valueFromContext(ctx, errors, id, args) {
-   const msg = ctx.getMessage(id);
++class GeckoLocalization extends Localization {
++  constructor(resourceIds, generateMessages = defaultGenerateMessages) {
++    super(resourceIds, generateMessages);
++  }
++}
 +
-   return ctx.format(msg, args, errors);
- }
- 
-@@ -467,44 +396,5 @@
-   return missingIds;
- }
- 
--/* 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.EXPORTED_SYMBOLS = ["Localization"];
-+this.Localization = Localization;
-+var EXPORTED_SYMBOLS = ["Localization"];
-diff -uNr ./dist/MessageContext.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm
---- ./dist/MessageContext.jsm	2018-04-13 08:25:20.698138486 -0700
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm	2018-04-13 08:27:20.944227388 -0700
++this.Localization = GeckoLocalization;
++this.EXPORTED_SYMBOLS = ["Localization"];
+diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/MessageContext.jsm /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/MessageContext.jsm
+--- ./intl/l10n/MessageContext.jsm	2018-08-03 13:11:36.949029757 -0700
++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/MessageContext.jsm	2018-08-01 09:15:58.176757688 -0700
 @@ -16,7 +16,7 @@
   */
  
  
--/* fluent-dom@0.2.0 */
-+/* fluent@0.6.3 */
+-/* fluent@0.6.0 */
++/* fluent-dom@0.3.0 */
  
  /*  eslint no-magic-numbers: [0]  */
  
-@@ -1858,63 +1858,5 @@
+@@ -1930,6 +1930,57 @@
    }
  }
  
--/*
-- * 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);
-- *
-- */
--
--/*
-- * @module fluent
-- * @overview
-- *
-- * `fluent` is a JavaScript implementation of Project Fluent, a localization
-- * framework designed to unleash the expressive power of the natural language.
-- *
-- */
--
++/*
++ * @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
++ * Cached{Sync|Async}Iterable.
++ * 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 CachedSyncIterable(generateMessages());
++ *     const ctx = mapContextSync(contexts, id);
++ *
++ */
++
++/*
++ * @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"];
-+var EXPORTED_SYMBOLS = ["MessageContext"];
+-this.FluentResource = FluentResource;
+-var EXPORTED_SYMBOLS = ["MessageContext", "FluentResource"];
++this.EXPORTED_SYMBOLS = ["MessageContext"];
--- a/intl/l10n/test/dom/test_domloc.xul
+++ b/intl/l10n/test/dom/test_domloc.xul
@@ -14,20 +14,20 @@
   const { DOMLocalization } =
     ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
 
   async function * generateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages(`
-file-menu
+file-menu =
     .label = File
     .accesskey = F
-new-tab
+new-tab =
     .label = New Tab
     .accesskey = N
 `);
     yield mc;
   }
 
   SimpleTest.waitForExplicitFinish();
 
--- a/intl/l10n/test/dom/test_domloc_translateElements.html
+++ b/intl/l10n/test/dom/test_domloc_translateElements.html
@@ -10,17 +10,17 @@
   const { DOMLocalization } =
     ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
 
   async function* mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages("title = Hello World");
-    mc.addMessages("link\n    .title = Click me");
+    mc.addMessages("link =\n    .title = Click me");
     yield mc;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       [],