Bug 1591328 - Migrate the window title to Fluent. r=fluent-reviewers,Gijs,mixedpuppy
authorZibi Braniecki <zbraniecki@mozilla.com>
Thu, 21 Nov 2019 19:16:15 +0000
changeset 503388 bff9eef8191f12cee9af275966a563517dcdf429
parent 503387 0bcc6c8444dba17eba5d340c54734fb808180583
child 503389 b1d9e0931ccedb4d7e60f6b83f281f7f2d398d1a
push id36833
push userbtara@mozilla.com
push dateFri, 22 Nov 2019 21:40:53 +0000
treeherdermozilla-central@2c912e46295e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfluent-reviewers, Gijs, mixedpuppy
bugs1591328
milestone72.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 1591328 - Migrate the window title to Fluent. r=fluent-reviewers,Gijs,mixedpuppy Depends on D53775 Differential Revision: https://phabricator.services.mozilla.com/D53776
browser/base/content/browser.js
browser/base/content/browser.xhtml
browser/base/content/tabbrowser.js
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/parent/ext-windows.js
browser/components/extensions/test/browser/browser_ext_windows.js
browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js
browser/locales/en-US/browser/browser.ftl
browser/modules/WindowsPreviewPerTab.jsm
python/l10n/fluent_migrations/bug_1591328_browser_title.py
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -8512,23 +8512,16 @@ var gPrivateBrowsingUI = {
     document.getElementById("Tools:Sanitize").setAttribute("disabled", "true");
 
     if (window.location.href != AppConstants.BROWSER_CHROME_URL) {
       return;
     }
 
     // Adjust the window's title
     let docElement = document.documentElement;
-    if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
-      docElement.title = docElement.getAttribute("title_privatebrowsing");
-      docElement.setAttribute(
-        "titlemodifier",
-        docElement.getAttribute("titlemodifier_privatebrowsing")
-      );
-    }
     docElement.setAttribute(
       "privatebrowsingmode",
       PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary"
     );
     gBrowser.updateTitlebar();
 
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       // Adjust the New Window menu entries
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -37,36 +37,22 @@
 ]>
 
 <html id="main-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:svg="http://www.w3.org/2000/svg"
         xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns="http://www.w3.org/1999/xhtml"
-        title_normal="&mainWindow.title;"
-#ifdef XP_MACOSX
-        title_privatebrowsing="&mainWindow.title;&mainWindow.titlemodifiermenuseparator;&mainWindow.titlePrivateBrowsingSuffix;"
-        titledefault="&mainWindow.title;"
-        titlemodifier=""
-        titlemodifier_normal=""
-        titlemodifier_privatebrowsing="&mainWindow.titlePrivateBrowsingSuffix;"
-#else
-        title_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;"
-        titlemodifier="&mainWindow.titlemodifier;"
-        titlemodifier_normal="&mainWindow.titlemodifier;"
-        titlemodifier_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;"
-#endif
 #ifdef XP_WIN
         chromemargin="0,2,2,2"
 #else
         chromemargin="0,-1,-1,-1"
 #endif
         tabsintitlebar="true"
-        titlemenuseparator="&mainWindow.titlemodifiermenuseparator;"
         windowtype="navigator:browser"
         macanimationtype="document"
         screenX="4" screenY="4"
         fullscreenbutton="true"
         sizemode="normal"
         retargetdocumentfocus="urlbar-input"
         scrolling="false"
         persist="screenX screenY width height sizemode"
@@ -74,17 +60,19 @@
 <head>
   <link rel="localization" href="branding/brand.ftl"/>
   <link rel="localization" href="browser/branding/sync-brand.ftl"/>
   <link rel="localization" href="browser/browser.ftl"/>
   <link rel="localization" href="browser/menubar.ftl"/>
   <link rel="localization" href="browser/protectionsPanel.ftl"/>
   <link rel="localization" href="browser/appmenu.ftl"/>
 
-  <title>&mainWindow.title;</title>
+  <title
+    data-l10n-id="browser-main-window-title"
+    data-l10n-args='{"mode": "default"}'></title>
 
 # All JS files which are needed by browser.xhtml and other top level windows to
 # support MacOS specific features *must* go into the global-scripts.inc file so
 # that they can be shared with macWindow.inc.xhtml.
 #include global-scripts.inc
 
 <script>
   /* eslint-env mozilla/browser-window */
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -204,16 +204,23 @@
     _multiSelectChangeSelected: false,
 
     /**
      * Tab close requests are ignored if the window is closing anyway,
      * e.g. when holding Ctrl+W.
      */
     _windowIsClosing: false,
 
+    /**
+     * We'll use this to cache the accessor to the title element.
+     * It's important that the defualt is `undefined`, so that it
+     * can be set to `null` by the `querySelector`.
+     */
+    _titleElement: undefined,
+
     preloadedBrowser: null,
 
     /**
      * This defines a proxy which allows us to access browsers by
      * index without actually creating a full array of browsers.
      */
     browsers: new Proxy([], {
       has: (target, name) => {
@@ -948,73 +955,87 @@
           description: aDescription,
           previewImageURL: aPreviewImage,
         };
         PlacesUtils.history.update(pageInfo).catch(Cu.reportError);
       }
     },
 
     getWindowTitleForBrowser(aBrowser) {
-      var newTitle = "";
-      var docElement = document.documentElement;
-      var sep = docElement.getAttribute("titlemenuseparator");
-      let tab = this.getTabForBrowser(aBrowser);
-      let docTitle;
-
-      if (tab._labelIsContentTitle) {
-        // Strip out any null bytes in the content title, since the
-        // underlying widget implementations of nsWindow::SetTitle pass
-        // null-terminated strings to system APIs.
-        docTitle = tab.getAttribute("label").replace(/\0/g, "");
-      }
-
-      if (!docTitle) {
-        docTitle = docElement.getAttribute("titledefault");
-      }
-
-      var modifier = docElement.getAttribute("titlemodifier");
-      if (docTitle) {
-        newTitle += docElement.getAttribute("titlepreface") || "";
-        newTitle += docTitle;
-        if (modifier) {
-          newTitle += sep;
-        }
-      }
-      newTitle += modifier;
+      let title = "";
+
+      let docElement = document.documentElement;
 
       // If location bar is hidden and the URL type supports a host,
       // add the scheme and host to the title to prevent spoofing.
       // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
       try {
         if (docElement.getAttribute("chromehidden").includes("location")) {
           const uri = Services.uriFixup.createExposableURI(aBrowser.currentURI);
-          if (uri.scheme === "about") {
-            newTitle = `${uri.spec}${sep}${newTitle}`;
-          } else if (uri.scheme === "moz-extension") {
+          let prefix = uri.prePath;
+          if (uri.scheme == "about") {
+            prefix = uri.spec;
+          } else if (uri.scheme == "moz-extension") {
             const ext = WebExtensionPolicy.getByHostname(uri.host);
             if (ext && ext.name) {
-              const prefix = document.querySelector("#urlbar-label-extension")
-                .value;
-              newTitle = `${prefix} (${ext.name})${sep}${newTitle}`;
-            } else {
-              newTitle = `${uri.prePath}${sep}${newTitle}`;
+              let extensionLabel = document.getElementById(
+                "urlbar-label-extension"
+              );
+              prefix = `${extensionLabel.value} (${ext.name})`;
             }
-          } else {
-            newTitle = `${uri.prePath}${sep}${newTitle}`;
           }
+          title = prefix + " - ";
         }
       } catch (e) {
         // ignored
       }
 
-      return newTitle;
-    },
-
-    updateTitlebar() {
-      document.title = this.getWindowTitleForBrowser(this.selectedBrowser);
+      if (docElement.hasAttribute("titlepreface")) {
+        title += docElement.getAttribute("titlepreface");
+      }
+
+      let tab = this.getTabForBrowser(aBrowser);
+
+      if (tab._labelIsContentTitle) {
+        // Strip out any null bytes in the content title, since the
+        // underlying widget implementations of nsWindow::SetTitle pass
+        // null-terminated strings to system APIs.
+        title += tab.getAttribute("label").replace(/\0/g, "");
+      }
+
+      let mode =
+        docElement.getAttribute("privatebrowsingmode") == "temporary"
+          ? "private"
+          : "default";
+
+      if (title) {
+        return {
+          id: "browser-main-window-content-title",
+          args: {
+            title,
+            mode,
+          },
+        };
+      }
+      return {
+        id: "browser-main-window-title",
+        args: {
+          mode,
+        },
+      };
+    },
+
+    async updateTitlebar() {
+      if (!this._titleElement) {
+        this._titleElement = document.documentElement.querySelector("title");
+      }
+
+      let { id, args } = this.getWindowTitleForBrowser(this.selectedBrowser);
+      document.l10n.setAttributes(this._titleElement, id, args);
+      await document.l10n.translateElements([this._titleElement]);
     },
 
     updateCurrentBrowser(aForceUpdate) {
       let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
       if (this.selectedBrowser == newBrowser && !aForceUpdate) {
         return;
       }
 
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -973,20 +973,24 @@ class Window extends WindowBase {
     }
   }
 
   get _title() {
     return this.window.document.title;
   }
 
   setTitlePreface(titlePreface) {
-    this.window.document.documentElement.setAttribute(
-      "titlepreface",
-      titlePreface
-    );
+    if (!titlePreface) {
+      this.window.document.documentElement.removeAttribute("titlepreface");
+    } else {
+      this.window.document.documentElement.setAttribute(
+        "titlepreface",
+        titlePreface
+      );
+    }
   }
 
   get focused() {
     return this.window.document.hasFocus();
   }
 
   get top() {
     return this.window.screenY;
--- a/browser/components/extensions/parent/ext-windows.js
+++ b/browser/components/extensions/parent/ext-windows.js
@@ -375,17 +375,17 @@ this.windows = class extends ExtensionAP
             }
             if (createData.titlePreface !== null) {
               win.setTitlePreface(createData.titlePreface);
             }
             return win.convert({ populate: true });
           });
         },
 
-        update: function(windowId, updateInfo) {
+        update: async function(windowId, updateInfo) {
           if (updateInfo.state !== null && updateInfo.state != "normal") {
             if (
               updateInfo.left !== null ||
               updateInfo.top !== null ||
               updateInfo.width !== null ||
               updateInfo.height !== null
             ) {
               return Promise.reject({
@@ -414,22 +414,22 @@ this.windows = class extends ExtensionAP
             // Bug 1257497 - Firefox can't cancel attention actions.
             win.window.getAttention();
           }
 
           win.updateGeometry(updateInfo);
 
           if (updateInfo.titlePreface !== null) {
             win.setTitlePreface(updateInfo.titlePreface);
-            win.window.gBrowser.updateTitlebar();
+            await win.window.gBrowser.updateTitlebar();
           }
 
           // TODO: All the other properties, focused=false...
 
-          return Promise.resolve(win.convert());
+          return win.convert();
         },
 
         remove: function(windowId) {
           let window = windowTracker.getWindow(windowId, context);
           if (!context.canAccessWindow(window)) {
             return Promise.reject({
               message: `Invalid window ID: ${windowId}`,
             });
--- a/browser/components/extensions/test/browser/browser_ext_windows.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows.js
@@ -1,12 +1,20 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+// Since we apply title localization asynchronously,
+// we'll use this helper to wait for the title to match
+// the condition and then test against it.
+async function verifyTitle(win, test, desc) {
+  await TestUtils.waitForCondition(test);
+  ok(true, desc);
+}
+
 add_task(async function testWindowGetAll() {
   let raisedWin = Services.ww.openWindow(
     null,
     AppConstants.BROWSER_CHROME_URL,
     "_blank",
     "chrome,dialog=no,all,alwaysRaised",
     null
   );
@@ -123,38 +131,43 @@ add_task(async function testWindowTitle(
 
   async function createApiWin(options) {
     let promiseLoaded = BrowserTestUtils.waitForNewWindow({ url: START_URL });
     extension.sendMessage("create", options);
     let apiWin = await extension.awaitMessage("created");
     let realWin = windowTracker.getWindow(apiWin.id);
     await promiseLoaded;
     let expectedPreface = options.titlePreface ? options.titlePreface : "";
-    ok(
-      realWin.document.title.startsWith(expectedPreface),
-      "Created window has the expected title preface."
-    );
-    ok(
-      realWin.document.title.includes(START_TITLE),
-      "Created window has the expected title text."
+    await verifyTitle(
+      realWin,
+      () => {
+        return (
+          realWin.document.title.startsWith(expectedPreface || START_TITLE) &&
+          realWin.document.title.includes(START_TITLE)
+        );
+      },
+      "Created window starts with the expected preface and includes the right title text."
     );
     return apiWin;
   }
 
   async function updateWindow(options, apiWin, expected) {
     extension.sendMessage("update", options, apiWin.id, expected);
     await extension.awaitMessage("updated");
     let realWin = windowTracker.getWindow(apiWin.id);
-    ok(
-      realWin.document.title.startsWith(expected.after.preface),
-      "Updated window has the expected title preface."
-    );
-    ok(
-      realWin.document.title.includes(expected.after.text),
-      "Updated window has the expected title text."
+    await verifyTitle(
+      realWin,
+      () => {
+        return (
+          realWin.document.title.startsWith(
+            expected.after.preface || expected.after.text
+          ) && realWin.document.title.includes(expected.after.text)
+        );
+      },
+      "Updated window starts with the expected preface and includes the right title text."
     );
     await BrowserTestUtils.closeWindow(realWin);
   }
 
   // Create a window without a preface.
   let apiWin = await createApiWin({ url: START_URL });
 
   // Add a titlePreface to the window.
@@ -175,23 +188,25 @@ add_task(async function testWindowTitle(
 
   // Navigate to a different url and check that title is reflected.
   let realWin = windowTracker.getWindow(apiWin.id);
   let promiseLoaded = BrowserTestUtils.browserLoaded(
     realWin.gBrowser.selectedBrowser
   );
   await BrowserTestUtils.loadURI(realWin.gBrowser.selectedBrowser, NEW_URL);
   await promiseLoaded;
-  ok(
-    realWin.document.title.startsWith(PREFACE1),
-    "Updated window has the expected title preface."
-  );
-  ok(
-    realWin.document.title.includes(NEW_TITLE),
-    "Updated window has the expected title text."
+  await verifyTitle(
+    realWin,
+    () => {
+      return (
+        realWin.document.title.startsWith(PREFACE1) &&
+        realWin.document.title.includes(NEW_TITLE)
+      );
+    },
+    "Updated window starts with the expected preface and includes the expected title."
   );
 
   // Update the titlePreface of the window.
   expected = {
     before: {
       preface: PREFACE1,
       text: NEW_TITLE,
     },
@@ -212,20 +227,26 @@ add_task(async function testWindowTitle(
       preface: PREFACE1,
       text: START_TITLE,
     },
     after: {
       preface: "",
       text: START_TITLE,
     },
   };
+  await verifyTitle(
+    realWin,
+    () => realWin.document.title.startsWith(expected.before.preface),
+    "Updated window has the expected title preface."
+  );
   await updateWindow({ titlePreface: "" }, apiWin, expected);
-  ok(
-    !realWin.document.title.startsWith(expected.before.preface),
-    "Updated window has the expected empty title preface."
+  await verifyTitle(
+    realWin,
+    () => !realWin.document.title.startsWith(expected.before.preface),
+    "Updated window doesn't not contain the preface after update."
   );
 
   // Create a window with a preface.
   apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
   realWin = windowTracker.getWindow(apiWin.id);
 
   // Update the window without a titlePreface.
   expected = {
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js
@@ -9,38 +9,26 @@ add_task(async function test() {
   const testPageURL =
     "http://mochi.test:8888/browser/" +
     "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html";
   requestLongerTimeout(2);
 
   // initialization of expected titles
   let test_title = "Test title";
   let app_name = document.title;
-  const isOSX = "nsILocalFileMac" in Ci;
-  let page_with_title;
-  let page_without_title;
-  let about_pb_title;
-  let pb_page_with_title;
-  let pb_page_without_title;
-  let pb_about_pb_title;
-  if (isOSX) {
-    page_with_title = test_title;
-    page_without_title = app_name;
-    about_pb_title = app_name;
-    pb_page_with_title = test_title + " - (Private Browsing)";
-    pb_page_without_title = app_name + " - (Private Browsing)";
-    pb_about_pb_title = app_name + " - (Private Browsing)";
-  } else {
-    page_with_title = test_title + " - " + app_name;
-    page_without_title = app_name;
-    about_pb_title = app_name;
-    pb_page_with_title = test_title + " - " + app_name + " (Private Browsing)";
-    pb_page_without_title = app_name + " (Private Browsing)";
-    pb_about_pb_title = app_name + " (Private Browsing)";
-  }
+
+  // XXX: Bug 1597849 - Dehardcode titles by fetching them from Fluent
+  //                    to compare with the actual values.
+  let page_with_title = test_title + " - " + app_name;
+  let page_without_title = app_name;
+  let about_pb_title = app_name;
+  let pb_page_with_title =
+    test_title + " - " + app_name + " (Private Browsing)";
+  let pb_page_without_title = app_name + " (Private Browsing)";
+  let pb_about_pb_title = app_name + " (Private Browsing)";
 
   async function testTabTitle(aWindow, url, insidePB, expected_title) {
     let tab = await BrowserTestUtils.openNewForegroundTab(aWindow.gBrowser);
     await BrowserTestUtils.loadURI(tab.linkedBrowser, url);
     await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
     await BrowserTestUtils.waitForCondition(() => {
       return aWindow.document.title === expected_title;
--- a/browser/locales/en-US/browser/browser.ftl
+++ b/browser/locales/en-US/browser/browser.ftl
@@ -1,12 +1,44 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+
+# This is the default window title in case there is no content
+# title to be displayed.
+#
+# Depending on the $mode, the string will look like this (in en-US):
+#
+# "default" - "Mozilla Firefox"
+# "private" - "Mozilla Firefox (Private Browsing)"
+#
+# Variables
+#   $mode (String) - "private" in case of a private browsing mode, "default" otherwise.
+browser-main-window-title = { $mode ->
+        [private] { -brand-full-name } (Private Browsing)
+       *[default] { -brand-full-name }
+    }
+
+# This is the default window title in case there is a content
+# title to be displayed.
+#
+# Depending on the $mode, the string will look like this (in en-US):
+#
+# "default" - "Example Title - Mozilla Firefox"
+# "private" - "Example Title - Mozilla Firefox (Private Browsing)"
+#
+# Variables
+#   $mode (String) - "private" in case of a private browsing mode, "default" otherwise.
+#   $title (String) - Content title string.
+browser-main-window-content-title = { $mode ->
+        [private] { $title } - { -brand-full-name } (Private Browsing)
+       *[default] { $title } - { -brand-full-name }
+    }
+
 urlbar-identity-button =
     .aria-label = View site information
 
 ## Tooltips for images appearing in the address bar
 
 urlbar-services-notification-anchor =
     .tooltiptext = Open install message panel
 urlbar-web-notification-anchor =
--- a/browser/modules/WindowsPreviewPerTab.jsm
+++ b/browser/modules/WindowsPreviewPerTab.jsm
@@ -242,22 +242,29 @@ PreviewController.prototype = {
     AeroPeek.resetCacheTimer();
     return PageThumbs.captureToCanvas(this.linkedBrowser, this.canvasPreview, {
       fullScale: aFullScale,
     }).catch(e => Cu.reportError(e));
     // If we're updating the canvas, then we're in the middle of a peek so
     // don't discard the cache of previews.
   },
 
-  updateTitleAndTooltip() {
-    let title = this.win.tabbrowser.getWindowTitleForBrowser(
+  async updateTitleAndTooltip() {
+    let { id, args } = this.win.tabbrowser.getWindowTitleForBrowser(
       this.linkedBrowser
     );
-    this.preview.title = title;
-    this.preview.tooltip = title;
+    let title = await this.win.tabbrowser.ownerDocument.l10n.formatValue(
+      id,
+      args
+    );
+    // Since the previous call is async, the `this.preview` may become empty.
+    if (this.preview) {
+      this.preview.title = title;
+      this.preview.tooltip = title;
+    }
   },
 
   // nsITaskbarPreviewController
 
   // window width and height, not browser
   get width() {
     return this.win.width;
   },
new file mode 100644
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1591328_browser_title.py
@@ -0,0 +1,28 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+    """Bug 1591328 - Migrate browser window title to Fluent, part {index}"""
+
+    ctx.add_transforms(
+        'browser/browser/browser.ftl',
+        'browser/browser/browser.ftl',
+        transforms_from(
+"""
+browser-main-window-title = { $mode ->
+        [private] { -brand-full-name } {COPY(from_path, "mainWindow.titlePrivateBrowsingSuffix")}
+       *[default] { -brand-full-name }
+    }
+browser-main-window-content-title = { $mode ->
+        [private] { $title } - { -brand-full-name } {COPY(from_path, "mainWindow.titlePrivateBrowsingSuffix")}
+       *[default] { $title } - { -brand-full-name }
+    }
+""", from_path="browser/chrome/browser/browser.dtd")
+    )
+