Bug 1560038 - Switch uses of FluentBundle to use fluent-rs. r=jfkthame
authorZibi Braniecki <zbraniecki@mozilla.com>
Sat, 14 Mar 2020 22:14:13 +0000
changeset 518813 7eedc316b2c796d9249645bf8c752395ebec6697
parent 518812 bdaa29b4d55dbcdc44e1e8cbb3175b3ea537293a
child 518814 fffc6cfc5d9c3a19e40194a5829e9f7c876c83a0
push id37216
push usercsabou@mozilla.com
push dateSun, 15 Mar 2020 09:37:49 +0000
treeherdermozilla-central@7eedc316b2c7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjfkthame
bugs1560038
milestone76.0a1
first release with
nightly linux32
7eedc316b2c7 / 76.0a1 / 20200315093749 / files
nightly linux64
7eedc316b2c7 / 76.0a1 / 20200315093749 / files
nightly mac
7eedc316b2c7 / 76.0a1 / 20200315093749 / files
nightly win32
7eedc316b2c7 / 76.0a1 / 20200315093749 / files
nightly win64
7eedc316b2c7 / 76.0a1 / 20200315093749 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1560038 - Switch uses of FluentBundle to use fluent-rs. r=jfkthame Differential Revision: https://phabricator.services.mozilla.com/D57403
dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html
dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html
dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html
dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html
dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml
dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html
dom/l10n/tests/mochitest/dom_localization/test_overlay.html
dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html
dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html
dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html
dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html
dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html
dom/l10n/tests/mochitest/dom_localization/test_translateElements.html
dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html
dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html
intl/l10n/Fluent.jsm
intl/l10n/L10nRegistry.jsm
intl/l10n/README
intl/l10n/fluent.js.patch
intl/l10n/moz.build
intl/l10n/test/mochitest/localization/test_formatMessages.html
intl/l10n/test/mochitest/localization/test_formatValue.html
intl/l10n/test/mochitest/localization/test_formatValues.html
intl/l10n/test/test_messagecontext.js
tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js
tools/lint/eslint/modules.json
--- a/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's attr sanitization functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 key1 = Value for Key 1
 
 key2 = Value for <a>Key 2<a/>.
     `));
     yield bundle;
--- a/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization.prototype.connectRoot</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 key1 = Value for Key 1
     `));
     yield bundle;
   }
 
--- a/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html
@@ -39,19 +39,16 @@
 
         document.domLoc.setAttributes(label, "key1");
       }
     }
     customElements.define("fluent-widget", FluentWidget);
   </script>
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 key1 = Value for Key 1
     `));
     yield bundle;
   }
 
--- a/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization.prototype.disconnectRoot</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 key1 = Value for Key 1
 key2 = Value for Key 2
     `));
     yield bundle;
   }
--- a/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml
+++ b/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml
@@ -5,19 +5,16 @@
 
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="Testing DOMLocalization in XUL environment">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
   <script type="application/javascript">
   <![CDATA[
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function * generateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 file-menu =
     .label = File
     .accesskey = F
 new-tab =
     .label = New Tab
--- a/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's MutationObserver</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource("title = Hello World"));
     bundle.addResource(new FluentResource("title2 = Hello Another World"));
     yield bundle;
   }
 
   window.onload = async function() {
--- a/dom/l10n/tests/mochitest/dom_localization/test_overlay.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's DOMOverlay functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource("title = <strong>Hello</strong> World"));
     bundle.addResource(new FluentResource(`title2 = This is <a data-l10n-name="link">a link</a>!`));
     yield bundle;
   }
 
   window.onload = async function() {
--- a/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's DOMOverlay functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     // No translations!
     yield bundle;
   }
 
   SimpleTest.waitForExplicitFinish();
   addLoadEvent(async () => {
--- a/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's DOMOverlay functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`));
     yield bundle;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
--- a/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's DOMOverlay functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`));
     yield bundle;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
--- a/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's DOMOverlay functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 key1 =
     .href = https://www.hacked.com
 
 key2 =
     .href = https://pl.wikipedia.org
--- a/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization's matching l10nIds functionality</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource(`
 key1 = Translation For Key 1
 
 key2 = Visit <a data-l10n-name="link">this link<a/>.
     `));
     yield bundle;
--- a/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization.prototype.translateElements</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource("title = Hello World"));
     bundle.addResource(new FluentResource("link =\n    .title = Click me"));
     yield bundle;
   }
 
   window.onload = async function() {
--- a/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization.prototype.translateFragment</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource("title = Hello World"));
     bundle.addResource(new FluentResource("subtitle = Welcome to FluentBundle"));
     yield bundle;
   }
 
   window.onload = async function() {
--- a/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html
+++ b/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMLocalization.prototype.translateRoots</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US");
     bundle.addResource(new FluentResource("title = Hello World"));
     bundle.addResource(new FluentResource("title2 = Hello Another World"));
     yield bundle;
   }
 
   window.onload = async function() {
deleted file mode 100644
--- a/intl/l10n/Fluent.jsm
+++ /dev/null
@@ -1,1222 +0,0 @@
-/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
-
-/* Copyright 2019 Mozilla Foundation and others
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-/* fluent-bundle@0.14.1 */
-
-/* global Intl */
-
-/**
- * The `FluentType` class is the base of Fluent's type system.
- *
- * Fluent types wrap JavaScript values and store additional configuration for
- * them, which can then be used in the `toString` method together with a proper
- * `Intl` formatter.
- */
-class FluentType {
-  /**
-   * Create a `FluentType` instance.
-   *
-   * @param   {Any} value - JavaScript value to wrap.
-   * @returns {FluentType}
-   */
-  constructor(value) {
-    /** The wrapped native value. */
-    this.value = value;
-  }
-
-  /**
-   * Unwrap the raw value stored by this `FluentType`.
-   *
-   * @returns {Any}
-   */
-  valueOf() {
-    return this.value;
-  }
-
-  /**
-   * Format this instance of `FluentType` to a string.
-   *
-   * Formatted values are suitable for use outside of the `FluentBundle`.
-   * This method can use `Intl` formatters available through the `scope`
-   * argument.
-   *
-   * @abstract
-   * @param   {Scope} scope
-   * @returns {string}
-   */
-  toString(scope) { // eslint-disable-line no-unused-vars
-    throw new Error("Subclasses of FluentType must implement toString.");
-  }
-}
-
-/**
- * A `FluentType` representing no correct value.
- */
-class FluentNone extends FluentType {
-  /**
-   * Create an instance of `FluentNone` with an optional fallback value.
-   * @param   {string} value - The fallback value of this `FluentNone`.
-   * @returns {FluentType}
-   */
-  constructor(value = "???") {
-    super(value);
-  }
-
-  /**
-   * Format this `FluentNone` to the fallback string.
-   * @returns {string}
-   */
-  toString() {
-    return `{${this.value}}`;
-  }
-}
-
-/**
- * A `FluentType` representing a number.
- */
-class FluentNumber extends FluentType {
-  /**
-   * Create an instance of `FluentNumber` with options to the
-   * `Intl.NumberFormat` constructor.
-   * @param   {number} value
-   * @param   {Intl.NumberFormatOptions} opts
-   * @returns {FluentType}
-   */
-  constructor(value, opts) {
-    super(value);
-    /** Options passed to Intl.NumberFormat. */
-    this.opts = opts;
-  }
-
-  /**
-   * Format this `FluentNumber` to a string.
-   * @param   {Scope} scope
-   * @returns {string}
-   */
-  toString(scope) {
-    try {
-      const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts);
-      return nf.format(this.value);
-    } catch (err) {
-      scope.reportError(err);
-      return this.value.toString(10);
-    }
-  }
-}
-
-/**
- * A `FluentType` representing a date and time.
- */
-class FluentDateTime extends FluentType {
-  /**
-   * Create an instance of `FluentDateTime` with options to the
-   * `Intl.DateTimeFormat` constructor.
-   * @param   {number} value - timestamp in milliseconds
-   * @param   {Intl.DateTimeFormatOptions} opts
-   * @returns {FluentType}
-   */
-  constructor(value, opts) {
-    super(value);
-    /** Options passed to Intl.DateTimeFormat. */
-    this.opts = opts;
-  }
-
-  /**
-   * Format this `FluentDateTime` to a string.
-   * @param   {Scope} scope
-   * @returns {string}
-   */
-  toString(scope) {
-    try {
-      const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
-      return dtf.format(this.value);
-    } catch (err) {
-      scope.reportError(err);
-      return (new Date(this.value)).toISOString();
-    }
-  }
-}
-
-/**
- * @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.
- */
-
-function merge(argopts, opts) {
-  return Object.assign({}, argopts, values(opts));
-}
-
-function values(opts) {
-  const unwrapped = {};
-  for (const [name, opt] of Object.entries(opts)) {
-    unwrapped[name] = opt.valueOf();
-  }
-  return unwrapped;
-}
-
-function NUMBER([arg], opts) {
-  if (arg instanceof FluentNone) {
-    return new FluentNone(`NUMBER(${arg.valueOf()})`);
-  }
-
-  let value = Number(arg.valueOf());
-  if (Number.isNaN(value)) {
-    throw new TypeError("Invalid argument to NUMBER");
-  }
-
-  return new FluentNumber(value, merge(arg.opts, opts));
-}
-
-function DATETIME([arg], opts) {
-  if (arg instanceof FluentNone) {
-    return new FluentNone(`DATETIME(${arg.valueOf()})`);
-  }
-
-  let value = Number(arg.valueOf());
-  if (Number.isNaN(value)) {
-    throw new TypeError("Invalid argument to DATETIME");
-  }
-
-  return new FluentDateTime(value, merge(arg.opts, opts));
-}
-
-const builtins = /*#__PURE__*/Object.freeze({
-  __proto__: null,
-  NUMBER: NUMBER,
-  DATETIME: DATETIME
-});
-
-/* global Intl */
-
-// The maximum number of placeables which can be expanded in a single call to
-// `formatPattern`. The limit protects against the Billion Laughs and Quadratic
-// Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
-const MAX_PLACEABLES = 100;
-
-// Unicode bidi isolation characters.
-const FSI = "\u2068";
-const PDI = "\u2069";
-
-
-// Helper: match a variant key to the given selector.
-function match(scope, selector, key) {
-  if (key === selector) {
-    // Both are strings.
-    return true;
-  }
-
-  // XXX Consider comparing options too, e.g. minimumFractionDigits.
-  if (key instanceof FluentNumber
-    && selector instanceof FluentNumber
-    && key.value === selector.value) {
-    return true;
-  }
-
-  if (selector instanceof FluentNumber && typeof key === "string") {
-    let category = scope
-      .memoizeIntlObject(Intl.PluralRules, selector.opts)
-      .select(selector.value);
-    if (key === category) {
-      return true;
-    }
-  }
-
-  return false;
-}
-
-// Helper: resolve the default variant from a list of variants.
-function getDefault(scope, variants, star) {
-  if (variants[star]) {
-    return resolvePattern(scope, variants[star].value);
-  }
-
-  scope.reportError(new RangeError("No default"));
-  return new FluentNone();
-}
-
-// Helper: resolve arguments to a call expression.
-function getArguments(scope, args) {
-  const positional = [];
-  const named = Object.create(null);
-
-  for (const arg of args) {
-    if (arg.type === "narg") {
-      named[arg.name] = resolveExpression(scope, arg.value);
-    } else {
-      positional.push(resolveExpression(scope, arg));
-    }
-  }
-
-  return {positional, named};
-}
-
-// Resolve an expression to a Fluent type.
-function resolveExpression(scope, expr) {
-  switch (expr.type) {
-    case "str":
-      return expr.value;
-    case "num":
-      return new FluentNumber(expr.value, {
-        minimumFractionDigits: expr.precision,
-      });
-    case "var":
-      return VariableReference(scope, expr);
-    case "mesg":
-      return MessageReference(scope, expr);
-    case "term":
-      return TermReference(scope, expr);
-    case "func":
-      return FunctionReference(scope, expr);
-    case "select":
-      return SelectExpression(scope, expr);
-    default:
-      return new FluentNone();
-  }
-}
-
-// Resolve a reference to a variable.
-function VariableReference(scope, {name}) {
-  let arg;
-  if (scope.params) {
-    // We're inside a TermReference. It's OK to reference undefined parameters.
-    if (Object.prototype.hasOwnProperty.call(scope.params, name)) {
-      arg = scope.params[name];
-    } else {
-      return new FluentNone(`$${name}`);
-    }
-  } else if (
-    scope.args
-    && Object.prototype.hasOwnProperty.call(scope.args, name)
-  ) {
-    // We're in the top-level Pattern or inside a MessageReference. Missing
-    // variables references produce ReferenceErrors.
-    arg = scope.args[name];
-  } else {
-    scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
-    return new FluentNone(`$${name}`);
-  }
-
-  // Return early if the argument already is an instance of FluentType.
-  if (arg instanceof FluentType) {
-    return arg;
-  }
-
-  // Convert the argument to a Fluent type.
-  switch (typeof arg) {
-    case "string":
-      return arg;
-    case "number":
-      return new FluentNumber(arg);
-    case "object":
-      if (arg instanceof Date) {
-        return new FluentDateTime(arg.getTime());
-      }
-    default:
-      scope.reportError(
-        new TypeError(`Variable type not supported: $${name}, ${typeof arg}`)
-      );
-      return new FluentNone(`$${name}`);
-  }
-}
-
-// Resolve a reference to another message.
-function MessageReference(scope, {name, attr}) {
-  const message = scope.bundle._messages.get(name);
-  if (!message) {
-    scope.reportError(new ReferenceError(`Unknown message: ${name}`));
-    return new FluentNone(name);
-  }
-
-  if (attr) {
-    const attribute = message.attributes[attr];
-    if (attribute) {
-      return resolvePattern(scope, attribute);
-    }
-    scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
-    return new FluentNone(`${name}.${attr}`);
-  }
-
-  if (message.value) {
-    return resolvePattern(scope, message.value);
-  }
-
-  scope.reportError(new ReferenceError(`No value: ${name}`));
-  return new FluentNone(name);
-}
-
-// Resolve a call to a Term with key-value arguments.
-function TermReference(scope, {name, attr, args}) {
-  const id = `-${name}`;
-  const term = scope.bundle._terms.get(id);
-  if (!term) {
-    scope.reportError(new ReferenceError(`Unknown term: ${id}`));
-    return new FluentNone(id);
-  }
-
-  if (attr) {
-    const attribute = term.attributes[attr];
-    if (attribute) {
-      // Every TermReference has its own variables.
-      scope.params = getArguments(scope, args).named;
-      const resolved = resolvePattern(scope, attribute);
-      scope.params = null;
-      return resolved;
-    }
-    scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
-    return new FluentNone(`${id}.${attr}`);
-  }
-
-  scope.params = getArguments(scope, args).named;
-  const resolved = resolvePattern(scope, term.value);
-  scope.params = null;
-  return resolved;
-}
-
-// Resolve a call to a Function with positional and key-value arguments.
-function FunctionReference(scope, {name, args}) {
-  // Some functions are built-in. Others may be provided by the runtime via
-  // the `FluentBundle` constructor.
-  const func = scope.bundle._functions[name] || builtins[name];
-  if (!func) {
-    scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
-    return new FluentNone(`${name}()`);
-  }
-
-  if (typeof func !== "function") {
-    scope.reportError(new TypeError(`Function ${name}() is not callable`));
-    return new FluentNone(`${name}()`);
-  }
-
-  try {
-    let resolved = getArguments(scope, args);
-    return func(resolved.positional, resolved.named);
-  } catch (err) {
-    scope.reportError(err);
-    return new FluentNone(`${name}()`);
-  }
-}
-
-// Resolve a select expression to the member object.
-function SelectExpression(scope, {selector, variants, star}) {
-  let sel = resolveExpression(scope, selector);
-  if (sel instanceof FluentNone) {
-    return getDefault(scope, variants, star);
-  }
-
-  // Match the selector against keys of each variant, in order.
-  for (const variant of variants) {
-    const key = resolveExpression(scope, variant.key);
-    if (match(scope, sel, key)) {
-      return resolvePattern(scope, variant.value);
-    }
-  }
-
-  return getDefault(scope, variants, star);
-}
-
-// Resolve a pattern (a complex string with placeables).
-function resolveComplexPattern(scope, ptn) {
-  if (scope.dirty.has(ptn)) {
-    scope.reportError(new RangeError("Cyclic reference"));
-    return new FluentNone();
-  }
-
-  // Tag the pattern as dirty for the purpose of the current resolution.
-  scope.dirty.add(ptn);
-  const result = [];
-
-  // Wrap interpolations with Directional Isolate Formatting characters
-  // only when the pattern has more than one element.
-  const useIsolating = scope.bundle._useIsolating && ptn.length > 1;
-
-  for (const elem of ptn) {
-    if (typeof elem === "string") {
-      result.push(scope.bundle._transform(elem));
-      continue;
-    }
-
-    scope.placeables++;
-    if (scope.placeables > MAX_PLACEABLES) {
-      scope.dirty.delete(ptn);
-      // This is a fatal error which causes the resolver to instantly bail out
-      // on this pattern. The length check protects against excessive memory
-      // usage, and throwing protects against eating up the CPU when long
-      // placeables are deeply nested.
-      throw new RangeError(
-        `Too many placeables expanded: ${scope.placeables}, ` +
-        `max allowed is ${MAX_PLACEABLES}`
-      );
-    }
-
-    if (useIsolating) {
-      result.push(FSI);
-    }
-
-    result.push(resolveExpression(scope, elem).toString(scope));
-
-    if (useIsolating) {
-      result.push(PDI);
-    }
-  }
-
-  scope.dirty.delete(ptn);
-  return result.join("");
-}
-
-// Resolve a simple or a complex Pattern to a FluentString (which is really the
-// string primitive).
-function resolvePattern(scope, node) {
-  // Resolve a simple pattern.
-  if (typeof node === "string") {
-    return scope.bundle._transform(node);
-  }
-
-  return resolveComplexPattern(scope, node);
-}
-
-class Scope {
-  constructor(bundle, errors, args) {
-    /** The bundle for which the given resolution is happening. */
-    this.bundle = bundle;
-    /** The list of errors collected while resolving. */
-    this.errors = errors;
-    /** A dict of developer-provided variables. */
-    this.args = args;
-
-    /** The Set of patterns already encountered during this resolution.
-      * Used to detect and prevent cyclic resolutions. */
-    this.dirty = new WeakSet();
-    /** A dict of parameters passed to a TermReference. */
-    this.params = null;
-    /** The running count of placeables resolved so far. Used to detect the
-      * Billion Laughs and Quadratic Blowup attacks. */
-    this.placeables = 0;
-  }
-
-  reportError(error) {
-    if (!this.errors) {
-      throw error;
-    }
-    this.errors.push(error);
-  }
-
-  memoizeIntlObject(ctor, opts) {
-    let cache = this.bundle._intls.get(ctor);
-    if (!cache) {
-      cache = {};
-      this.bundle._intls.set(ctor, cache);
-    }
-    let id = JSON.stringify(opts);
-    if (!cache[id]) {
-      cache[id] = new ctor(this.bundle.locales, opts);
-    }
-    return cache[id];
-  }
-}
-
-/**
- * Message bundles are single-language stores of translation resources. They are
- * responsible for formatting message values and attributes to strings.
- */
-class FluentBundle {
-  /**
-   * Create an instance of `FluentBundle`.
-   *
-   * The `locales` argument is used to instantiate `Intl` formatters used by
-   * translations. The `options` object can be used to configure the bundle.
-   *
-   * Examples:
-   *
-   *     let bundle = new FluentBundle(["en-US", "en"]);
-   *
-   *     let bundle = new FluentBundle(locales, {useIsolating: false});
-   *
-   *     let bundle = new FluentBundle(locales, {
-   *       useIsolating: true,
-   *       functions: {
-   *         NODE_ENV: () => process.env.NODE_ENV
-   *       }
-   *     });
-   *
-   * Available options:
-   *
-   *   - `functions` - an object of additional functions available to
-   *     translations as builtins.
-   *
-   *   - `useIsolating` - boolean specifying whether to use Unicode isolation
-   *     marks (FSI, PDI) for bidi interpolations. Default: `true`.
-   *
-   *   - `transform` - a function used to transform string parts of patterns.
-   *
-   * @param   {(string|Array.<string>)} locales - The locales of the bundle
-   * @param   {Object} [options]
-   * @returns {FluentBundle}
-   */
-  constructor(locales, {
-    functions = {},
-    useIsolating = true,
-    transform = v => v,
-  } = {}) {
-    this.locales = Array.isArray(locales) ? locales : [locales];
-
-    this._terms = new Map();
-    this._messages = new Map();
-    this._functions = functions;
-    this._useIsolating = useIsolating;
-    this._transform = transform;
-    this._intls = new WeakMap();
-  }
-
-  /**
-   * Check if a message is present in the bundle.
-   *
-   * @param {string} id - The identifier of the message to check.
-   * @returns {bool}
-   */
-  hasMessage(id) {
-    return this._messages.has(id);
-  }
-
-  /**
-   * Return a raw unformatted message object from the bundle.
-   *
-   * Raw messages are `{value, attributes}` shapes containing translation units
-   * called `Patterns`. `Patterns` are implementation-specific; they should be
-   * treated as black boxes and formatted with `FluentBundle.formatPattern`.
-   *
-   *     interface RawMessage {
-   *         value: Pattern | null;
-   *         attributes: Record<string, Pattern>;
-   *     }
-   *
-   * @param {string} id - The identifier of the message to check.
-   * @returns {{value: ?Pattern, attributes: Object.<string, Pattern>}}
-   */
-  getMessage(id) {
-    return this._messages.get(id);
-  }
-
-  /**
-   * Add a translation resource to the bundle.
-   *
-   * The translation resource must be an instance of `FluentResource`.
-   *
-   *     let res = new FluentResource("foo = Foo");
-   *     bundle.addResource(res);
-   *     bundle.getMessage("foo");
-   *     // → {value: .., attributes: {..}}
-   *
-   * Available options:
-   *
-   *   - `allowOverrides` - boolean specifying whether it's allowed to override
-   *     an existing message or term with a new value. Default: `false`.
-   *
-   * @param   {FluentResource} res - FluentResource object.
-   * @param   {Object} [options]
-   * @returns {Array.<FluentError>}
-   */
-  addResource(res, {
-    allowOverrides = false,
-  } = {}) {
-    const errors = [];
-
-    for (let i = 0; i < res.body.length; i++) {
-      let entry = res.body[i];
-      if (entry.id.startsWith("-")) {
-        // Identifiers starting with a dash (-) define terms. Terms are private
-        // and cannot be retrieved from FluentBundle.
-        if (allowOverrides === false && this._terms.has(entry.id)) {
-          errors.push(`Attempt to override an existing term: "${entry.id}"`);
-          continue;
-        }
-        this._terms.set(entry.id, entry);
-      } else {
-        if (allowOverrides === false && this._messages.has(entry.id)) {
-          errors.push(`Attempt to override an existing message: "${entry.id}"`);
-          continue;
-        }
-        this._messages.set(entry.id, entry);
-      }
-    }
-
-    return errors;
-  }
-
-  /**
-   * Format a `Pattern` to a string.
-   *
-   * Format a raw `Pattern` into a string. `args` will be used to resolve
-   * references to variables passed as arguments to the translation.
-   *
-   * In case of errors `formatPattern` 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.
-   *
-   *     let errors = [];
-   *     bundle.addResource(
-   *         new FluentResource("hello = Hello, {$name}!"));
-   *
-   *     let hello = bundle.getMessage("hello");
-   *     if (hello.value) {
-   *         bundle.formatPattern(hello.value, {name: "Jane"}, errors);
-   *         // Returns "Hello, Jane!" and `errors` is empty.
-   *
-   *         bundle.formatPattern(hello.value, undefined, errors);
-   *         // Returns "Hello, {$name}!" and `errors` is now:
-   *         // [<ReferenceError: Unknown variable: name>]
-   *     }
-   *
-   * If `errors` is omitted, the first encountered error will be thrown.
-   *
-   * @param   {Pattern} pattern
-   * @param   {?Object} args
-   * @param   {?Array.<Error>} errors
-   * @returns {string}
-   */
-  formatPattern(pattern, args, errors) {
-    // Resolve a simple pattern without creating a scope. No error handling is
-    // required; by definition simple patterns don't have placeables.
-    if (typeof pattern === "string") {
-      return this._transform(pattern);
-    }
-
-    // Resolve a complex pattern.
-    let scope = new Scope(this, errors, args);
-    try {
-      let value = resolveComplexPattern(scope, pattern);
-      return value.toString(scope);
-    } catch (err) {
-      if (scope.errors) {
-        scope.errors.push(err);
-        return new FluentNone().toString(scope);
-      }
-      throw err;
-    }
-  }
-}
-
-class FluentError extends Error {}
-
-// This regex is used to iterate through the beginnings of messages and terms.
-// With the /m flag, the ^ matches at the beginning of every line.
-const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */mg;
-
-// Both Attributes and Variants are parsed in while loops. These regexes are
-// used to break out of them.
-const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y;
-const RE_VARIANT_START = /\*?\[/y;
-
-const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y;
-const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y;
-const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y;
-const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/;
-
-// A "run" is a sequence of text or string literal characters which don't
-// require any special handling. For TextElements such special characters are: {
-// (starts a placeable), and line breaks which require additional logic to check
-// if the next line is indented. For StringLiterals they are: \ (starts an
-// escape sequence), " (ends the literal), and line breaks which are not allowed
-// in StringLiterals. Note that string runs may be empty; text runs may not.
-const RE_TEXT_RUN = /([^{}\n\r]+)/y;
-const RE_STRING_RUN = /([^\\"\n\r]*)/y;
-
-// Escape sequences.
-const RE_STRING_ESCAPE = /\\([\\"])/y;
-const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y;
-
-// Used for trimming TextElements and indents.
-const RE_LEADING_NEWLINES = /^\n+/;
-const RE_TRAILING_SPACES = / +$/;
-// Used in makeIndent to strip spaces from blank lines and normalize CRLF to LF.
-const RE_BLANK_LINES = / *\r?\n/g;
-// Used in makeIndent to measure the indentation.
-const RE_INDENT = /( *)$/;
-
-// Common tokens.
-const TOKEN_BRACE_OPEN = /{\s*/y;
-const TOKEN_BRACE_CLOSE = /\s*}/y;
-const TOKEN_BRACKET_OPEN = /\[\s*/y;
-const TOKEN_BRACKET_CLOSE = /\s*] */y;
-const TOKEN_PAREN_OPEN = /\s*\(\s*/y;
-const TOKEN_ARROW = /\s*->\s*/y;
-const TOKEN_COLON = /\s*:\s*/y;
-// Note the optional comma. As a deviation from the Fluent EBNF, the parser
-// doesn't enforce commas between call arguments.
-const TOKEN_COMMA = /\s*,?\s*/y;
-const TOKEN_BLANK = /\s+/y;
-
-/**
- * Fluent Resource is a structure storing parsed localization entries.
- */
-class FluentResource {
-  constructor(source) {
-    this.body = this._parse(source);
-  }
-
-  _parse(source) {
-    RE_MESSAGE_START.lastIndex = 0;
-
-    let resource = [];
-    let cursor = 0;
-
-    // Iterate over the beginnings of messages and terms to efficiently skip
-    // comments and recover from errors.
-    while (true) {
-      let next = RE_MESSAGE_START.exec(source);
-      if (next === null) {
-        break;
-      }
-
-      cursor = RE_MESSAGE_START.lastIndex;
-      try {
-        resource.push(parseMessage(next[1]));
-      } catch (err) {
-        if (err instanceof FluentError) {
-          // Don't report any Fluent syntax errors. Skip directly to the
-          // beginning of the next message or term.
-          continue;
-        }
-        throw err;
-      }
-    }
-
-    return resource;
-
-    // The parser implementation is inlined below for performance reasons,
-    // as well as for convenience of accessing `source` and `cursor`.
-
-    // The parser focuses on minimizing the number of false negatives at the
-    // expense of increasing the risk of false positives. In other words, it
-    // aims at parsing valid Fluent messages with a success rate of 100%, but it
-    // may also parse a few invalid messages which the reference parser would
-    // reject. The parser doesn't perform any validation and may produce entries
-    // which wouldn't make sense in the real world. For best results users are
-    // advised to validate translations with the fluent-syntax parser
-    // pre-runtime.
-
-    // The parser makes an extensive use of sticky regexes which can be anchored
-    // to any offset of the source string without slicing it. Errors are thrown
-    // to bail out of parsing of ill-formed messages.
-
-    function test(re) {
-      re.lastIndex = cursor;
-      return re.test(source);
-    }
-
-    // Advance the cursor by the char if it matches. May be used as a predicate
-    // (was the match found?) or, if errorClass is passed, as an assertion.
-    function consumeChar(char, errorClass) {
-      if (source[cursor] === char) {
-        cursor++;
-        return true;
-      }
-      if (errorClass) {
-        throw new errorClass(`Expected ${char}`);
-      }
-      return false;
-    }
-
-    // Advance the cursor by the token if it matches. May be used as a predicate
-    // (was the match found?) or, if errorClass is passed, as an assertion.
-    function consumeToken(re, errorClass) {
-      if (test(re)) {
-        cursor = re.lastIndex;
-        return true;
-      }
-      if (errorClass) {
-        throw new errorClass(`Expected ${re.toString()}`);
-      }
-      return false;
-    }
-
-    // Execute a regex, advance the cursor, and return all capture groups.
-    function match(re) {
-      re.lastIndex = cursor;
-      let result = re.exec(source);
-      if (result === null) {
-        throw new FluentError(`Expected ${re.toString()}`);
-      }
-      cursor = re.lastIndex;
-      return result;
-    }
-
-    // Execute a regex, advance the cursor, and return the capture group.
-    function match1(re) {
-      return match(re)[1];
-    }
-
-    function parseMessage(id) {
-      let value = parsePattern();
-      let attributes = parseAttributes();
-
-      if (value === null && Object.keys(attributes).length === 0) {
-        throw new FluentError("Expected message value or attributes");
-      }
-
-      return {id, value, attributes};
-    }
-
-    function parseAttributes() {
-      let attrs = Object.create(null);
-
-      while (test(RE_ATTRIBUTE_START)) {
-        let name = match1(RE_ATTRIBUTE_START);
-        let value = parsePattern();
-        if (value === null) {
-          throw new FluentError("Expected attribute value");
-        }
-        attrs[name] = value;
-      }
-
-      return attrs;
-    }
-
-    function parsePattern() {
-      // First try to parse any simple text on the same line as the id.
-      if (test(RE_TEXT_RUN)) {
-        var first = match1(RE_TEXT_RUN);
-      }
-
-      // If there's a placeable on the first line, parse a complex pattern.
-      if (source[cursor] === "{" || source[cursor] === "}") {
-        // Re-use the text parsed above, if possible.
-        return parsePatternElements(first ? [first] : [], Infinity);
-      }
-
-      // RE_TEXT_VALUE stops at newlines. Only continue parsing the pattern if
-      // what comes after the newline is indented.
-      let indent = parseIndent();
-      if (indent) {
-        if (first) {
-          // If there's text on the first line, the blank block is part of the
-          // translation content in its entirety.
-          return parsePatternElements([first, indent], indent.length);
-        }
-        // Otherwise, we're dealing with a block pattern, i.e. a pattern which
-        // starts on a new line. Discrad the leading newlines but keep the
-        // inline indent; it will be used by the dedentation logic.
-        indent.value = trim(indent.value, RE_LEADING_NEWLINES);
-        return parsePatternElements([indent], indent.length);
-      }
-
-      if (first) {
-        // It was just a simple inline text after all.
-        return trim(first, RE_TRAILING_SPACES);
-      }
-
-      return null;
-    }
-
-    // Parse a complex pattern as an array of elements.
-    function parsePatternElements(elements = [], commonIndent) {
-      while (true) {
-        if (test(RE_TEXT_RUN)) {
-          elements.push(match1(RE_TEXT_RUN));
-          continue;
-        }
-
-        if (source[cursor] === "{") {
-          elements.push(parsePlaceable());
-          continue;
-        }
-
-        if (source[cursor] === "}") {
-          throw new FluentError("Unbalanced closing brace");
-        }
-
-        let indent = parseIndent();
-        if (indent) {
-          elements.push(indent);
-          commonIndent = Math.min(commonIndent, indent.length);
-          continue;
-        }
-
-        break;
-      }
-
-      let lastIndex = elements.length - 1;
-      // Trim the trailing spaces in the last element if it's a TextElement.
-      if (typeof elements[lastIndex] === "string") {
-        elements[lastIndex] = trim(elements[lastIndex], RE_TRAILING_SPACES);
-      }
-
-      let baked = [];
-      for (let element of elements) {
-        if (element.type === "indent") {
-          // Dedent indented lines by the maximum common indent.
-          element = element.value.slice(0, element.value.length - commonIndent);
-        }
-        if (element) {
-          baked.push(element);
-        }
-      }
-      return baked;
-    }
-
-    function parsePlaceable() {
-      consumeToken(TOKEN_BRACE_OPEN, FluentError);
-
-      let selector = parseInlineExpression();
-      if (consumeToken(TOKEN_BRACE_CLOSE)) {
-        return selector;
-      }
-
-      if (consumeToken(TOKEN_ARROW)) {
-        let variants = parseVariants();
-        consumeToken(TOKEN_BRACE_CLOSE, FluentError);
-        return {type: "select", selector, ...variants};
-      }
-
-      throw new FluentError("Unclosed placeable");
-    }
-
-    function parseInlineExpression() {
-      if (source[cursor] === "{") {
-        // It's a nested placeable.
-        return parsePlaceable();
-      }
-
-      if (test(RE_REFERENCE)) {
-        let [, sigil, name, attr = null] = match(RE_REFERENCE);
-
-        if (sigil === "$") {
-          return {type: "var", name};
-        }
-
-        if (consumeToken(TOKEN_PAREN_OPEN)) {
-          let args = parseArguments();
-
-          if (sigil === "-") {
-            // A parameterized term: -term(...).
-            return {type: "term", name, attr, args};
-          }
-
-          if (RE_FUNCTION_NAME.test(name)) {
-            return {type: "func", name, args};
-          }
-
-          throw new FluentError("Function names must be all upper-case");
-        }
-
-        if (sigil === "-") {
-          // A non-parameterized term: -term.
-          return {type: "term", name, attr, args: []};
-        }
-
-        return {type: "mesg", name, attr};
-      }
-
-      return parseLiteral();
-    }
-
-    function parseArguments() {
-      let args = [];
-      while (true) {
-        switch (source[cursor]) {
-          case ")": // End of the argument list.
-            cursor++;
-            return args;
-          case undefined: // EOF
-            throw new FluentError("Unclosed argument list");
-        }
-
-        args.push(parseArgument());
-        // Commas between arguments are treated as whitespace.
-        consumeToken(TOKEN_COMMA);
-      }
-    }
-
-    function parseArgument() {
-      let expr = parseInlineExpression();
-      if (expr.type !== "mesg") {
-        return expr;
-      }
-
-      if (consumeToken(TOKEN_COLON)) {
-        // The reference is the beginning of a named argument.
-        return {type: "narg", name: expr.name, value: parseLiteral()};
-      }
-
-      // It's a regular message reference.
-      return expr;
-    }
-
-    function parseVariants() {
-      let variants = [];
-      let count = 0;
-      let star;
-
-      while (test(RE_VARIANT_START)) {
-        if (consumeChar("*")) {
-          star = count;
-        }
-
-        let key = parseVariantKey();
-        let value = parsePattern();
-        if (value === null) {
-          throw new FluentError("Expected variant value");
-        }
-        variants[count++] = {key, value};
-      }
-
-      if (count === 0) {
-        return null;
-      }
-
-      if (star === undefined) {
-        throw new FluentError("Expected default variant");
-      }
-
-      return {variants, star};
-    }
-
-    function parseVariantKey() {
-      consumeToken(TOKEN_BRACKET_OPEN, FluentError);
-      let key = test(RE_NUMBER_LITERAL)
-        ? parseNumberLiteral()
-        : {type: "str", value: match1(RE_IDENTIFIER)};
-      consumeToken(TOKEN_BRACKET_CLOSE, FluentError);
-      return key;
-    }
-
-    function parseLiteral() {
-      if (test(RE_NUMBER_LITERAL)) {
-        return parseNumberLiteral();
-      }
-
-      if (source[cursor] === "\"") {
-        return parseStringLiteral();
-      }
-
-      throw new FluentError("Invalid expression");
-    }
-
-    function parseNumberLiteral() {
-      let [, value, fraction = ""] = match(RE_NUMBER_LITERAL);
-      let precision = fraction.length;
-      return {type: "num", value: parseFloat(value), precision};
-    }
-
-    function parseStringLiteral() {
-      consumeChar("\"", FluentError);
-      let value = "";
-      while (true) {
-        value += match1(RE_STRING_RUN);
-
-        if (source[cursor] === "\\") {
-          value += parseEscapeSequence();
-          continue;
-        }
-
-        if (consumeChar("\"")) {
-          return {type: "str", value};
-        }
-
-        // We've reached an EOL of EOF.
-        throw new FluentError("Unclosed string literal");
-      }
-    }
-
-    // Unescape known escape sequences.
-    function parseEscapeSequence() {
-      if (test(RE_STRING_ESCAPE)) {
-        return match1(RE_STRING_ESCAPE);
-      }
-
-      if (test(RE_UNICODE_ESCAPE)) {
-        let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE);
-        let codepoint = parseInt(codepoint4 || codepoint6, 16);
-        return codepoint <= 0xD7FF || 0xE000 <= codepoint
-          // It's a Unicode scalar value.
-          ? String.fromCodePoint(codepoint)
-          // Lonely surrogates can cause trouble when the parsing result is
-          // saved using UTF-8. Use U+FFFD REPLACEMENT CHARACTER instead.
-          : "�";
-      }
-
-      throw new FluentError("Unknown escape sequence");
-    }
-
-    // Parse blank space. Return it if it looks like indent before a pattern
-    // line. Skip it othwerwise.
-    function parseIndent() {
-      let start = cursor;
-      consumeToken(TOKEN_BLANK);
-
-      // Check the first non-blank character after the indent.
-      switch (source[cursor]) {
-        case ".":
-        case "[":
-        case "*":
-        case "}":
-        case undefined: // EOF
-          // A special character. End the Pattern.
-          return false;
-        case "{":
-          // Placeables don't require indentation (in EBNF: block-placeable).
-          // Continue the Pattern.
-          return makeIndent(source.slice(start, cursor));
-      }
-
-      // If the first character on the line is not one of the special characters
-      // listed above, it's a regular text character. Check if there's at least
-      // one space of indent before it.
-      if (source[cursor - 1] === " ") {
-        // It's an indented text character (in EBNF: indented-char). Continue
-        // the Pattern.
-        return makeIndent(source.slice(start, cursor));
-      }
-
-      // A not-indented text character is likely the identifier of the next
-      // message. End the Pattern.
-      return false;
-    }
-
-    // Trim blanks in text according to the given regex.
-    function trim(text, re) {
-      return text.replace(re, "");
-    }
-
-    // Normalize a blank block and extract the indent details.
-    function makeIndent(blank) {
-      let value = blank.replace(RE_BLANK_LINES, "\n");
-      let length = RE_INDENT.exec(blank)[1].length;
-      return {type: "indent", value, length};
-    }
-  }
-}
-
-this.EXPORTED_SYMBOLS = [
-  ...Object.keys({
-    FluentBundle,
-    FluentResource,
-    FluentError,
-    FluentType,
-    FluentNumber,
-    FluentDateTime,
-  }),
-];
--- a/intl/l10n/L10nRegistry.jsm
+++ b/intl/l10n/L10nRegistry.jsm
@@ -1,13 +1,12 @@
 const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 // eslint-disable-next-line mozilla/use-services
 const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
-const { FluentBundle, FluentResource } = ChromeUtils.import("resource://gre/modules/Fluent.jsm");
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 ChromeUtils.defineModuleGetter(
   this,
   "NetUtil",
   "resource://gre/modules/NetUtil.jsm"
 );
 
@@ -131,22 +130,22 @@ class L10nRegistryService {
    *
    * @param {Array} requestedLangs
    * @param {Array} resourceIds
    * @returns {AsyncIterator<FluentBundle>}
    */
   async* generateBundles(requestedLangs, resourceIds) {
     const resourceIdsDedup = Array.from(new Set(resourceIds));
     const sourcesOrder = Array.from(this.sources.keys()).reverse();
-    const pseudoNameFromPref = Services.prefs.getStringPref("intl.l10n.pseudo", "");
+    const pseudoStrategy = Services.prefs.getStringPref("intl.l10n.pseudo", "");
     for (const locale of requestedLangs) {
       for await (const dataSets of generateResourceSetsForLocale(locale, sourcesOrder, resourceIdsDedup)) {
         const bundle = new FluentBundle(locale, {
           ...MSG_CONTEXT_OPTIONS,
-          transform: PSEUDO_STRATEGIES[pseudoNameFromPref],
+          pseudoStrategy,
         });
         for (const data of dataSets) {
           if (data === null) {
             return;
           }
           bundle.addResource(data);
         }
         yield bundle;
@@ -164,22 +163,22 @@ class L10nRegistryService {
    *
    * @param {Array} requestedLangs
    * @param {Array} resourceIds
    * @returns {Iterator<FluentBundle>}
    */
   * generateBundlesSync(requestedLangs, resourceIds) {
     const resourceIdsDedup = Array.from(new Set(resourceIds));
     const sourcesOrder = Array.from(this.sources.keys()).reverse();
-    const pseudoNameFromPref = Services.prefs.getStringPref("intl.l10n.pseudo", "");
+    const pseudoStrategy = Services.prefs.getStringPref("intl.l10n.pseudo", "");
     for (const locale of requestedLangs) {
       for (const dataSets of generateResourceSetsForLocaleSync(locale, sourcesOrder, resourceIdsDedup)) {
         const bundle = new FluentBundle(locale, {
           ...MSG_CONTEXT_OPTIONS,
-          transform: PSEUDO_STRATEGIES[pseudoNameFromPref],
+          pseudoStrategy
         });
         for (const data of dataSets) {
           if (data === null) {
             return;
           }
           bundle.addResource(data);
         }
         yield bundle;
@@ -422,122 +421,16 @@ function* generateResourceSetsForLocaleS
     }
   }
 }
 
 const MSG_CONTEXT_OPTIONS = {
   // Temporarily disable bidi isolation due to Microsoft not supporting FSI/PDI.
   // See bug 1439018 for details.
   useIsolating: Services.prefs.getBoolPref("intl.l10n.enable-bidi-marks", false),
-  functions: {
-    /**
-     * PLATFORM is a built-in allowing localizers to differentiate message
-     * variants depending on the target platform.
-     */
-    PLATFORM: () => {
-      switch (AppConstants.platform) {
-        case "linux":
-        case "android":
-          return AppConstants.platform;
-        case "win":
-          return "windows";
-        case "macosx":
-          return "macos";
-        default:
-          return "other";
-      }
-    },
-  },
-};
-
-/**
- * Pseudolocalizations
- *
- * PSEUDO_STRATEGIES is a dict of strategies to be used to modify a
- * context in order to create pseudolocalizations.  These can be used by
- * developers to test the localizability of their code without having to
- * actually speak a foreign language.
- *
- * Currently, the following pseudolocales are supported:
- *
- *   accented - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ
- *
- *     In Accented English all Latin letters are replaced by accented
- *     Unicode counterparts which don't impair the readability of the content.
- *     This allows developers to quickly test if any given string is being
- *     correctly displayed in its 'translated' form.  Additionally, simple
- *     heuristics are used to make certain words longer to better simulate the
- *     experience of international users.
- *
- *   bidi - ɥsıʅƃuƎ ıpıԐ
- *
- *     Bidi English is a fake RTL locale.  All words are surrounded by
- *     Unicode formatting marks forcing the RTL directionality of characters.
- *     In addition, to make the reversed text easier to read, individual
- *     letters are flipped.
- *
- *     Note: The name above is hardcoded to be RTL in case code editors have
- *     trouble with the RLO and PDF Unicode marks.  In reality, it should be
- *     surrounded by those marks as well.
- *
- * See https://bugzil.la/1450781 for more information.
- *
- * In this implementation we use code points instead of inline unicode characters
- * because the encoding of JSM files mangles them otherwise.
- */
-
-const ACCENTED_MAP = {
-      // ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ
-      "caps": [550, 385, 391, 7698, 7702, 401, 403, 294, 298, 308, 310, 319, 7742, 544, 510, 420, 586, 344, 350, 358, 364, 7804, 7814, 7818, 7822, 7824],
-      // ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ
-      "small": [551, 384, 392, 7699, 7703, 402, 608, 295, 299, 309, 311, 320, 7743, 414, 511, 421, 587, 345, 351, 359, 365, 7805, 7815, 7819, 7823, 7825],
-};
-
-const FLIPPED_MAP = {
-      // ∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z
-      "caps": [8704, 1296, 8579, 5601, 398, 8498, 8513, 72, 73, 383, 1276, 8514, 87, 78, 79, 1280, 210, 7450, 83, 8869, 8745, 581, 77, 88, 8516, 90],
-      // ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz
-      "small": [592, 113, 596, 112, 477, 607, 387, 613, 305, 638, 670, 645, 623, 117, 111, 100, 98, 633, 115, 647, 110, 652, 653, 120, 654, 122],
-};
-
-function transformString(map, elongate = false, prefix = "", postfix = "", msg) {
-  // Exclude access-keys and other single-char messages
-  if (msg.length === 1) {
-    return msg;
-  }
-  // XML entities (&#x202a;) and XML tags.
-  const reExcluded = /(&[#\w]+;|<\s*.+?\s*>)/;
-
-  const parts = msg.split(reExcluded);
-  const modified = parts.map((part) => {
-    if (reExcluded.test(part)) {
-      return part;
-    }
-    return prefix + part.replace(/[a-z]/ig, (ch) => {
-      let cc = ch.charCodeAt(0);
-      if (cc >= 97 && cc <= 122) {
-        const newChar = String.fromCodePoint(map.small[cc - 97]);
-        // duplicate "a", "e", "o" and "u" to emulate ~30% longer text
-        if (elongate && (cc === 97 || cc === 101 || cc === 111 || cc === 117)) {
-          return newChar + newChar;
-        }
-        return newChar;
-      }
-      if (cc >= 65 && cc <= 90) {
-        return String.fromCodePoint(map.caps[cc - 65]);
-      }
-      return ch;
-    }) + postfix;
-  });
-  return modified.join("");
-}
-
-const PSEUDO_STRATEGIES = {
-  "accented": transformString.bind(null, ACCENTED_MAP, true, "", ""),
-  "bidi": transformString.bind(null, FLIPPED_MAP, false, "\u202e", "\u202c"),
 };
 
 /**
  * Generates a single FluentBundle by loading all resources
  * from the listed sources for a given locale.
  *
  * The function casts all error cases into a Promise that resolves with
  * value `null`.
--- a/intl/l10n/README
+++ b/intl/l10n/README
@@ -1,15 +1,8 @@
 The content of this directory is partially sourced from the fluent.js project.
 
 The following files are affected:
- - Fluent.jsm
  - Localization.jsm
 
 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. 
deleted file mode 100644
--- a/intl/l10n/fluent.js.patch
+++ /dev/null
@@ -1,475 +0,0 @@
-diff --git a/intl/l10n/Fluent.jsm b/intl/l10n/Fluent.jsm
---- a/intl/l10n/Fluent.jsm
-+++ b/intl/l10n/Fluent.jsm
-@@ -16,7 +16,7 @@
-  */
- 
- 
--/* fluent@0.10.0 */
-+/* fluent-dom@0.4.0 */
- 
- /* global Intl */
- 
-@@ -139,53 +139,7 @@ function values(opts) {
-   return unwrapped;
- }
- 
--/**
-- * @overview
-- *
-- * The role of the Fluent resolver is to format a translation object to an
-- * instance of `FluentType` or an array of instances.
-- *
-- * Translations can contain references to other messages or 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
-- * bundle'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:
-- *
-- *  * {FluentBundle} bundle
-- *      bundle 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.
-- */
-+/* global Intl */
- 
- // Prevent expansion of too long placeables.
- const MAX_PLACEABLE_LENGTH = 2500;
-@@ -514,7 +468,7 @@ function Pattern(env, ptn) {
-  */
- function resolve(bundle, args, message, errors = []) {
-   const env = {
--    bundle, args, errors, dirty: new WeakSet(),
-+    bundle, args, errors, dirty: new WeakSet()
-   };
-   return Type(env, message).toString(bundle);
- }
-@@ -1064,7 +1018,7 @@ class FluentBundle {
-   constructor(locales, {
-     functions = {},
-     useIsolating = true,
--    transform = v => v,
-+    transform = v => v
-   } = {}) {
-     this.locales = Array.isArray(locales) ? locales : [locales];
- 
-@@ -1235,6 +1189,14 @@ class FluentBundle {
-   }
- }
- 
-+/*
-+ * @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.FluentBundle = FluentBundle;
--this.FluentResource = FluentResource;
--var EXPORTED_SYMBOLS = ["FluentBundle", "FluentResource"];
-+this.EXPORTED_SYMBOLS = ["FluentBundle"];
-diff --git a/intl/l10n/Localization.jsm b/intl/l10n/Localization.jsm
---- a/intl/l10n/Localization.jsm
-+++ b/intl/l10n/Localization.jsm
-@@ -16,34 +16,27 @@
-  */
- 
- 
--/* fluent-dom@fa25466f (October 12, 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", {});
-+/* fluent-dom@0.4.0 */
- 
- /*
-  * Base CachedIterable class.
-  */
- 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;
-+    /**
-+     * 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);
-     }
--
--    return new this(iterable);
--  }
- }
- 
- /*
-@@ -53,80 +46,88 @@ class CachedIterable extends Array {
-  * iterable.
-  */
- class CachedAsyncIterable extends CachedIterable {
--  /**
--   * Create an `CachedAsyncIterable` instance.
--   *
--   * @param {Iterable} iterable
--   * @returns {CachedAsyncIterable}
--   */
--  constructor(iterable) {
--    super();
-+    /**
-+     * 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.");
-+        }
-+    }
- 
--    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;
- 
--  /**
--   * 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 {
-+            next() {
-+                if (cached.length === cur) {
-+                    return {value: undefined, done: true};
-+                }
-+                return cached[cur++];
-+            }
-+        };
-+    }
- 
--    return {
--      async next() {
--        if (cached.length <= cur) {
--          cached.push(cached.iterator.next());
--        }
--        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;
- 
--  /**
--   * 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 && (await last).done) {
--        break;
--      }
--      this.push(this.iterator.next());
-+        return {
-+            async next() {
-+                if (cached.length <= cur) {
-+                    cached.push(await cached.iterator.next());
-+                }
-+                return cached[cur++];
-+            }
-+        };
-     }
--    // 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];
--  }
-+
-+    /**
-+     * 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 FluentBundles.
-- *
-- * 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 defaultGenerateBundles(resourceIds) {
--  const appLocales = Services.locale.appLocalesAsBCP47;
--  return L10nRegistry.generateBundles(appLocales, resourceIds);
--}
-+/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
- 
- /**
-  * The `Localization` class is a central high-level API for vanilla
-@@ -142,21 +143,16 @@ class Localization {
-    *
-    * @returns {Localization}
-    */
--  constructor(resourceIds = [], generateBundles = defaultGenerateBundles) {
-+  constructor(resourceIds = [], generateBundles) {
-     this.resourceIds = resourceIds;
-     this.generateBundles = generateBundles;
-     this.bundles = CachedAsyncIterable.from(
-       this.generateBundles(this.resourceIds));
-   }
- 
--  /**
--   * @param {Array<String>} resourceIds - List of resource IDs
--   * @param {bool}                eager - whether the I/O for new context should
--   *                                      begin eagerly
--   */
--  addResourceIds(resourceIds, eager = false) {
-+  addResourceIds(resourceIds) {
-     this.resourceIds.push(...resourceIds);
--    this.onChange(eager);
-+    this.onChange();
-     return this.resourceIds.length;
-   }
- 
-@@ -188,12 +184,9 @@ class Localization {
-         break;
-       }
- 
--      if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) {
-+      if (typeof console !== "undefined") {
-         const locale = bundle.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}`);
-       }
-     }
-@@ -281,64 +274,21 @@ class Localization {
-     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);
--  }
--
--  /**
--   * 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();
-   }
- 
-   /**
-    * This method should be called when there's a reason to believe
-    * that language negotiation or available resources changed.
--   *
--   * @param {bool} eager - whether the I/O for new context should begin eagerly
-    */
--  onChange(eager = false) {
-+  onChange() {
-     this.bundles = CachedAsyncIterable.from(
-       this.generateBundles(this.resourceIds));
--    if (eager) {
--      // If the first app locale is the same as last fallback
--      // it means that we have all resources in this locale, and
--      // we want to eagerly fetch just that one.
--      // Otherwise, we're in a scenario where the first locale may
--      // be partial and we want to eagerly fetch a fallback as well.
--      const appLocale = Services.locale.appLocaleAsBCP47;
--      const lastFallback = Services.locale.lastFallbackLocale;
--      const prefetchCount = appLocale === lastFallback ? 1 : 2;
--      this.bundles.touchNext(prefetchCount);
--    }
-+    this.bundles.touchNext(2);
-   }
- }
- 
--Localization.prototype.QueryInterface = ChromeUtils.generateQI([
--  Ci.nsISupportsWeakReference,
--]);
--
- /**
-  * Format the value of a message into a string.
-  *
-@@ -430,7 +380,7 @@ function messageFromBundle(bundle, error
-  * See `Localization.formatWithFallback` for more info on how this is used.
-  *
-  * @param {Function}       method
-- * @param {FluentBundle}   bundle
-+ * @param {FluentBundle} bundle
-  * @param {Array<string>}  keys
-  * @param {{Array<{value: string, attributes: Object}>}} translations
-  *
-@@ -458,5 +408,44 @@ function keysFromBundle(method, bundle, 
-   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 FluentBundles.
-+ *
-+ * 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 defaultGenerateBundles(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, generateBundles = defaultGenerateBundles) {
-+    super(resourceIds, generateBundles);
-+  }
-+}
-+
-+this.Localization = GeckoLocalization;
-+this.EXPORTED_SYMBOLS = ["Localization"];
--- a/intl/l10n/moz.build
+++ b/intl/l10n/moz.build
@@ -13,17 +13,16 @@ EXPORTS.mozilla.intl += [
 
 UNIFIED_SOURCES += [
     'FluentBundle.cpp',
     'FluentResource.cpp',
     'Localization.cpp',
 ]
 
 EXTRA_JS_MODULES += [
-    'Fluent.jsm',
     'L10nRegistry.jsm',
     'Localization.jsm',
 ]
 
 TESTING_JS_MODULES += [
     'FluentSyntax.jsm',
 ]
 
--- a/intl/l10n/test/mochitest/localization/test_formatMessages.html
+++ b/intl/l10n/test/mochitest/localization/test_formatMessages.html
@@ -2,18 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test Localization.prototype.formatMessages API</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
 
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US", {
       useIsolating: false,
     });
     bundle.addResource(new FluentResource(`
 key1 = Value
   .title = Title 1
--- a/intl/l10n/test/mochitest/localization/test_formatValue.html
+++ b/intl/l10n/test/mochitest/localization/test_formatValue.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test Localization.prototype.formatValue API</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US", {
       useIsolating: false,
     });
     bundle.addResource(new FluentResource(`
 key1 = Value
 key2 = Value { $user }
 key3 = Value { $count }
--- a/intl/l10n/test/mochitest/localization/test_formatValues.html
+++ b/intl/l10n/test/mochitest/localization/test_formatValues.html
@@ -2,19 +2,16 @@
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test Localization.prototype.formatValues API</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   async function* mockGenerateMessages(resourceIds) {
     const bundle = new FluentBundle("en-US",
     {
       useIsolating: false,
     });
     bundle.addResource(new FluentResource(`
 key1 = Value
 key2 = Value { $user }
--- a/intl/l10n/test/test_messagecontext.js
+++ b/intl/l10n/test/test_messagecontext.js
@@ -1,15 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function run_test() {
-  const { FluentBundle, FluentResource } =
-    ChromeUtils.import("resource://gre/modules/Fluent.jsm");
-
   test_methods_presence(FluentBundle);
   test_methods_calling(FluentBundle, FluentResource);
 
   ok(true);
 }
 
 function test_methods_presence(FluentBundle) {
   const bundle = new FluentBundle(["en-US", "pl"]);
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js
@@ -168,16 +168,18 @@ module.exports = {
     FileSystem: false,
     FileSystemDirectoryEntry: false,
     FileSystemDirectoryReader: false,
     FileSystemEntry: false,
     FileSystemFileEntry: false,
     Flex: false,
     FlexItemValues: false,
     FlexLineValues: false,
+    FluentBundle: false,
+    FluentResource: false,
     FocusEvent: false,
     FontFace: false,
     FontFaceSet: false,
     FontFaceSetLoadEvent: false,
     FormData: false,
     FrameLoader: false,
     GainNode: false,
     Gamepad: false,
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -63,17 +63,16 @@
   "ExtensionAPI.jsm": ["ExtensionAPI", "ExtensionAPIs"],
   "ExtensionXPCShellUtils.jsm": ["ExtensionTestUtils"],
   "fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"],
   "file_expandosharing.jsm": ["checkFromJSM"],
   "file_stringencoding.jsm": ["checkFromJSM"],
   "file_url.jsm": ["checkFromJSM"],
   "file_worker_url.jsm": ["checkFromJSM"],
   "Finder.jsm": ["Finder", "GetClipboardSearchString"],
-  "Fluent.jsm": ["FluentBundle", "FluentResource"],
   "format.js": ["pprint", "truncate"],
   "formautofill.jsm": ["Address", "CreditCard", "DumpAddresses", "DumpCreditCards"],
   "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
   "FormAutofillStorage.jsm": ["formAutofillStorage"],
   "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
   "FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"],
   "forms.js": ["FormEngine", "FormRec", "FormValidator"],
   "forms.jsm": ["FormData"],