Bug 1438375 - Refactor "extensionControlled" Preferences code to use Fluent. r=flod,Gijs
☠☠ backed out by 42c89029f1ad ☠ ☠
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 17 Apr 2018 15:31:50 -0700
changeset 469079 fb836a045f2024b51bdd9899fe2e8db19bd96f8a
parent 469078 de6af6a44e14ba6426717c39ce3e52ee9e4c4a70
child 469080 801fa27ae7b8b454bd56677c09038e264421c807
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, Gijs
bugs1438375
milestone61.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 1438375 - Refactor "extensionControlled" Preferences code to use Fluent. r=flod,Gijs MozReview-Commit-ID: 9XJxyuMUCHV
browser/components/preferences/connection.xul
browser/components/preferences/in-content/extensionControlled.js
browser/components/preferences/in-content/home.js
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/tests/browser_extension_controlled.js
browser/locales/en-US/browser/preferences/preferences.ftl
browser/locales/en-US/chrome/browser/preferences/preferences.properties
python/l10n/fluent_migrations/bug_1438375_preferences_extension_controlled.py
--- a/browser/components/preferences/connection.xul
+++ b/browser/components/preferences/connection.xul
@@ -15,30 +15,31 @@
         persist="lastSelected screenX screenY"
         role="dialog"
         onbeforeaccept="return gConnectionsDialog.beforeAccept();"
         onload="gConnectionsDialog.checkForSystemProxy();"
         helpTopic="prefs-connection-settings"
         ondialoghelp="openPrefsHelp()">
 
   <link rel="localization" href="browser/preferences/connection.ftl"/>
+
+  <!-- Used for extension-controlled lockdown message -->
+  <link rel="localization" href="browser/preferences/preferences.ftl"/>
+  <link rel="localization" href="branding/brand.ftl"/>
   <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
 
   <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
   <script type="application/javascript" src="chrome://browser/content/preferences/in-content/extensionControlled.js"/>
 
   <keyset>
     <key data-l10n-id="connection-close-key" modifiers="accel" oncommand="Preferences.close(event)"/>
   </keyset>
 
   <vbox id="ConnectionsDialogPane" class="prefpane largeDialogContainer">
-
-    <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/>
-    <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/>
     <script type="application/javascript" src="chrome://browser/content/preferences/connection.js"/>
 
     <hbox id="proxyExtensionContent" align="top" hidden="true">
       <description control="disableProxyExtension" flex="1" />
       <button id="disableProxyExtension"
               class="extension-controlled-button accessory-button"
               data-l10n-id="connection-disable-extension" />
     </hbox>
--- a/browser/components/preferences/in-content/extensionControlled.js
+++ b/browser/components/preferences/in-content/extensionControlled.js
@@ -52,24 +52,24 @@ let extensionControlledContentIds = {
       section:
         trackingprotectionUiEnabled ?
           "trackingProtectionExtensionContentLabel" :
           "trackingProtectionPBMExtensionContentLabel",
     };
   }
 };
 
-function getExtensionControlledArgs(settingName) {
-  switch (settingName) {
-    case "proxyConfig":
-      return [document.getElementById("bundleBrand").getString("brandShortName")];
-    default:
-      return [];
-  }
-}
+const extensionControlledL10nKeys = {
+  "homepage_override": "homepage-override",
+  "newTabURL": "new-tab-url",
+  "defaultSearch": "default-search",
+  "privacy.containers": "privacy-containers",
+  "websites.trackingProtectionMode": "websites-tracking-protection-mode",
+  "proxyConfig": "proxy-config",
+};
 
 let extensionControlledIds = {};
 
 /**
   * Check if a pref is being managed by an extension.
   */
 async function getControllingExtensionInfo(type, settingName) {
   await ExtensionSettingsStore.initialize();
@@ -91,71 +91,94 @@ function getControllingExtensionEls(sett
 
 async function getControllingExtension(type, settingName) {
   let info = await getControllingExtensionInfo(type, settingName);
   let addon = info && info.id
     && await AddonManager.getAddonByID(info.id);
   return addon;
 }
 
-async function handleControllingExtension(type, settingName, stringId) {
+async function handleControllingExtension(type, settingName) {
   let addon = await getControllingExtension(type, settingName);
 
   // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
   // an extension is controlling a setting but the extension has been uninstalled
   // outside of the regular lifecycle. If the extension isn't currently installed
   // then we should treat the setting as not being controlled.
   // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
   if (addon) {
     extensionControlledIds[settingName] = addon.id;
-    showControllingExtension(settingName, addon, stringId);
+    showControllingExtension(settingName, addon);
   } else {
     let elements = getControllingExtensionEls(settingName);
     if (extensionControlledIds[settingName]
         && !document.hidden
         && elements.button) {
       showEnableExtensionMessage(settingName);
     } else {
       hideControllingExtension(settingName);
     }
     delete extensionControlledIds[settingName];
   }
 
   return !!addon;
 }
 
-function getControllingExtensionFragment(stringId, addon, ...extraArgs) {
-  let msg = document.getElementById("bundlePreferences").getString(stringId);
-  let image = document.createElement("image");
+function settingNameToL10nID(settingName) {
+  if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) {
+    throw new Error(`Unknown extension controlled setting name: ${settingName}`);
+  }
+  return `extension-controlled-${extensionControlledL10nKeys[settingName]}`;
+}
+
+
+/**
+ * Set the localization data for the description of the controlling extension.
+ *
+ * @param elem {Element}
+ *        <description> element to be annotated
+ * @param addon {Object?}
+ *        Addon object with meta information about the addon (or null)
+ * @param settingName {String}
+ *        If `addon` is set this handled the name of the setting that will be used
+ *        to fetch the l10n id for the given message.
+ *        If `addon` is set to null, this will be the full l10n-id assigned to the
+ *        element.
+ */
+function setControllingExtensionDescription(elem, addon, settingName) {
+  // Remove the old content from the description.
+  while (elem.firstChild) {
+    elem.firstChild.remove();
+  }
+
+  if (addon === null) {
+    document.l10n.setAttributes(elem, settingName);
+    return;
+  }
+
+  let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
   const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
   image.setAttribute("src", addon.iconURL || defaultIcon);
+  image.setAttribute("data-l10n-name", "icon");
   image.classList.add("extension-controlled-icon");
-  let addonBit = document.createDocumentFragment();
-  addonBit.appendChild(image);
-  addonBit.appendChild(document.createTextNode(" " + addon.name));
-  return BrowserUtils.getLocalizedFragment(document, msg, addonBit, ...extraArgs);
+  elem.appendChild(image);
+  const l10nId = settingNameToL10nID(settingName);
+  document.l10n.setAttributes(elem, l10nId, {
+    name: addon.name
+  });
 }
 
-async function showControllingExtension(
-  settingName, addon, stringId = `extensionControlled.${settingName}`) {
+async function showControllingExtension(settingName, addon) {
   // Tell the user what extension is controlling the setting.
   let elements = getControllingExtensionEls(settingName);
-  let extraArgs = getExtensionControlledArgs(settingName);
 
   elements.section.classList.remove("extension-controlled-disabled");
   let description = elements.description;
 
-  // Remove the old content from the description.
-  while (description.firstChild) {
-    description.firstChild.remove();
-  }
-
-  let fragment = getControllingExtensionFragment(
-    stringId, addon, ...extraArgs);
-  description.appendChild(fragment);
+  setControllingExtensionDescription(description, addon, settingName);
 
   if (elements.button) {
     elements.button.hidden = false;
   }
 
   // Show the controlling extension row and hide the old label.
   elements.section.hidden = false;
 }
@@ -168,29 +191,49 @@ function hideControllingExtension(settin
   }
 }
 
 function showEnableExtensionMessage(settingName) {
   let elements = getControllingExtensionEls(settingName);
 
   elements.button.hidden = true;
   elements.section.classList.add("extension-controlled-disabled");
-  let icon = url => {
-    let img = document.createElement("image");
+
+  elements.description.textContent = "";
+
+  // We replace localization of the <description> with a DOM Fragment containing
+  // the enable-extension-enable message. That means a change from:
+  //
+  // <description data-l10n-id="..."/>
+  //
+  // to:
+  //
+  // <description>
+  //   <img/>
+  //   <label data-l10n-id="..."/>
+  // </description>
+  //
+  // We need to remove the l10n-id annotation from the <description> to prevent
+  // Fluent from overwriting the element in case of any retranslation.
+  elements.description.removeAttribute("data-l10n-id");
+
+  let icon = (url, name) => {
+    let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
     img.src = url;
+    img.setAttribute("data-l10n-name", name);
     img.className = "extension-controlled-icon";
     return img;
   };
-  let addonIcon = icon("chrome://mozapps/skin/extensions/extensionGeneric-16.svg");
-  let toolbarIcon = icon("chrome://browser/skin/menu.svg");
-  let message = document.getElementById("bundlePreferences")
-                        .getString("extensionControlled.enable");
-  let frag = BrowserUtils.getLocalizedFragment(document, message, addonIcon, toolbarIcon);
-  elements.description.innerHTML = "";
-  elements.description.appendChild(frag);
+  let label = document.createElement("label");
+  let addonIcon = icon("chrome://mozapps/skin/extensions/extensionGeneric-16.svg", "addons-icon");
+  let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon");
+  label.appendChild(addonIcon);
+  label.appendChild(toolbarIcon);
+  document.l10n.setAttributes(label, "extension-controlled-enable");
+  elements.description.appendChild(label);
   let dismissButton = document.createElement("image");
   dismissButton.setAttribute("class", "extension-controlled-icon close-icon");
   dismissButton.addEventListener("click", function dismissHandler() {
     hideControllingExtension(settingName);
     dismissButton.removeEventListener("click", dismissHandler);
   });
   elements.description.appendChild(dismissButton);
 }
--- a/browser/components/preferences/in-content/home.js
+++ b/browser/components/preferences/in-content/home.js
@@ -26,31 +26,30 @@ Preferences.addAll([
   { id: "pref.browser.homepage.disable_button.bookmark_page", type: "bool" },
   { id: "pref.browser.homepage.disable_button.restore_default", type: "bool" },
   { id: "browser.newtabpage.enabled", type: "bool" }
 ]);
 
 const HOMEPAGE_OVERRIDE_KEY = "homepage_override";
 const URL_OVERRIDES_TYPE = "url_overrides";
 const NEW_TAB_KEY = "newTabURL";
-const NEW_TAB_STRING_ID = "extensionControlled.newTabURL2";
 
 let gHomePane = {
   HOME_MODE_FIREFOX_HOME: "0",
   HOME_MODE_BLANK: "1",
   HOME_MODE_CUSTOM: "2",
   NEWTAB_ENABLED_PREF: "browser.newtabpage.enabled",
 
   /**
    * _handleNewTabOverrides: disables new tab settings UI. Called by
    * an observer in ._watchNewTab that watches for new tab url changes
    */
   async _handleNewTabOverrides() {
     const isControlled = await handleControllingExtension(
-      URL_OVERRIDES_TYPE, NEW_TAB_KEY, NEW_TAB_STRING_ID);
+      URL_OVERRIDES_TYPE, NEW_TAB_KEY);
     const el = document.getElementById("newTabMode");
     el.disabled = isControlled;
   },
 
   /**
    * watchNewTab: Listen for changes to the new tab url and disable appropriate
    * areas of the UI
    */
@@ -122,18 +121,17 @@ let gHomePane = {
    * @resolves {bool} Is the homepage being controlled by an extension?
    * @returns {Promise}
    */
   isHomePageControlled() {
     const homePref = Preferences.get("browser.startup.homepage");
     if (homePref.locked) {
       return Promise.resolve(false);
     }
-    return handleControllingExtension(
-      PREF_SETTING_TYPE, HOMEPAGE_OVERRIDE_KEY, "extensionControlled.homepage_override2");
+    return handleControllingExtension(PREF_SETTING_TYPE, HOMEPAGE_OVERRIDE_KEY);
   },
 
   /**
    * _isTabAboutPreferences: Is a given tab set to about:preferences?
    * @param {Element} aTab A tab element
    * @returns {bool} Is the linkedBrowser of aElement set to about:preferences?
    */
   _isTabAboutPreferences(aTab) {
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -927,31 +927,23 @@ var gMainPane = {
     gSubDialog.open("chrome://browser/content/preferences/connection.xul",
                     null, null, this.updateProxySettingsUI.bind(this));
   },
 
   // Update the UI to show the proper description depending on whether an
   // extension is in control or not.
   async updateProxySettingsUI() {
     let controllingExtension = await getControllingExtension(PREF_SETTING_TYPE, PROXY_KEY);
-    let fragment = controllingExtension ?
-      getControllingExtensionFragment(
-        "extensionControlled.proxyConfig", controllingExtension, this._brandShortName) :
-      BrowserUtils.getLocalizedFragment(
-        document,
-        this._prefsBundle.getString("connectionDesc.label"),
-        this._brandShortName);
     let description = document.getElementById("connectionSettingsDescription");
 
-    // Remove the old content from the description.
-    while (description.firstChild) {
-      description.firstChild.remove();
+    if (controllingExtension) {
+      setControllingExtensionDescription(description, controllingExtension, "proxyConfig");
+    } else {
+      setControllingExtensionDescription(description, null, "network-proxy-connection-description");
     }
-
-    description.appendChild(fragment);
   },
 
   async checkBrowserContainers(event) {
     let checkbox = document.getElementById("browserContainersCheckbox");
     if (checkbox.checked) {
       Services.prefs.setBoolPref("privacy.userContext.enabled", true);
       return;
     }
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -68,20 +68,20 @@ function waitForMessageShown(messageId, 
 
 function waitForEnableMessage(messageId, doc) {
   return waitForMessageChange(
     getElement(messageId, doc),
     target => target.classList.contains("extension-controlled-disabled"),
     { attributeFilter: ["class"], attributes: true });
 }
 
-function waitForMessageContent(messageId, content, doc) {
+function waitForMessageContent(messageId, l10nId, doc) {
   return waitForMessageChange(
     getElement(messageId, doc),
-    target => target.textContent === content,
+    target => doc.l10n.getAttributes(target).id === l10nId,
     { childList: true });
 }
 
 add_task(async function testExtensionControlledHomepage() {
   await openPreferencesViaOpenPreferencesAPI("paneHome", {leaveOpen: true});
   // eslint-disable-next-line mozilla/no-cpows-in-tests
   let doc = gBrowser.contentDocument;
   is(gBrowser.currentURI.spec, "about:preferences#home",
@@ -102,30 +102,34 @@ add_task(async function testExtensionCon
 
   // Install an extension that will set the homepage.
   await installAddon("set_homepage.xpi");
   await waitForMessageShown("browserHomePageExtensionContent");
 
   // The homepage has been set by the extension, the user is notified and it isn't editable.
   let controlledLabel = controlledContent.querySelector("description");
   is(homepagePref(), extensionHomepage, "homepage is set by extension");
-  // There are two spaces before "set_homepage" because it's " <image /> set_homepage".
-  is(controlledLabel.textContent, "An extension,  set_homepage, is controlling your home page.",
-     "The user is notified that an extension is controlling the homepage");
+  Assert.deepEqual(doc.l10n.getAttributes(controlledLabel), {
+    id: "extension-controlled-homepage-override",
+    args: {
+      name: "set_homepage",
+    }
+  }, "The user is notified that an extension is controlling the homepage");
   is(controlledContent.hidden, false, "The extension controlled row is hidden");
   is(homeModeEl.disabled, true, "The homepage input is disabled");
 
   // Disable the extension.
   let enableMessageShown = waitForEnableMessage(controlledContent.id);
   doc.getElementById("disableHomePageExtension").click();
   await enableMessageShown;
 
   // The user is notified how to enable the extension.
-  is(controlledLabel.textContent, "To enable the extension go to  Add-ons in the  menu.",
-     "The user is notified of how to enable the extension again");
+  is(doc.l10n.getAttributes(controlledLabel.querySelector("label")).id,
+    "extension-controlled-enable",
+    "The user is notified of how to enable the extension again");
 
   // The user can dismiss the enable instructions.
   let hidden = waitForMessageHidden("browserHomePageExtensionContent");
   controlledLabel.querySelector("image:last-of-type").click();
   await hidden;
 
   // The homepage elements are reset to their original state.
   is(homepagePref(), originalHomepagePref, "homepage is set back to default");
@@ -304,28 +308,32 @@ add_task(async function testExtensionCon
   // Install an extension that will set the new tab page.
   await installAddon("set_newtab.xpi");
 
   await waitForMessageShown("browserNewTabExtensionContent");
 
   // The new tab page has been set by the extension and the user is notified.
   let controlledLabel = controlledContent.querySelector("description");
   ok(aboutNewTabService.newTabURL.startsWith("moz-extension:"), "new tab url is set by extension");
-  // There are two spaces before "set_newtab" because it's " <image /> set_newtab".
-  is(controlledLabel.textContent, "An extension,  set_newtab, is controlling your New Tab page.",
-     "The user is notified that an extension is controlling the new tab page");
+  Assert.deepEqual(doc.l10n.getAttributes(controlledLabel), {
+    id: "extension-controlled-new-tab-url",
+    args: {
+      name: "set_newtab",
+    }
+  }, "The user is notified that an extension is controlling the new tab page");
   is(controlledContent.hidden, false, "The extension controlled row is hidden");
 
   // Disable the extension.
   doc.getElementById("disableNewTabExtension").click();
 
   // Verify the user is notified how to enable the extension.
   await waitForEnableMessage(controlledContent.id);
-  is(controlledLabel.textContent, "To enable the extension go to  Add-ons in the  menu.",
-     "The user is notified of how to enable the extension again");
+  is(doc.l10n.getAttributes(controlledLabel.querySelector("label")).id,
+    "extension-controlled-enable",
+    "The user is notified of how to enable the extension again");
 
   // Verify the enable message can be dismissed.
   let hidden = waitForMessageHidden(controlledContent.id);
   let dismissButton = controlledLabel.querySelector("image:last-of-type");
   dismissButton.click();
   await hidden;
 
   // Ensure the New Tab page has been reset and there is no message.
@@ -383,20 +391,22 @@ add_task(async function testExtensionCon
 
   let addon = await AddonManager.getAddonByID(extensionId);
   is(addon.version, "1.0", "The addon has the expected version.");
 
   // The default search engine has been set by the extension and the user is notified.
   let controlledLabel = controlledContent.querySelector("description");
   let extensionEngine = Services.search.currentEngine;
   ok(initialEngine != extensionEngine, "The default engine has changed.");
-  // There are two spaces before "set_default_search" because it's " <image /> set_default_search".
-  is(controlledLabel.textContent,
-     "An extension,  set_default_search, has set your default search engine.",
-     "The user is notified that an extension is controlling the default search engine");
+  Assert.deepEqual(doc.l10n.getAttributes(controlledLabel), {
+    id: "extension-controlled-default-search",
+    args: {
+      name: "set_default_search",
+    }
+  }, "The user is notified that an extension is controlling the default search engine");
   is(controlledContent.hidden, false, "The extension controlled row is shown");
 
   // Set the engine back to the initial one, ensure the message is hidden.
   setEngine(initialEngine);
   await waitForMessageHidden(controlledContent.id);
 
   is(initialEngine, Services.search.currentEngine,
      "default search engine is set back to default");
@@ -524,19 +534,22 @@ add_task(async function testExtensionCon
     is(tpEnabledPref(), isControlled, "TP pref is set to the expected value.");
 
     let controlledLabel = doc.getElementById(CONTROLLED_LABEL_ID[uiType]);
 
     is(controlledLabel.hidden, !isControlled, "The extension controlled row's visibility is as expected.");
     is(controlledButton.hidden, !isControlled, "The disable extension button's visibility is as expected.");
     if (isControlled) {
       let controlledDesc = controlledLabel.querySelector("description");
-      // There are two spaces before "set_tp" because it's " <image /> set_tp".
-      is(controlledDesc.textContent, "An extension,  set_tp, is controlling tracking protection.",
-         "The user is notified that an extension is controlling TP.");
+      Assert.deepEqual(doc.l10n.getAttributes(controlledDesc), {
+        id: "extension-controlled-websites-tracking-protection-mode",
+        args: {
+          name: "set_tp",
+        }
+      }, "The user is notified that an extension is controlling TP.");
     }
 
     if (uiType === "new") {
       for (let element of doc.querySelectorAll("#trackingProtectionRadioGroup > radio")) {
         is(element.disabled, isControlled, "TP controls are enabled.");
       }
       is(doc.querySelector("#trackingProtectionDesc > label").disabled,
          isControlled,
@@ -556,18 +569,19 @@ add_task(async function testExtensionCon
     let controlledLabel = doc.getElementById(labelId);
 
     let enableMessageShown = waitForEnableMessage(labelId);
     doc.getElementById("disableTrackingProtectionExtension").click();
     await enableMessageShown;
 
     // The user is notified how to enable the extension.
     let controlledDesc = controlledLabel.querySelector("description");
-    is(controlledDesc.textContent, "To enable the extension go to  Add-ons in the  menu.",
-       "The user is notified of how to enable the extension again");
+    is(doc.l10n.getAttributes(controlledDesc.querySelector("label")).id,
+      "extension-controlled-enable",
+      "The user is notified of how to enable the extension again");
 
     // The user can dismiss the enable instructions.
     let hidden = waitForMessageHidden(labelId);
     controlledLabel.querySelector("image:last-of-type").click();
     await hidden;
   }
 
   async function reEnableExtension(addon) {
@@ -645,48 +659,50 @@ add_task(async function testExtensionCon
 
   await SpecialPowers.pushPrefEnv({"set": [[PROXY_PREF, PROXY_DEFAULT]]});
 
   function background() {
     browser.browserSettings.proxyConfig.set({value: {proxyType: "none"}});
   }
 
   function expectedConnectionSettingsMessage(doc, isControlled) {
-    let brandShortName = doc.getElementById("bundleBrand").getString("brandShortName");
     return isControlled ?
-      `An extension,  set_proxy, is controlling how ${brandShortName} connects to the internet.` :
-      `Configure how ${brandShortName} connects to the internet.`;
+      "extension-controlled-proxy-config" :
+      "network-proxy-connection-description";
   }
 
   function connectionSettingsMessagePromise(doc, isControlled) {
     return waitForMessageContent(
       CONNECTION_SETTINGS_DESC_ID,
-      expectedConnectionSettingsMessage(doc, isControlled)
+      expectedConnectionSettingsMessage(doc, isControlled),
+      doc
     );
   }
 
   function verifyState(doc, isControlled) {
     let isPanel = doc.getElementById(CONTROLLED_BUTTON_ID);
-    let brandShortName = doc.getElementById("bundleBrand").getString("brandShortName");
     is(proxyType === proxySvc.PROXYCONFIG_DIRECT, isControlled,
       "Proxy pref is set to the expected value.");
 
     if (isPanel) {
       let controlledSection = doc.getElementById(CONTROLLED_SECTION_ID);
 
       is(controlledSection.hidden, !isControlled, "The extension controlled row's visibility is as expected.");
       if (isPanel) {
         is(doc.getElementById(CONTROLLED_BUTTON_ID).hidden, !isControlled,
            "The disable extension button's visibility is as expected.");
       }
       if (isControlled) {
         let controlledDesc = controlledSection.querySelector("description");
-        // There are two spaces before "set_proxy" because it's " <image /> set_proxy".
-        is(controlledDesc.textContent, `An extension,  set_proxy, is controlling how ${brandShortName} connects to the internet.`,
-          "The user is notified that an extension is controlling proxy settings.");
+        Assert.deepEqual(doc.l10n.getAttributes(controlledDesc), {
+          id: "extension-controlled-proxy-config",
+          args: {
+            name: "set_proxy",
+          }
+        }, "The user is notified that an extension is controlling proxy settings.");
       }
       function getProxyControls() {
         let controlGroup = doc.getElementById("networkProxyType");
         let manualControlContainer = controlGroup.querySelector("grid");
         return {
           manualControls: [
             ...manualControlContainer.querySelectorAll("label"),
             ...manualControlContainer.querySelectorAll("textbox"),
@@ -707,34 +723,36 @@ add_task(async function testExtensionCon
       for (let element of controls.pacControls) {
         let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_PAC;
         is(element.disabled, disabled, `Proxy controls are ${controlState}.`);
       }
       for (let element of controls.otherControls) {
         is(element.disabled, isControlled, `Proxy controls are ${controlState}.`);
       }
     } else {
-      is(doc.getElementById(CONNECTION_SETTINGS_DESC_ID).textContent,
-         expectedConnectionSettingsMessage(doc, isControlled),
-         "The connection settings description is as expected.");
+      let elem = doc.getElementById(CONNECTION_SETTINGS_DESC_ID);
+      is(doc.l10n.getAttributes(elem).id,
+        expectedConnectionSettingsMessage(doc, isControlled),
+        "The connection settings description is as expected.");
     }
   }
 
   async function disableViaClick() {
     let sectionId = CONTROLLED_SECTION_ID;
     let controlledSection = panelDoc.getElementById(sectionId);
 
     let enableMessageShown = waitForEnableMessage(sectionId, panelDoc);
     panelDoc.getElementById(CONTROLLED_BUTTON_ID).click();
     await enableMessageShown;
 
     // The user is notified how to enable the extension.
     let controlledDesc = controlledSection.querySelector("description");
-    is(controlledDesc.textContent, "To enable the extension go to  Add-ons in the  menu.",
-       "The user is notified of how to enable the extension again");
+    is(panelDoc.l10n.getAttributes(controlledDesc.querySelector("label")).id,
+      "extension-controlled-enable",
+      "The user is notified of how to enable the extension again");
 
     // The user can dismiss the enable instructions.
     let hidden = waitForMessageHidden(sectionId, panelDoc);
     controlledSection.querySelector("image:last-of-type").click();
     return hidden;
   }
 
   async function reEnableExtension(addon) {
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -71,16 +71,57 @@ close-button =
 
 feature-enable-requires-restart = { -brand-short-name } must restart to enable this feature.
 feature-disable-requires-restart = { -brand-short-name } must restart to disable this feature.
 should-restart-title = Restart { -brand-short-name }
 should-restart-ok = Restart { -brand-short-name } now
 cancel-no-restart-button = Cancel
 restart-later = Restart Later
 
+## Extension Control Notifications
+##
+## These strings are used to inform the user
+## about changes made by extensions to browser settings.
+##
+## <img data-l10n-name="icon"/> is going to be replaced by the extension icon.
+##
+## Variables:
+##   $name (String): name of the extension
+
+# This string is shown to notify the user that their home page
+# is being controlled by an extension.
+extension-controlled-homepage-override = An extension, <img data-l10n-name="icon"/> { $name }, is controlling your home page.
+
+# This string is shown to notify the user that their new tab page
+# is being controlled by an extension.
+extension-controlled-new-tab-url = An extension, <img data-l10n-name="icon"/> { $name }, is controlling your New Tab page.
+
+# This string is shown to notify the user that the default search engine
+# is being controlled by an extension.
+extension-controlled-default-search = An extension, <img data-l10n-name="icon"/> { $name }, has set your default search engine.
+
+# This string is shown to notify the user that Container Tabs
+# are being enabled by an extension.
+extension-controlled-privacy-containers = An extension, <img data-l10n-name="icon"/> { $name }, requires Container Tabs.
+
+# This string is shown to notify the user that their tracking protection preferences
+# are being controlled by an extension.
+extension-controlled-websites-tracking-protection-mode = An extension, <img data-l10n-name="icon"/> { $name }, is controlling tracking protection.
+
+# This string is shown to notify the user that their proxy configuration preferences
+# are being controlled by an extension.
+extension-controlled-proxy-config = An extension, <img data-l10n-name="icon"/> { $name }, is controlling how { -brand-short-name } connects to the internet.
+
+# This string is shown after the user disables an extension to notify the user
+# how to enable an extension that they disabled.
+#
+# <img data-l10n-name="addons-icon"/> will be replaced with Add-ons icon
+# <img data-l10n-name="menu-icon"/> will be replaced with Menu icon
+extension-controlled-enable = To enable the extension go to <img data-l10n-name="addons-icon"/> Add-ons in the <img data-l10n-name="menu-icon"/> menu.
+
 ## Preferences UI Search Results
 
 search-results-header = Search Results
 
 # `<span data-l10n-name="query"></span>` will be replaced by the search term.
 search-results-empty-message =
     { PLATFORM() ->
         [windows] Sorry! There are no results in Options for “<span data-l10n-name="query"></span>”.
@@ -357,16 +398,18 @@ browsing-use-cursor-navigation =
 browsing-search-on-start-typing =
     .label = Search for text when you start typing
     .accesskey = x
 
 ## General Section - Proxy
 
 network-proxy-title = Network Proxy
 
+network-proxy-connection-description = Configure how { -brand-short-name } connects to the internet.
+
 network-proxy-connection-learn-more = Learn More
 
 network-proxy-connection-settings =
     .label = Settings…
     .accesskey = e
 
 ## Home Section
 
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -163,46 +163,8 @@ spaceAlert.over5GB.message1=%S is runnin
 # LOCALIZATION NOTE (spaceAlert.over5GB.messageWin1):
 # - On Windows Preferences is called Options
 # - %S = brandShortName
 spaceAlert.over5GB.messageWin1=%S is running out of disk space. Website contents may not display properly. You can clear stored data in Options > Privacy & Security > Cookies and Site Data.
 spaceAlert.under5GB.okButton.label=OK, Got it
 spaceAlert.under5GB.okButton.accesskey=K
 # LOCALIZATION NOTE (spaceAlert.under5GB.message): %S = brandShortName
 spaceAlert.under5GB.message=%S is running out of disk space. Website contents may not display properly. Visit “Learn More” to optimize your disk usage for better browsing experience.
-
-# LOCALIZATION NOTE (extensionControlled.homepage_override2):
-# This string is shown to notify the user that their home page is being controlled by an extension.
-extensionControlled.homepage_override2 = An extension, %S, is controlling your home page.
-
-# LOCALIZATION NOTE (extensionControlled.newTabURL2):
-# This string is shown to notify the user that their new tab page is being controlled by an extension.
-extensionControlled.newTabURL2 = An extension, %S, is controlling your New Tab page.
-
-# LOCALIZATION NOTE (extensionControlled.defaultSearch):
-# This string is shown to notify the user that the default search engine is being controlled
-# by an extension. %S is the icon and name of the extension.
-extensionControlled.defaultSearch = An extension, %S, has set your default search engine.
-
-# LOCALIZATION NOTE (extensionControlled.privacy.containers):
-# This string is shown to notify the user that Container Tabs are being enabled by an extension
-# %S is the container addon controlling it
-extensionControlled.privacy.containers = An extension, %S, requires Container Tabs.
-
-# LOCALIZATION NOTE (extensionControlled.websites.trackingProtectionMode):
-# This string is shown to notify the user that their tracking protection preferences are being controlled by an extension.
-extensionControlled.websites.trackingProtectionMode = An extension, %S, is controlling tracking protection.
-
-# LOCALIZATION NOTE (extensionControlled.proxyConfig):
-# This string is shown to notify the user that their proxy configuration preferences are being controlled by an extension.
-# %1$S is the icon and name of the extension.
-# %2$S is the brandShortName from brand.properties (for example "Nightly")
-extensionControlled.proxyConfig = An extension, %1$S, is controlling how %2$S connects to the internet.
-
-# LOCALIZATION NOTE (extensionControlled.enable):
-# %1$S is replaced with the icon for the add-ons menu.
-# %2$S is replaced with the icon for the toolbar menu.
-# This string is shown to notify the user how to enable an extension that they disabled.
-extensionControlled.enable = To enable the extension go to %1$S Add-ons in the %2$S menu.
-
-# LOCALIZATION NOTE (connectionDesc.label):
-# %S is the brandShortName from brand.properties (for example "Nightly")
-connectionDesc.label = Configure how %S connects to the internet.
new file mode 100644
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1438375_preferences_extension_controlled.py
@@ -0,0 +1,120 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+import fluent.syntax.ast as FTL
+from fluent.migrate.helpers import MESSAGE_REFERENCE, EXTERNAL_ARGUMENT
+from fluent.migrate import COPY, CONCAT, REPLACE
+
+
+def migrate(ctx):
+    """Bug 1438375 - Migrate Extension Controlled settings in Preferences to Fluent, part {index}."""
+
+    ctx.add_transforms(
+        'browser/browser/preferences/preferences.ftl',
+        'browser/browser/preferences/preferences.ftl',
+        [
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-homepage-override'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.homepage_override2',
+                    {
+                        '%S': CONCAT(
+                            FTL.TextElement('<img data-l10n-name="icon"/> '),
+                            EXTERNAL_ARGUMENT('name')
+                        )
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-new-tab-url'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.newTabURL2',
+                    {
+                        '%S': CONCAT(
+                            FTL.TextElement('<img data-l10n-name="icon"/> '),
+                            EXTERNAL_ARGUMENT('name')
+                        )
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-default-search'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.defaultSearch',
+                    {
+                        '%S': CONCAT(
+                            FTL.TextElement('<img data-l10n-name="icon"/> '),
+                            EXTERNAL_ARGUMENT('name')
+                        )
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-privacy-containers'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.privacy.containers',
+                    {
+                        '%S': CONCAT(
+                            FTL.TextElement('<img data-l10n-name="icon"/> '),
+                            EXTERNAL_ARGUMENT('name')
+                        )
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-websites-tracking-protection-mode'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.websites.trackingProtectionMode',
+                    {
+                        '%S': CONCAT(
+                            FTL.TextElement('<img data-l10n-name="icon"/> '),
+                            EXTERNAL_ARGUMENT('name')
+                        )
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-proxy-config'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.proxyConfig',
+                    {
+                        '%1$S': CONCAT(
+                            FTL.TextElement('<img data-l10n-name="icon"/> '),
+                            EXTERNAL_ARGUMENT('name')
+                        ),
+                        '%2$S': MESSAGE_REFERENCE('-brand-short-name'),
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('extension-controlled-enable'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'extensionControlled.enable',
+                    {
+                        '%1$S': FTL.TextElement('<img data-l10n-name="addons-icon"/>'),
+                        '%2$S': FTL.TextElement('<img data-l10n-name="menu-icon"/>'),
+                    }
+                )
+            ),
+            FTL.Message(
+                id=FTL.Identifier('network-proxy-connection-description'),
+                value=REPLACE(
+                    'browser/chrome/browser/preferences/preferences.properties',
+                    'connectionDesc.label',
+                    {
+                        '%S': MESSAGE_REFERENCE('-brand-short-name'),
+                    }
+                )
+            ),
+        ]
+    )