diff --git a/dom/apps/Langpacks.jsm b/dom/apps/Langpacks.jsm index 17dc70843462..b2e1769163cf 100644 --- a/dom/apps/Langpacks.jsm +++ b/dom/apps/Langpacks.jsm @@ -47,14 +47,17 @@ this.Langpacks = { _data: {}, _broadcaster: null, _appIdFromManifestURL: null, + _appFromManifestURL: null, init: function() { ppmm.addMessageListener("Webapps:GetLocalizationResource", this); + ppmm.addMessageListener("Webapps:GetLocalizedValue", this); }, - registerRegistryFunctions: function(aBroadcaster, aIdGetter) { + registerRegistryFunctions: function(aBroadcaster, aIdGetter, aAppGetter) { this._broadcaster = aBroadcaster; this._appIdFromManifestURL = aIdGetter; + this._appFromManifestURL = aAppGetter; }, receiveMessage: function(aMessage) { @@ -64,6 +67,9 @@ this.Langpacks = { case "Webapps:GetLocalizationResource": this.getLocalizationResource(data, mm); break; + case "Webapps:GetLocalizedValue": + this.getLocalizedValue(data, mm); + break; default: debug("Unexpected message: " + aMessage.name); } @@ -108,6 +114,35 @@ this.Langpacks = { this._broadcaster("Webapps:UpdateState", message); }, + _getResource: function(aURL, aResponseType) { + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + xhr.mozBackgroundRequest = true; + xhr.open("GET", aURL); + + // Default to text response type, but the webidl binding takes care of + // validating the dataType value. + xhr.responseType = "text"; + if (aResponseType === "json") { + xhr.responseType = "json"; + } else if (aResponseType === "binary") { + xhr.responseType = "blob"; + } + + return new Promise((aResolve, aReject) => { + xhr.addEventListener("load", function() { + debug("Success loading " + aURL); + if (xhr.status >= 200 && xhr.status < 400) { + aResolve(xhr.response); + } else { + aReject(); + } + }); + xhr.addEventListener("error", aReject); + xhr.send(null); + }); + }, + getLocalizationResource: function(aData, aMm) { debug("getLocalizationResource " + uneval(aData)); @@ -143,33 +178,103 @@ this.Langpacks = { let href = item.url + aData.path; debug("Will load " + href); - let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] - .createInstance(Ci.nsIXMLHttpRequest); - xhr.mozBackgroundRequest = true; - xhr.open("GET", href); + this._getResource(href, aData.dataType).then( + (aResponse) => { + aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return", + { requestID: aData.requestID, oid: aData.oid, data: aResponse }); + }, + () => { sendError("Error loading " + href, "UnavailableResource"); } + ); + }, - // Default to text response type, but the webidl binding takes care of - // validating the dataType value. - xhr.responseType = "text"; - if (aData.dataType === "json") { - xhr.responseType = "json"; - } else if (aData.dataType === "binary") { - xhr.responseType = "blob"; + getLocalizedValue: function(aData, aMm) { + debug("getLocalizedValue " + aData.property); + function sendError(aMsg, aCode) { + debug(aMsg); + aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return", + { success: false, + requestID: aData.requestID, + oid: aData.oid, + error: aCode }); } - xhr.addEventListener("load", function() { - debug("Success loading " + href); - if (xhr.status >= 200 && xhr.status < 400) { - aMm.sendAsyncMessage("Webapps:GetLocalizationResource:Return", - { requestID: aData.requestID, oid: aData.oid, data: xhr.response }); + function getValueFromManifest(aManifest) { + debug("Getting " + aData.property + " from the manifest."); + let value = aManifest._localeProp(aData.property); + if (!value) { + sendError("No property " + aData.property + " in manifest", "UnknownProperty"); } else { - sendError("Error loading " + href, "UnavailableResource"); + aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return", + { success: true, + requestID: aData.requestID, + oid: aData.oid, + value: value }); } - }); - xhr.addEventListener("error", function() { - sendError("Error loading " + href, "UnavailableResource"); - }); - xhr.send(null); + } + + let self = this; + + function getValueFromLangpack(aItem, aManifest) { + debug("Getting value from langpack at " + aItem.url + "/manifest.json") + let href = aItem.url + "/manifest.json"; + + function getProperty(aResponse, aProp) { + let root = aData.entryPoint && aResponse.entry_points && + aResponse.entry_points[aData.entryPoint] + ? aResponse.entry_points[aData.entryPoint] + : aResponse; + return root[aProp]; + } + + self._getResource(href, "json").then( + (aResponse) => { + let propValue = getProperty(aResponse, aData.property); + if (propValue) { + aMm.sendAsyncMessage("Webapps:GetLocalizedValue:Return", + { success: true, + requestID: aData.requestID, + oid: aData.oid, + value: propValue }); + } else { + getValueFromManifest(aManifest); + } + }, + () => { getValueFromManifest(aManifest); } + ); + } + + // We need to get the app with the manifest since the version is only + // available in the manifest. + this._appFromManifestURL(aData.manifestURL, aData.entryPoint) + .then(aApp => { + let manifest = aApp.manifest; + + // No langpack for this app or we have langpack(s) for this app, but + // not for this language. + // Fallback to the manifest values. + if (!this._data[aData.manifestURL] || + !this._data[aData.manifestURL].langs[aData.lang]) { + getValueFromManifest(manifest); + return; + } + + if (!manifest.version) { + getValueFromManifest(manifest); + return; + } + + // Check that we have the langpack for the right app version. + let item = this._data[aData.manifestURL].langs[aData.lang]; + // Only keep x.y in the manifest's version in case it's x.y.z + let manVersion = manifest.version.split('.').slice(0, 2).join('.'); + if (item.target == manVersion) { + getValueFromLangpack(item, manifest); + return; + } + // Fallback on getting the value from the manifest. + getValueFromManifest(manifest); + }) + .catch(aError => { sendError("No app!", "NoSuchApp") }); }, // Validates the langpack part of a manifest. diff --git a/dom/apps/Webapps.js b/dom/apps/Webapps.js index ca4ab0c689c8..83e628b60107 100644 --- a/dom/apps/Webapps.js +++ b/dom/apps/Webapps.js @@ -699,6 +699,24 @@ WebappsApplication.prototype = { }); }, + getLocalizedValue: function(aProperty, aLang, aEntryPoint) { + this.addMessageListeners(["Webapps:GetLocalizedValue:Return"]); + return this.createPromise((aResolve, aReject) => { + cpmm.sendAsyncMessage("Webapps:GetLocalizedValue", + { manifestURL: this.manifestURL, + oid: this._id, + topId: this._topId, + property: aProperty, + lang: aLang, + entryPoint: aEntryPoint, + requestID: this.getPromiseResolverId({ + resolve: aResolve, + reject: aReject + }) + }); + }); + }, + _prepareForContent: function() { if (this.__DOM_IMPL__) { return this.__DOM_IMPL__; @@ -736,7 +754,8 @@ WebappsApplication.prototype = { if (aMessage.name == "Webapps:Connect:Return:OK" || aMessage.name == "Webapps:Connect:Return:KO" || aMessage.name == "Webapps:GetConnections:Return:OK" || - aMessage.name == "Webapps:Export:Return") { + aMessage.name == "Webapps:Export:Return" || + aMessage.name == "Webapps:GetLocalizedValue:Return") { req = this.takePromiseResolver(msg.requestID); } else { req = this.takeRequest(msg.requestID); @@ -832,6 +851,14 @@ WebappsApplication.prototype = { req.reject(new this._window.DOMError(msg.error || "")); } break; + case "Webapps:GetLocalizedValue:Return": + this.removeMessageListeners(["Webapps:GetLocalizedValue:Return"]); + if (msg.success) { + req.resolve(msg.value); + } else { + req.reject(new this._window.DOMError(msg.error || "")); + } + break; } }, diff --git a/dom/apps/Webapps.jsm b/dom/apps/Webapps.jsm index 0e7626e041b2..674c342e0c43 100755 --- a/dom/apps/Webapps.jsm +++ b/dom/apps/Webapps.jsm @@ -251,7 +251,8 @@ this.DOMApplicationRegistry = { this.loadAndUpdateApps(); Langpacks.registerRegistryFunctions(this.broadcastMessage.bind(this), - this._appIdForManifestURL.bind(this)); + this._appIdForManifestURL.bind(this), + this.getFullAppByManifestURL.bind(this)); }, // loads the current registry, that could be empty on first run. @@ -4669,6 +4670,30 @@ this.DOMApplicationRegistry = { return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL); }, + // Returns a promise that resolves to the app object with the manifest. + getFullAppByManifestURL: function(aManifestURL, aEntryPoint) { + let app = this.getAppByManifestURL(aManifestURL); + if (!app) { + return Promise.reject("NoSuchApp"); + } + + return this.getManifestFor(aManifestURL).then((aManifest) => { + let manifest = aEntryPoint && aManifest.entry_points && + aManifest.entry_points[aEntryPoint] + ? aManifest.entry_points[aEntryPoint] + : aManifest; + + // `version` doesn't change based on entry points, and we need it + // to check langpack versions. + if (manifest !== aManifest) { + manifest.version = aManifest.version; + } + + app.manifest = new ManifestHelper(manifest, app.origin, app.manifestURL); + return app; + }); + }, + _getAppWithManifest: Task.async(function*(aManifestURL) { let app = this.getAppByManifestURL(aManifestURL); if (!app) { diff --git a/dom/apps/tests/langpack/fr/manifest.json b/dom/apps/tests/langpack/fr/manifest.json new file mode 100644 index 000000000000..761100849bf6 --- /dev/null +++ b/dom/apps/tests/langpack/fr/manifest.json @@ -0,0 +1,8 @@ +{ + "name" : "Version française.", + "entry_points": { + "dialer": { + "name": "téléphone" + } + } +} \ No newline at end of file diff --git a/dom/apps/tests/langpack/lang1.webapp b/dom/apps/tests/langpack/lang1.webapp index f3b7b0b91659..0c7aa00a1735 100644 --- a/dom/apps/tests/langpack/lang1.webapp +++ b/dom/apps/tests/langpack/lang1.webapp @@ -6,7 +6,9 @@ "revision": 201411051234, "name": "Français", "apps": { - "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/fr/" + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/fr/", + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest_no_version.webapp": "tests/dom/apps/tests/langpack/fr/", + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest_version_xyz.webapp": "tests/dom/apps/tests/langpack/fr/" } } }, diff --git a/dom/apps/tests/langpack/lang2.webapp b/dom/apps/tests/langpack/lang2.webapp index 5aa54cd24f75..ecb4f322b5c5 100644 --- a/dom/apps/tests/langpack/lang2.webapp +++ b/dom/apps/tests/langpack/lang2.webapp @@ -6,14 +6,18 @@ "revision": 201411051234, "name": "Deutsch", "apps": { - "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/de/" + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/de/", + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest_no_version.webapp": "tests/dom/apps/tests/langpack/de/", + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest_version_xyz.webapp": "tests/dom/apps/tests/langpack/de/" } }, "pl": { "revision": 201411051234, "name": "Polski", "apps": { - "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/pl/" + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest.webapp": "tests/dom/apps/tests/langpack/pl/", + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest_no_version.webapp": "tests/dom/apps/tests/langpack/pl/", + "http://mochi.test:8888/tests/dom/apps/tests/langpack/manifest_version_xyz.webapp": "tests/dom/apps/tests/langpack/pl/" } } }, diff --git a/dom/apps/tests/langpack/localizedvalues.html b/dom/apps/tests/langpack/localizedvalues.html new file mode 100644 index 000000000000..2733e8efdbe0 --- /dev/null +++ b/dom/apps/tests/langpack/localizedvalues.html @@ -0,0 +1,74 @@ + + + + Langpack Test : localized values + + + +

Langpack Test : localized values

+ + \ No newline at end of file diff --git a/dom/apps/tests/langpack/manifest.webapp b/dom/apps/tests/langpack/manifest.webapp index 87fe0590bff2..aa6ec34cf2a6 100644 --- a/dom/apps/tests/langpack/manifest.webapp +++ b/dom/apps/tests/langpack/manifest.webapp @@ -1,3 +1,9 @@ { - "name": "Localization test app" + "name": "Localization test app", + "version": "2.2", + "entry_points": { + "dialer": { + "name": "dialer" + } + } } diff --git a/dom/apps/tests/langpack/manifest_no_version.webapp b/dom/apps/tests/langpack/manifest_no_version.webapp new file mode 100644 index 000000000000..8ac6e122ed6a --- /dev/null +++ b/dom/apps/tests/langpack/manifest_no_version.webapp @@ -0,0 +1,8 @@ +{ + "name": "Localization test app", + "entry_points": { + "dialer": { + "name": "dialer" + } + } +} diff --git a/dom/apps/tests/langpack/manifest_version_xyz.webapp b/dom/apps/tests/langpack/manifest_version_xyz.webapp new file mode 100644 index 000000000000..5def58c8f2ee --- /dev/null +++ b/dom/apps/tests/langpack/manifest_version_xyz.webapp @@ -0,0 +1,9 @@ +{ + "name": "Localization test app", + "version": "2.2.1", + "entry_points": { + "dialer": { + "name": "dialer" + } + } +} diff --git a/dom/apps/tests/test_langpacks.html b/dom/apps/tests/test_langpacks.html index 45bc7eabdd6e..0ee09e3ead89 100644 --- a/dom/apps/tests/test_langpacks.html +++ b/dom/apps/tests/test_langpacks.html @@ -206,6 +206,101 @@ function runTest() { req.onerror = mozAppsError; yield undefined; } + + // Test localized values with a x.y manifest version. + installApp(appManifestURL); + yield undefined; + + // Install the fr langpack. + installApp(lang1ManifestURL); + yield undefined; + + // Install the de and pl langpack. + installApp(lang2ManifestURL); + yield undefined; + + // Opens the iframe to the localized values test page. + openPage("localizedvalues.html", + ["Localization test app", + "UnknownProperty", + "Version française.", + "dialer", + "téléphone"]); + yield undefined; + + // Clean up after ourselves by uninstalling apps. + info(apps.length + " applications to uninstall."); + while (apps.length) { + let app = apps.pop(); + req = navigator.mozApps.mgmt.uninstall(app); + req.onsuccess = continueTest; + req.onerror = mozAppsError; + yield undefined; + } + + // Test localized values with versionless manifest. + appManifestURL = uriPrefix + "manifest_no_version.webapp"; + installApp(appManifestURL); + yield undefined; + + // Install the fr langpack. + installApp(lang1ManifestURL); + yield undefined; + + // Install the de and pl langpack. + installApp(lang2ManifestURL); + yield undefined; + + // Opens the iframe to the localized values test page. + openPage("localizedvalues.html", + ["Localization test app", + "UnknownProperty", + "Localization test app", + "dialer", + "dialer"]); + yield undefined; + + // Clean up after ourselves by uninstalling apps. + info(apps.length + " applications to uninstall."); + while (apps.length) { + let app = apps.pop(); + req = navigator.mozApps.mgmt.uninstall(app); + req.onsuccess = continueTest; + req.onerror = mozAppsError; + yield undefined; + } + + // Test localized values with a x.y.z manifest version. + appManifestURL = uriPrefix + "manifest_version_xyz.webapp" + installApp(appManifestURL); + yield undefined; + + // Install the fr langpack. + installApp(lang1ManifestURL); + yield undefined; + + // Install the de and pl langpack. + installApp(lang2ManifestURL); + yield undefined; + + // Opens the iframe to the localized values test page. + openPage("localizedvalues.html", + ["Localization test app", + "UnknownProperty", + "Version française.", + "dialer", + "téléphone"]); + yield undefined; + + // Clean up after ourselves by uninstalling apps. + info(apps.length + " applications to uninstall."); + while (apps.length) { + let app = apps.pop(); + req = navigator.mozApps.mgmt.uninstall(app); + req.onsuccess = continueTest; + req.onerror = mozAppsError; + yield undefined; + } } diff --git a/dom/webidl/Apps.webidl b/dom/webidl/Apps.webidl index 846db2c6afd0..7ad8695943e6 100644 --- a/dom/webidl/Apps.webidl +++ b/dom/webidl/Apps.webidl @@ -106,6 +106,12 @@ interface DOMApplication : EventTarget { // Export this app as a shareable Blob. Promise export(); + + // Returns the localized value of a property, using either the manifest or + // a langpack if one is available. + Promise getLocalizedValue(DOMString property, + DOMString locale, + optional DOMString entryPoint); }; [JSImplementation="@mozilla.org/webapps/manager;1",