Bug 1450071 - Use fluent-react to localize application panel;r=flod,stas,ladybenko draft
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 28 May 2018 10:51:58 +0200
changeset 802593 31b6ccb2515425812770770417b0ed2f82a007f3
parent 802592 549200589ee8f33a950cf28787ae170ee43229c1
child 802733 99bb1976431ad71d545ba725729a9205cb151e48
child 802736 6b309908788cc61b7f4ad61da47c71edca73503a
child 802746 446d0861840a478ec1ed3bb5ecaf86add056c2f9
push id111921
push userjdescottes@mozilla.com
push dateFri, 01 Jun 2018 07:43:58 +0000
reviewersflod, stas, ladybenko
bugs1450071
milestone62.0a1
Bug 1450071 - Use fluent-react to localize application panel;r=flod,stas,ladybenko MozReview-Commit-ID: 8zWePxv6i33
devtools/client/application/initializer.js
devtools/client/application/src/components/App.js
devtools/client/application/src/components/Worker.css
devtools/client/application/src/components/Worker.js
devtools/client/application/src/components/WorkerList.css
devtools/client/application/src/components/WorkerList.js
devtools/client/application/src/components/WorkerListEmpty.js
devtools/client/locales/en-US/application.ftl
devtools/client/locales/jar.mn
--- a/devtools/client/application/initializer.js
+++ b/devtools/client/application/initializer.js
@@ -9,16 +9,18 @@ const require = BrowserLoader({
   baseURI: "resource://devtools/client/application/",
   window,
 }).require;
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
 const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
 const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { L10nRegistry } = require("resource://gre/modules/L10nRegistry.jsm");
+const Services = require("Services");
 
 const { configureStore } = require("./src/create-store");
 const actions = require("./src/actions/index");
 
 const App = createFactory(require("./src/components/App"));
 
 /**
  * Global Application object in this panel. This object is expected by panel.js and is
@@ -46,21 +48,41 @@ window.Application = {
     this.client.addListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
     this.client.addListener("registration-changed", this.updateWorkers);
     this.client.addListener("processListChanged", this.updateWorkers);
     this.toolbox.target.on("navigate", this.updateDomain);
 
     this.updateDomain();
     await this.updateWorkers();
 
+    const messageContexts = await this.createMessageContexts();
+
     // Render the root Application component.
-    const app = App({ client: this.client, serviceContainer });
+    const app = App({ client: this.client, messageContexts, serviceContainer });
     render(Provider({ store: this.store }, app), this.mount);
   },
 
+  /**
+   * Retrieve message contexts for the current locales, and return them as an array of
+   * MessageContext elements.
+   */
+  async createMessageContexts() {
+    const locales = Services.locale.getAppLocalesAsBCP47();
+    let generator = L10nRegistry.generateContexts(locales, ["devtools/application.ftl"]);
+
+    // Return value of generateContexts is a generator and should be converted to
+    // a sync iterable before using it with React.
+    let contexts = [];
+    for await (let message of generator) {
+      contexts.push(message);
+    }
+
+    return contexts;
+  },
+
   async updateWorkers() {
     let { service } = await this.client.mainRoot.listAllWorkers();
     this.actions.updateWorkers(service);
   },
 
   updateDomain() {
     this.actions.updateDomain(this.toolbox.target.url);
   },
--- a/devtools/client/application/src/components/App.js
+++ b/devtools/client/application/src/components/App.js
@@ -4,45 +4,51 @@
 
 "use strict";
 
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { createFactory, Component } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { main } = require("devtools/client/shared/vendor/react-dom-factories");
 
+const FluentReact = require("devtools/client/shared/vendor/fluent-react");
+const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
+
 const WorkerList = createFactory(require("./WorkerList"));
 const WorkerListEmpty = createFactory(require("./WorkerListEmpty"));
 
 /**
  * This is the main component for the application panel.
  */
 class App extends Component {
   static get propTypes() {
     return {
       client: PropTypes.object.isRequired,
       workers: PropTypes.object.isRequired,
       serviceContainer: PropTypes.object.isRequired,
       domain: PropTypes.string.isRequired,
+      messageContexts: PropTypes.array.isRequired,
     };
   }
 
   render() {
-    let { workers, domain, client, serviceContainer } = this.props;
+    let { workers, domain, client, serviceContainer, messageContexts } = this.props;
 
     // Filter out workers from other domains
     workers = workers.filter((x) => new URL(x.url).hostname === domain);
     const isEmpty = workers.length === 0;
 
     return (
-      main(
-        { className: `application ${isEmpty ? "application--empty" : ""}` },
-        isEmpty
-          ? WorkerListEmpty({ serviceContainer })
-          : WorkerList({ workers, client })
+      LocalizationProvider(
+        { messages: messageContexts },
+        main(
+          { className: `application ${isEmpty ? "application--empty" : ""}` },
+          isEmpty ? WorkerListEmpty({ serviceContainer })
+                  : WorkerList({ workers, client })
+        )
       )
     );
   }
 }
 
 // Exports
 
 module.exports = connect(
--- a/devtools/client/application/src/components/Worker.css
+++ b/devtools/client/application/src/components/Worker.css
@@ -68,13 +68,8 @@
 
 .worker__data > * {
   margin: 0;
 }
 
 .worker__data__updated {
   color: var(--theme-body-color-alt);
 }
-
-.worker__unregister-button {
-  /* TODO: remove this once/if we have proper capitalization in the strings file */
-  text-transform: capitalize;
-}
--- a/devtools/client/application/src/components/Worker.js
+++ b/devtools/client/application/src/components/Worker.js
@@ -1,29 +1,28 @@
 /* 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/. */
 
 "use strict";
 
-const { Component } = require("devtools/client/shared/vendor/react");
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { a, br, button, dd, dl, dt, header, li, section, span, time } =
   require("devtools/client/shared/vendor/react-dom-factories");
-const Services = require("Services");
 const { getUnicodeUrl, getUnicodeUrlPath } = require("devtools/client/shared/unicode-url");
 
+const FluentReact = require("devtools/client/shared/vendor/fluent-react");
+const Localized = createFactory(FluentReact.Localized);
+
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "gDevToolsBrowser",
   "devtools/client/framework/devtools-browser", true);
 
-const Strings = Services.strings.createBundle(
-  "chrome://devtools/locale/aboutdebugging.properties");
-
 /**
  * This component is dedicated to display a worker, more accurately a service worker, in
  * the list of workers displayed in the application panel. It displays information about
  * the worker as well as action links and buttons to interact with the worker (e.g. debug,
  * unregister, update etc...).
  */
 class Worker extends Component {
   static get propTypes() {
@@ -111,65 +110,88 @@ class Worker extends Component {
     return getUnicodeUrlPath(parts[parts.length - 1]);
   }
 
   render() {
     let { worker } = this.props;
     let status = this.getServiceWorkerStatus();
 
     const unregisterButton = this.isActive() ?
-      button({
-        onClick: this.unregister,
-        className: "devtools-button worker__unregister-button js-unregister-button",
-        "data-standalone": true
-      },
-        Strings.GetStringFromName("unregister"))
-      : null;
+      Localized(
+        { id: "serviceworker-worker-unregister" },
+        button({
+          onClick: this.unregister,
+          className: "devtools-button worker__unregister-button js-unregister-button",
+          "data-standalone": true
+        })
+      ) : null;
 
     const debugLinkDisabled = this.isRunning() ? "" : "disabled";
 
-    const debugLink = a({
-      onClick: this.isRunning() ? this.debug : null,
-      title: this.isRunning() ? null : "Only running service workers can be debugged",
-      className: `${debugLinkDisabled} worker__debug-link js-debug-link`
+    const debugLink = Localized({
+      id: "serviceworker-worker-debug",
+      // The localized title is only displayed if the debug link is disabled.
+      attrs: { title: !this.isRunning() }
     },
-      Strings.GetStringFromName("debug"));
+      a({
+        onClick: this.isRunning() ? this.debug : null,
+        className: `${debugLinkDisabled} worker__debug-link js-debug-link`
+      })
+    );
 
     const startLink = !this.isRunning() ?
-      a({ onClick: this.start, className: "worker__start-link" },
-        Strings.GetStringFromName("start"))
-      : null;
+      Localized(
+        { id: "serviceworker-worker-start" },
+        a({
+          onClick: this.start,
+          className: "worker__start-link"
+        })
+      ) : null;
 
-    const lastUpdated = worker.lastUpdateTime
-      ? span({ className: "worker__data__updated" },
-          "Updated ",
-          time({ className: "js-sw-updated"},
-            new Date(worker.lastUpdateTime / 1000).toLocaleString()))
-      : null;
+    const lastUpdated = worker.lastUpdateTime ?
+      Localized(
+        {
+          id: "serviceworker-worker-updated",
+          // XXX: $date should normally be a Date object, but we pass the timestamp as a
+          // workaround. See Bug 1465718. worker.lastUpdateTime is in microseconds,
+          // convert to a valid timestamp in milliseconds by dividing by 1000.
+          "$date": worker.lastUpdateTime / 1000,
+          time: time({ className: "js-sw-updated" })
+        },
+        span({ className: "worker__data__updated" })
+      ) : null;
 
     return li({ className: "worker js-sw-container" },
       header(
         { className: "worker__header" },
         span({ title: worker.scope, className: "worker__scope js-sw-scope" },
           this.formatScope(worker.scope)),
         section(
           { className: "worker__controls" },
           unregisterButton),
       ),
       dl(
         { className: "worker__data" },
-        dt({ className: "worker__meta-name" }, "Source"),
+        Localized({ id: "serviceworker-worker-source" },
+          dt({ className: "worker__meta-name" })
+        ),
         dd({},
             span({ title: worker.scope, className: "worker__source-url js-source-url" },
               this.formatSource(worker.url)),
             debugLink,
             lastUpdated ? br({}) : null,
             lastUpdated ? lastUpdated : null),
-        dt({ className: "worker__meta-name" }, "Status"),
+        Localized({ id: "serviceworker-worker-status" },
+          dt({ className: "worker__meta-name" })
+        ),
         dd({},
-          Strings.GetStringFromName(status).toLowerCase(),
-          startLink)
+          Localized(
+            { id: "serviceworker-worker-status-" + status },
+            span({}),
+          ),
+          startLink
+        )
       )
     );
   }
 }
 
 module.exports = Worker;
--- a/devtools/client/application/src/components/WorkerList.css
+++ b/devtools/client/application/src/components/WorkerList.css
@@ -3,14 +3,14 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .aboutdebugging-plug {
   text-align: right;
   padding: 1rem 0;
 }
 
 .aboutdebugging-plug__link {
-  margin-right: 0;
+  margin: 0;
 }
 
 .workers-container {
   flex-grow: 1;
 }
--- a/devtools/client/application/src/components/WorkerList.js
+++ b/devtools/client/application/src/components/WorkerList.js
@@ -5,16 +5,19 @@
 "use strict";
 
 const { openTrustedLink } = require("devtools/client/shared/link");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { createFactory, Component } = require("devtools/client/shared/vendor/react");
 const { a, article, footer, h1, ul } = require("devtools/client/shared/vendor/react-dom-factories");
 const Worker = createFactory(require("./Worker"));
 
+const FluentReact = require("devtools/client/shared/vendor/fluent-react");
+const Localized = createFactory(FluentReact.Localized);
+
 /**
  * This component handles the list of service workers displayed in the application panel
  * and also displays a suggestion to use about debugging for debugging other service
  * workers.
  */
 class WorkerList extends Component {
   static get propTypes() {
     return {
@@ -23,30 +26,38 @@ class WorkerList extends Component {
     };
   }
 
   render() {
     const { workers, client } = this.props;
 
     return [
       article({ className: "workers-container" },
-        h1({}, "Service Workers"),
+        Localized(
+          { id: "serviceworker-list-header" },
+          h1({})
+        ),
         ul({},
           workers.map(worker => Worker({
             client,
             debugDisabled: false,
             worker,
           })))
       ),
-      footer({ className: "aboutdebugging-plug" },
-        "See about:debugging for Service Workers from other domains",
-        a({ className: "aboutdebugging-plug__link",
-            onClick: () => openTrustedLink("about:debugging#workers") },
-          "Open about:debugging"
-        )
+      Localized(
+        {
+          id: "serviceworker-list-aboutdebugging",
+          a: a(
+            {
+              className: "aboutdebugging-plug__link",
+              onClick: () => openTrustedLink("about:debugging#workers")
+            }
+          )
+        },
+        footer({ className: "aboutdebugging-plug" })
       )
     ];
   }
 }
 
 // Exports
 
 module.exports = WorkerList;
--- a/devtools/client/application/src/components/WorkerListEmpty.js
+++ b/devtools/client/application/src/components/WorkerListEmpty.js
@@ -1,18 +1,22 @@
 /* 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/. */
 
 "use strict";
 
 const { openWebLink, openTrustedLink } = require("devtools/client/shared/link");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const { Component } = require("devtools/client/shared/vendor/react");
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
 const { a, article, h1, li, p, ul } = require("devtools/client/shared/vendor/react-dom-factories");
+
+const FluentReact = require("devtools/client/shared/vendor/fluent-react");
+const Localized = createFactory(FluentReact.Localized);
+
 const DOC_URL = "https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers" +
   "?utm_source=devtools&utm_medium=sw-panel-blank";
 
 /**
  * This component displays help information when no service workers are found for the
  * current target.
  */
 class WorkerListEmpty extends Component {
@@ -36,55 +40,46 @@ class WorkerListEmpty extends Component 
 
   openDocumentation() {
     openWebLink(DOC_URL);
   }
 
   render() {
     return article(
       { className: "worker-list-empty" },
-      h1(
-        { className: "worker-list-empty__title" },
-        "You need to register a Service Worker to inspect it here.",
-        a(
-          { className: "external-link", onClick: () => this.openDocumentation() },
-          "Learn More"
-        )
+      Localized({
+        id: "serviceworker-empty-intro",
+        a: a({ className: "external-link", onClick: () => this.openDocumentation() })
+      },
+        h1({ className: "worker-list-empty__title" })
       ),
-      p(
-        {},
-        `If the current page should have a service worker, ` +
-        `here are some things you can try:`,
+      Localized(
+        { id: "serviceworker-empty-suggestions" },
+        p({})
       ),
       ul(
         { className: "worker-list-empty__tips" },
-        li(
-          { className: "worker-list-empty__tips__item" },
-          "Look for errors in the Console.",
-          a(
-            { className: "link", onClick: () => this.switchToConsole() },
-            "Open the Console"
-          )
+        Localized({
+          id: "serviceworker-empty-suggestions-console",
+          a: a({ className: "link", onClick: () => this.switchToConsole() })
+        },
+          li({ className: "worker-list-empty__tips__item" })
         ),
-        li(
-          { className: "worker-list-empty__tips__item" },
-          "Step through you Service Worker registration and look for exceptions.",
-          a(
-            { className: "link", onClick: () => this.switchToDebugger()},
-            "Open the Debugger"
-          )
+        Localized({
+          id: "serviceworker-empty-suggestions-debugger",
+          a: a({ className: "link", onClick: () => this.switchToDebugger() })
+        },
+          li({ className: "worker-list-empty__tips__item" })
         ),
-        li(
-          { className: "worker-list-empty__tips__item" },
-          "Inspect Service Workers from other domains.",
-          a(
-            { className: "external-link", onClick: () => this.openAboutDebugging() },
-            "Open about:debugging"
-          )
-        )
+        Localized({
+          id: "serviceworker-empty-suggestions-aboutdebugging",
+          a: a({ className: "link", onClick: () => this.openAboutDebugging() })
+        },
+          li({ className: "worker-list-empty__tips__item" })
+        ),
       )
     );
   }
 }
 
 // Exports
 
 module.exports = WorkerListEmpty;
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/application.ftl
@@ -0,0 +1,75 @@
+# 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/.
+
+### These strings are used inside the Application panel which is available
+### by setting the preference `devtools-application-enabled` to true.
+
+### The correct localization of this file might be to keep it in English, or another
+### language commonly spoken among web developers. You want to make that choice consistent
+### across the developer tools. A good criteria is the language in which you'd find the
+### best documentation on web development on the web.
+
+# Header for the list of Service Workers displayed in the application panel for the current page.
+serviceworker-list-header = Service Workers
+
+# Text displayed next to the list of Service Workers to encourage users to check out
+# about:debugging to see all registered Service Workers.
+serviceworker-list-aboutdebugging = Open <a>about:debugging</a> for Service Workers from other domains
+
+# Text for the button to unregister a Service Worker. Displayed for active Service Workers.
+serviceworker-worker-unregister = Unregister
+
+# Text for the debug link displayed for an already started Service Worker. Clicking on the
+# link opens a new devtools toolbox for this service worker. The title attribute is only
+# displayed when the link is disabled.
+serviceworker-worker-debug = Debug
+  .title = Only running service workers can be debugged
+
+# Text for the start link displayed for a registered but not running Service Worker.
+# Clicking on the link will attempt to start the service worker.
+serviceworker-worker-start = Start
+
+# Text displayed for the updated time of the service worker. The <time> element will
+# display the last update time of the service worker script.
+serviceworker-worker-updated = Updated <time>{ DATETIME($date, month: "long", year: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric") }</time>
+
+# Text displayed next to the URL for the source of the service worker (e-g. "Source my/path/to/worker-js")
+serviceworker-worker-source = Source
+
+# Text displayed next to the current status of the service worker.
+serviceworker-worker-status = Status
+
+## Service Worker status strings: all serviceworker-worker-status-* strings are also
+## defined in aboutdebugging.properties and should be synchronized with them.
+
+# Service Worker status. A running service worker is registered, currently executed, can
+# be debugged and stopped.
+serviceworker-worker-status-running = Running
+
+# Service Worker status. A stopped service worker is registered but not currently active.
+serviceworker-worker-status-stopped = Stopped
+
+# Service Worker status. A registering service worker is not yet registered and cannot be
+# started or debugged.
+serviceworker-worker-status-registering = Registering
+
+# Text displayed when no service workers are visible for the current page. Clicking on the
+# link will open https://developer-mozilla-org/docs/Web/API/Service_Worker_API/Using_Service_Workers
+serviceworker-empty-intro = You need to register a Service Worker to inspect it here. <a>Learn more</a>
+
+# Text displayed when there are no Service Workers to display for the current page,
+# introducing hints to debug Service Worker issues.
+serviceworker-empty-suggestions = If the current page should have a service worker, here are some things you can try
+
+# Suggestion to check for errors in the Console to investigate why a service worker is not
+# registered. Clicking on the link opens the webconsole.
+serviceworker-empty-suggestions-console = Look for errors in the Console. <a>Open the Console</a>
+
+# Suggestion to use the debugger to investigate why a service worker is not registered.
+# Clicking on the link will switch from the Application panel to the debugger.
+serviceworker-empty-suggestions-debugger = Step through your Service Worker registration and look for exceptions. <a>Open the Debugger</a>
+
+# Suggestion to go to about:debugging in order to see Service Workers for all domains.
+# Clicking on the link will open about:debugging in a new tab.
+serviceworker-empty-suggestions-aboutdebugging = Inspect Service Workers from other domains. <a>Open about:debugging</a>
--- a/devtools/client/locales/jar.mn
+++ b/devtools/client/locales/jar.mn
@@ -1,8 +1,13 @@
 #filter substitution
 # 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/.
 
+[localization] @AB_CD@.jar:
+    devtools (en-US/**/*.ftl)
+
 @AB_CD@.jar:
 %   locale devtools @AB_CD@ %locale/@AB_CD@/devtools/client/
-    locale/@AB_CD@/devtools/client/ (%*)
+    locale/@AB_CD@/devtools/client/ (%*.properties)
+    locale/@AB_CD@/devtools/client/ (%*.dtd)
+