Bug 1214955: [webext] Automatically localize all localizable manifest properties. r=billm
authorKris Maglione <maglione.k@gmail.com>
Mon, 29 Feb 2016 19:34:49 -0800
changeset 324545 4c924e5c274980190d1e897fd2fd8cc3c87da0f0
parent 324544 47eb779302fa63ada9a1f7e10c0107ea0b08a9bd
child 324546 0a65c11157dac3a0940bd834a10bc3ff5702d2f2
push id1128
push userjlund@mozilla.com
push dateWed, 01 Jun 2016 01:31:59 +0000
treeherdermozilla-release@fe0d30de989d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1214955
milestone47.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 1214955: [webext] Automatically localize all localizable manifest properties. r=billm MozReview-Commit-ID: 2kvYT44NIE8
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-utils.js
browser/components/extensions/schemas/browser_action.json
browser/components/extensions/schemas/page_action.json
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
browser/components/extensions/test/browser/browser_ext_pageAction_context.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/ext-backgroundPage.js
toolkit/components/extensions/schemas/manifest.json
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
toolkit/components/extensions/test/xpcshell/test_locale_data.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -28,30 +28,23 @@ function BrowserAction(options, extensio
 
   let widgetId = makeWidgetId(extension.id);
   this.id = `${widgetId}-browser-action`;
   this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
   this.widget = null;
 
   this.tabManager = TabManager.for(extension);
 
-  let title = extension.localize(options.default_title || "");
-  let popup = extension.localize(options.default_popup || "");
-  if (popup) {
-    popup = extension.baseURI.resolve(popup);
-  }
-
   this.defaults = {
     enabled: true,
-    title: title || extension.name,
+    title: options.default_title || extension.name,
     badgeText: "",
     badgeBackgroundColor: null,
-    icon: IconDetails.normalize({path: options.default_icon}, extension,
-                                null, true),
-    popup: popup,
+    icon: IconDetails.normalize({path: options.default_icon}, extension),
+    popup: options.default_popup || "",
   };
 
   this.tabContext = new TabContext(tab => Object.create(this.defaults),
                                    extension);
 
   EventEmitter.decorate(this);
 }
 
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -14,28 +14,21 @@ var pageActionMap = new WeakMap();
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
 function PageAction(options, extension) {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-page-action";
 
   this.tabManager = TabManager.for(extension);
 
-  let title = extension.localize(options.default_title || "");
-  let popup = extension.localize(options.default_popup || "");
-  if (popup) {
-    popup = extension.baseURI.resolve(popup);
-  }
-
   this.defaults = {
     show: false,
-    title: title || extension.name,
-    icon: IconDetails.normalize({path: options.default_icon}, extension,
-                                null, true),
-    popup: popup && extension.baseURI.resolve(popup),
+    title: options.default_title || extension.name,
+    icon: IconDetails.normalize({path: options.default_icon}, extension),
+    popup: options.default_popup || "",
   };
 
   this.tabContext = new TabContext(tab => Object.create(this.defaults),
                                    extension);
 
   this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
 
   // WeakMap[ChromeWindow -> <xul:image>]
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -31,17 +31,17 @@ global.IconDetails = {
   // with icon size as key and icon URL as value.
   //
   // If a context is specified (function is called from an extension):
   // Throws an error if an invalid icon size was provided or the
   // extension is not allowed to load the specified resources.
   //
   // If no context is specified, instead of throwing an error, this
   // function simply logs a warning message.
-  normalize(details, extension, context = null, localize = false) {
+  normalize(details, extension, context = null) {
     let result = {};
 
     try {
       if (details.imageData) {
         let imageData = details.imageData;
 
         // The global might actually be from Schema.jsm, which
         // normalizes most of our arguments. In that case it won't have
@@ -68,22 +68,17 @@ global.IconDetails = {
 
         let baseURI = context ? context.uri : extension.baseURI;
 
         for (let size of Object.keys(path)) {
           if (!INTEGER.test(size)) {
             throw new Error(`Invalid icon size ${size}, must be an integer`);
           }
 
-          let url = path[size];
-          if (localize) {
-            url = extension.localize(url);
-          }
-
-          url = baseURI.resolve(path[size]);
+          let url = baseURI.resolve(path[size]);
 
           // The Chrome documentation specifies these parameters as
           // relative paths. We currently accept absolute URLs as well,
           // which means we need to check that the extension is allowed
           // to load them. This will throw an error if it's not allowed.
           Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
             extension.principal, url,
             Services.scriptSecurityManager.DISALLOW_SCRIPT);
--- a/browser/components/extensions/schemas/browser_action.json
+++ b/browser/components/extensions/schemas/browser_action.json
@@ -7,19 +7,31 @@
     "namespace": "manifest",
     "types": [
       {
         "$extend": "WebExtensionManifest",
         "properties": {
           "browser_action": {
             "type": "object",
             "properties": {
-              "default_title": { "type": "string", "optional": true },
-              "default_icon": { "$ref": "IconPath", "optional": true },
-              "default_popup": { "type": "string", "format": "relativeUrl", "optional": true }
+              "default_title": {
+                "type": "string",
+                "optional": true,
+                "preprocess": "localize"
+              },
+              "default_icon": {
+                "$ref": "IconPath",
+                "optional": true
+              },
+              "default_popup": {
+                "type": "string",
+                "format": "relativeUrl",
+                "optional": true,
+                "preprocess": "localize"
+              }
             },
             "optional": true
           }
         }
       }
     ]
   },
   {
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -7,19 +7,31 @@
     "namespace": "manifest",
     "types": [
       {
         "$extend": "WebExtensionManifest",
         "properties": {
           "page_action": {
             "type": "object",
             "properties": {
-              "default_title": { "type": "string", "optional": true },
-              "default_icon": { "$ref": "IconPath", "optional": true },
-              "default_popup": { "type": "string", "format": "relativeUrl", "optional": true }
+              "default_title": {
+                "type": "string",
+                "optional": true,
+                "preprocess": "localize"
+              },
+              "default_icon": {
+                "$ref": "IconPath",
+                "optional": true
+              },
+              "default_popup": {
+                "type": "string",
+                "format": "relativeUrl",
+                "optional": true,
+                "preprocess": "localize"
+              }
             },
             "optional": true
           }
         }
       }
     ]
   },
   {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -77,16 +77,18 @@ function* runTests(options) {
 
       nextTest();
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
 
+    files: options.files || {},
+
     background: `(${background})(${options.getTests})`,
   });
 
 
   let browserActionId = makeWidgetId(extension.id) + "-browser-action";
 
   function checkDetails(details) {
     let button = document.getElementById(browserActionId);
@@ -135,22 +137,39 @@ function* runTests(options) {
   yield extension.unload();
 }
 
 add_task(function* testTabSwitchContext() {
   yield runTests({
     manifest: {
       "browser_action": {
         "default_icon": "default.png",
-        "default_popup": "default.html",
-        "default_title": "Default Title",
+        "default_popup": "__MSG_popup__",
+        "default_title": "Default __MSG_title__",
       },
+
+      "default_locale": "en",
+
       "permissions": ["tabs"],
     },
 
+    "files": {
+      "_locales/en/messages.json": {
+        "popup": {
+          "message": "default.html",
+          "description": "Popup",
+        },
+
+        "title": {
+          "message": "Title",
+          "description": "Title",
+        },
+      },
+    },
+
     getTests(tabs, expectDefaults) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
          "badgeBackgroundColor": null},
         {"icon": browser.runtime.getURL("1.png"),
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -77,16 +77,18 @@ function* runTests(options) {
     });
 
     runTests();
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
 
+    files: options.files || {},
+
     background: `(${background})(${options.getTests})`,
   });
 
   let pageActionId = makeWidgetId(extension.id) + "-page-action";
   let currentWindow = window;
   let windows = [];
 
   function checkDetails(details) {
@@ -149,23 +151,39 @@ function* runTests(options) {
 
 add_task(function* testTabSwitchContext() {
   yield runTests({
     manifest: {
       "name": "Foo Extension",
 
       "page_action": {
         "default_icon": "default.png",
-        "default_popup": "default.html",
-        "default_title": "Default Title \u263a",
+        "default_popup": "__MSG_popup__",
+        "default_title": "Default __MSG_title__ \u263a",
       },
 
+      "default_locale": "en",
+
       "permissions": ["tabs"],
     },
 
+    "files": {
+      "_locales/en/messages.json": {
+        "popup": {
+          "message": "default.html",
+          "description": "Popup",
+        },
+
+        "title": {
+          "message": "Title",
+          "description": "Title",
+        },
+      },
+    },
+
     getTests(tabs) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title \u263a"},
         {"icon": browser.runtime.getURL("1.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title \u263a"},
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -634,30 +634,42 @@ ExtensionData.prototype = {
 
   // Reads the extension's |manifest.json| file, and stores its
   // parsed contents in |this.manifest|.
   readManifest() {
     return Promise.all([
       this.readJSON("manifest.json"),
       Management.lazyInit(),
     ]).then(([manifest]) => {
+      this.manifest = manifest;
+      this.rawManifest = manifest;
+
+      if (manifest && manifest.default_locale) {
+        return this.initLocale();
+      }
+    }).then(() => {
       let context = {
         url: this.baseURI && this.baseURI.spec,
 
         principal: this.principal,
 
         logError: error => {
           this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
         },
+
+        preprocessors: {},
       };
 
-      let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
+      if (this.localeData) {
+        context.preprocessors.localize = this.localize.bind(this);
+      }
+
+      let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
       if (normalized.error) {
         this.manifestError(normalized.error);
-        this.manifest = manifest;
       } else {
         this.manifest = normalized.value;
       }
 
       try {
         this.id = this.manifest.applications.gecko.id;
       } catch (e) {
         // Errors are handled by the type checks above.
@@ -691,17 +703,17 @@ ExtensionData.prototype = {
   normalizeLocaleCode(locale) {
     return String.replace(locale, /_/g, "-");
   },
 
   // Reads the locale file for the given Gecko-compatible locale code, and
   // stores its parsed contents in |this.localeMessages.get(locale)|.
   readLocaleFile: Task.async(function* (locale) {
     let locales = yield this.promiseLocales();
-    let dir = locales.get(locale);
+    let dir = locales.get(locale) || locale;
     let file = `_locales/${dir}/messages.json`;
 
     try {
       let messages = yield this.readJSON(file);
       return this.localeData.addLocale(locale, messages, this);
     } catch (e) {
       this.packagingError(`Loading locale file ${file}: ${e}`);
       return new Map();
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -78,29 +78,35 @@ function getValueBaseType(value) {
   return t;
 }
 
 class Context {
   constructor(params) {
     this.params = params;
 
     this.path = [];
+    this.preprocessors = {
+      localize(value, context) {
+        return value;
+      },
+    };
 
     let props = ["addListener", "callFunction", "callAsyncFunction",
                  "hasListener", "removeListener",
-                 "getProperty", "setProperty"];
+                 "getProperty", "setProperty",
+                 "checkLoadURL", "logError",
+                 "preprocessors"];
     for (let prop of props) {
-      this[prop] = params[prop];
-    }
-
-    if ("checkLoadURL" in params) {
-      this.checkLoadURL = params.checkLoadURL;
-    }
-    if ("logError" in params) {
-      this.logError = params.logError;
+      if (prop in params) {
+        if (prop in this && typeof this[prop] == "object") {
+          Object.assign(this[prop], params[prop]);
+        } else {
+          this[prop] = params[prop];
+        }
+      }
     }
   }
 
   get cloneScope() {
     return this.params.cloneScope;
   }
 
   get url() {
@@ -262,16 +268,34 @@ class Entry {
      *
      * If the value is any other truthy value, a generic deprecation
      * message will be emitted.
      */
     this.deprecated = false;
     if ("deprecated" in schema) {
       this.deprecated = schema.deprecated;
     }
+
+    /**
+     * If set to a string value, and a preprocessor of the same is
+     * defined in the validation context, it will be applied to this
+     * value prior to any normalization.
+     */
+    this.preprocessor = schema.preprocess || null;
+  }
+
+  /**
+   * Preprocess the given value with the preprocessor declared in
+   * `preprocessor`.
+   */
+  preprocess(value, context) {
+    if (this.preprocessor) {
+      return context.preprocessors[this.preprocessor](value, context);
+    }
+    return value;
   }
 
   /**
    * Logs a deprecation warning for this entry, based on the value of
    * its `deprecated` property.
    */
   logDeprecation(context, value = null) {
     let message = "This property is deprecated";
@@ -330,17 +354,17 @@ class Type extends Entry {
     return false;
   }
 
   // Helper method that simply relies on checkBaseType to implement
   // normalize. Subclasses can choose to use it or not.
   normalizeBase(type, value, context) {
     if (this.checkBaseType(getValueBaseType(value))) {
       this.checkDeprecated(context, value);
-      return {value};
+      return {value: this.preprocess(value, context)};
     }
     return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`);
   }
 }
 
 // Type that allows any value.
 class AnyType extends Type {
   normalize(value, context) {
@@ -429,16 +453,17 @@ class StringType extends Type {
     this.format = format;
   }
 
   normalize(value, context) {
     let r = this.normalizeBase("string", value, context);
     if (r.error) {
       return r;
     }
+    value = r.value;
 
     if (this.enumeration) {
       if (this.enumeration.includes(value)) {
         return {value};
       }
       return context.error(`Invalid enumeration value ${JSON.stringify(value)}`);
     }
 
@@ -505,16 +530,17 @@ class ObjectType extends Type {
     return baseType == "object";
   }
 
   normalize(value, context) {
     let v = this.normalizeBase("object", value, context);
     if (v.error) {
       return v;
     }
+    value = v.value;
 
     if (this.isInstanceOf) {
       if (Object.keys(this.properties).length ||
           this.patternProperties.length ||
           !(this.additionalProperties instanceof AnyType)) {
         throw new Error("InternalError: isInstanceOf can only be used with objects that are otherwise unrestricted");
       }
 
@@ -634,17 +660,17 @@ class SubModuleType extends Type {
 
 class NumberType extends Type {
   normalize(value, context) {
     let r = this.normalizeBase("number", value, context);
     if (r.error) {
       return r;
     }
 
-    if (isNaN(value) || !Number.isFinite(value)) {
+    if (isNaN(r.value) || !Number.isFinite(r.value)) {
       return context.error("NaN or infinity are not valid");
     }
 
     return r;
   }
 
   checkBaseType(baseType) {
     return baseType == "number" || baseType == "integer";
@@ -658,16 +684,17 @@ class IntegerType extends Type {
     this.maximum = maximum;
   }
 
   normalize(value, context) {
     let r = this.normalizeBase("integer", value, context);
     if (r.error) {
       return r;
     }
+    value = r.value;
 
     // Ensure it's between -2**31 and 2**31-1
     if (!Number.isSafeInteger(value)) {
       return context.error("Integer is out of range");
     }
 
     if (value < this.minimum) {
       return context.error(`Integer ${value} is too small (must be at least ${this.minimum})`);
@@ -702,16 +729,17 @@ class ArrayType extends Type {
     this.maxItems = maxItems;
   }
 
   normalize(value, context) {
     let v = this.normalizeBase("array", value, context);
     if (v.error) {
       return v;
     }
+    value = v.value;
 
     let result = [];
     for (let [i, element] of value.entries()) {
       element = context.withPath(String(i), () => this.itemType.normalize(element, context));
       if (element.error) {
         return element;
       }
       result.push(element.value);
@@ -1027,17 +1055,17 @@ this.Schemas = {
     ns.set(symbol, value);
   },
 
   parseType(path, type, extraProperties = []) {
     let allowedProperties = new Set(extraProperties);
 
     // Do some simple validation of our own schemas.
     function checkTypeProperties(...extra) {
-      let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated"]);
+      let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated", "preprocess"]);
       for (let prop of Object.keys(type)) {
         if (!allowedSet.has(prop)) {
           throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`);
         }
       }
     }
 
     if ("choices" in type) {
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -71,25 +71,18 @@ BackgroundPage.prototype = {
       if (event.target != window.document) {
         return;
       }
       event.currentTarget.removeEventListener("load", loadListener, true);
 
       if (this.scripts) {
         let doc = window.document;
         for (let script of this.scripts) {
-          let url = this.extension.baseURI.resolve(script);
-
-          if (!this.extension.isExtensionURL(url)) {
-            this.extension.manifestError("Background scripts must be files within the extension");
-            continue;
-          }
-
           let tag = doc.createElement("script");
-          tag.setAttribute("src", url);
+          tag.setAttribute("src", script);
           tag.async = false;
           doc.body.appendChild(tag);
         }
       }
 
       if (this.extension.onStartup) {
         this.extension.onStartup();
       }
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -38,33 +38,48 @@
                   }
                 }
               }
             }
           },
 
           "name": {
             "type": "string",
-            "optional": false
+            "optional": false,
+            "preprocess": "localize"
+          },
+
+          "short_name": {
+            "type": "string",
+            "optional": true,
+            "preprocess": "localize"
           },
 
           "description": {
             "type": "string",
-            "optional": true
+            "optional": true,
+            "preprocess": "localize"
+          },
+
+          "creator": {
+            "type": "string",
+            "optional": true,
+            "preprocess": "localize"
           },
 
           "version": {
             "type": "string",
             "optional": false
           },
 
           "homepage_url": {
             "type": "string",
             "format": "url",
-            "optional": true
+            "optional": true,
+            "preprocess": "localize"
           },
 
           "icons": {
             "type": "object",
             "optional": true,
             "patternProperties": {
               "^[1-9]\\d*$": { "type": "string" }
             }
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -244,16 +244,32 @@ let json = [
                },
              },
            },
          },
        ],
      },
 
      {
+       name: "localize",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           type: "object",
+           properties: {
+             foo: {type: "string", "preprocess": "localize", "optional": true},
+             bar: {type: "string", "optional": true},
+             url: {type: "string", "preprocess": "localize", "format": "url", "optional": true},
+           },
+         },
+       ],
+     },
+
+     {
        name: "extended1",
        type: "function",
        parameters: [
          {name: "val", $ref: "basetype1"},
        ],
      },
 
      {
@@ -320,16 +336,22 @@ function checkErrors(errors) {
 
 let wrapper = {
   url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
 
   checkLoadURL(url) {
     return !url.startsWith("chrome:");
   },
 
+  preprocessors: {
+    localize(value, context) {
+      return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
+    },
+  },
+
   logError(message) {
     talliedErrors.push(message);
   },
 
   callFunction(path, name, args) {
     let ns = path.join(".");
     tally("call", ns, name, args);
   },
@@ -600,16 +622,26 @@ add_task(function* () {
     target[Symbol.toStringTag] = () => "[object Object]";
     let proxy = new Proxy(target, {});
     Assert.throws(() => root.testing.quack(proxy),
                   /Expected a plain JavaScript object, got a Proxy/,
                   "should throw when passing a Proxy");
   }
 
 
+  root.testing.localize({foo: "__MSG_foo__", bar: "__MSG_foo__", url: "__MSG_http://example.com/__"});
+  verify("call", "testing", "localize", [{foo: "FOO", bar: "__MSG_foo__", url: "http://example.com/"}]);
+  tallied = null;
+
+
+  Assert.throws(() => root.testing.localize({url: "__MSG_/foo/bar__"}),
+                /\/FOO\/BAR is not a valid URL\./,
+                "should throw for invalid URL");
+
+
   root.testing.extended1({prop1: "foo", prop2: "bar"});
   verify("call", "testing", "extended1", [{prop1: "foo", prop2: "bar"}]);
   tallied = null;
 
   Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: 12}),
                 /Expected string instead of 12/,
                 "should throw for wrong property type");
 
--- a/toolkit/components/extensions/test/xpcshell/test_locale_data.js
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
@@ -50,63 +50,78 @@ add_task(function* testInvalidDefaultLoc
       "default_locale": "en",
     },
 
     "files": {
       "_locales/en_US/messages.json": {},
     },
   });
 
-  equal(extension.errors.length, 0, "No errors reported");
-
-  yield extension.initAllLocales();
-
   equal(extension.errors.length, 1, "One error reported");
 
   do_print(`Got error: ${extension.errors[0]}`);
 
-  ok(extension.errors[0].includes('"default_locale" property must correspond'),
+  ok(extension.errors[0].includes("Loading locale file _locales/en/messages.json"),
+     "Got invalid default_locale error");
+
+  yield extension.initAllLocales();
+
+  equal(extension.errors.length, 2, "Two errors reported");
+
+  do_print(`Got error: ${extension.errors[1]}`);
+
+  ok(extension.errors[1].includes('"default_locale" property must correspond'),
      "Got invalid default_locale error");
 });
 
 
 add_task(function* testUnexpectedDefaultLocale() {
   let extension = yield generateAddon({
     "manifest": {
       "default_locale": "en_US",
     },
   });
 
-  equal(extension.errors.length, 0, "No errors reported");
-
-  yield extension.initAllLocales();
-
   equal(extension.errors.length, 1, "One error reported");
 
   do_print(`Got error: ${extension.errors[0]}`);
 
-  ok(extension.errors[0].includes('"default_locale" property must correspond'),
+  ok(extension.errors[0].includes("Loading locale file _locales/en-US/messages.json"),
+     "Got invalid default_locale error");
+
+  yield extension.initAllLocales();
+
+  equal(extension.errors.length, 2, "One error reported");
+
+  do_print(`Got error: ${extension.errors[1]}`);
+
+  ok(extension.errors[1].includes('"default_locale" property must correspond'),
      "Got unexpected default_locale error");
 });
 
 
 add_task(function* testInvalidSyntax() {
   let extension = yield generateAddon({
     "manifest": {
       "default_locale": "en_US",
     },
 
     "files": {
       "_locales/en_US/messages.json": '{foo: {message: "bar", description: "baz"}}',
     },
   });
 
-  equal(extension.errors.length, 0, "No errors reported");
-
-  yield extension.initAllLocales();
-
-  equal(extension.errors.length, 1, "One error reported");
+  equal(extension.errors.length, 1, "No errors reported");
 
   do_print(`Got error: ${extension.errors[0]}`);
 
   ok(extension.errors[0].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
      "Got syntax error");
+
+  yield extension.initAllLocales();
+
+  equal(extension.errors.length, 2, "One error reported");
+
+  do_print(`Got error: ${extension.errors[1]}`);
+
+  ok(extension.errors[1].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
+     "Got syntax error");
 });
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -893,21 +893,25 @@ var loadManifestFromWebManifest = Task.a
   // WebExtensions don't use iconURLs
   addon.iconURL = null;
   addon.icon64URL = null;
   addon.icons = manifest.icons || {};
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
   function getLocale(aLocale) {
+    // Use the raw manifest, here, since we need values with their
+    // localization placeholders still in place.
+    let rawManifest = extension.rawManifest;
+
     let result = {
-      name: extension.localize(manifest.name, aLocale),
-      description: extension.localize(manifest.description, aLocale),
-      creator: null,
-      homepageURL: null,
+      name: extension.localize(rawManifest.name, aLocale),
+      description: extension.localize(rawManifest.description, aLocale),
+      creator: extension.localize(rawManifest.creator, aLocale),
+      homepageURL: extension.localize(rawManifest.homepage_url, aLocale),
 
       developers: null,
       translators: null,
       contributors: null,
       locales: [aLocale],
     };
     return result;
   }