Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #387 from ochameau/html-localization

Bug 739249: Implement HTML Localization. r=@gozala
  • Loading branch information...
commit 9bd8d3b509881883d31c8138589bd49663c0629f 2 parents a4a202d + 9bddccd
ochameau ochameau authored
23 packages/addon-kit/data/test-localization.html
View
@@ -0,0 +1,23 @@
+<!-- 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/. -->
+
+<html>
+ <head>
+ <title>HTML Localization</title>
+ </head>
+ <body>
+ <div data-l10n-id="Not translated">Kept as-is</div>
+ <ul data-l10n-id="Translated">
+ <li>Inner html content is replaced,</li>
+ <li data-l10n-id="text-content">
+ Elements with data-l10n-id attribute whose parent element is translated
+ will be replaced by the content of the translation.
+ </li>
+ </ul>
+ <div data-l10n-id="text-content">No</div>
+ <div data-l10n-id="Translated">
+ A data-l10n-id value can be used in multiple elements
+ </div>
+ </body>
+</html
80 packages/addon-kit/lib/l10n.js
View
@@ -3,25 +3,21 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { Cc, Ci } = require("chrome");
-const { getPreferedLocales, findClosestLocale } = require("api-utils/l10n/locale");
+const core = require("api-utils/l10n/core");
const { getRulesForLocale } = require("api-utils/l10n/plural-rules");
-// Get URI for the addon root folder:
-const { rootURI } = require("@packaging");
-
-let globalHash = {};
-let pluralMappingFunction = getRulesForLocale("en");
+// Retrieve the plural mapping function
+let pluralMappingFunction = getRulesForLocale(core.language()) ||
+ getRulesForLocale("en");
exports.get = function get(k) {
-
// For now, we only accept a "string" as first argument
// TODO: handle plural forms in gettext pattern
if (typeof k !== "string")
throw new Error("First argument of localization method should be a string");
// Get translation from big hashmap or default to hard coded string:
- let localized = globalHash[k] || k;
+ let localized = core.get(k) || k;
// # Simplest usecase:
// // String hard coded in source code:
@@ -81,69 +77,3 @@ exports.get = function get(k) {
return localized;
}
-
-function readURI(uri) {
- let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
- createInstance(Ci.nsIXMLHttpRequest);
- request.open('GET', uri, false);
- request.overrideMimeType('text/plain');
- request.send();
- return request.responseText;
-}
-
-function readJsonUri(uri) {
- try {
- return JSON.parse(readURI(uri));
- }
- catch(e) {
- console.error("Error while reading locale file:\n" + uri + "\n" + e);
- }
- return {};
-}
-
-// Returns the array stored in `locales.json` manifest that list available
-// locales files
-function getAvailableLocales() {
- let uri = rootURI + "locales.json";
- let manifest = readJsonUri(uri);
-
- return "locales" in manifest && Array.isArray(manifest.locales) ?
- manifest.locales : [];
-}
-
-// Returns URI of the best locales file to use from the XPI
-function getBestLocaleFile() {
-
- // Read localization manifest file that contains list of available languages
- let availableLocales = getAvailableLocales();
-
- // Retrieve list of prefered locales to use
- let preferedLocales = getPreferedLocales();
-
- // Compute the most preferable locale to use by using these two lists
- let bestMatchingLocale = findClosestLocale(availableLocales, preferedLocales);
-
- // It may be null if the addon doesn't have any locale file
- if (!bestMatchingLocale)
- return null;
-
- // Retrieve the related plural mapping function
- let shortLocaleCode = bestMatchingLocale.split("-")[0].toLowerCase();
- pluralMappingFunction = getRulesForLocale(shortLocaleCode);
-
- return rootURI + "locale/" + bestMatchingLocale + ".json";
-}
-
-function init() {
- // First, search for a locale file:
- let localeURI = getBestLocaleFile();
- if (!localeURI)
- return;
-
- // Locale files only contains one big JSON object that is used as
- // an hashtable of: "key to translate" => "translated key"
- // TODO: We are likely to change this in order to be able to overload
- // a specific key translation. For a specific package, module or line?
- globalHash = readJsonUri(localeURI);
-}
-init();
2  packages/addon-kit/locale/en-GB.properties
View
@@ -4,6 +4,8 @@
Translated= Yes
+text-content=no <b>HTML</b> injection
+
downloadsCount=%d downloads
downloadsCount[one]=one download
47 packages/addon-kit/tests/test-l10n.js
View
@@ -52,6 +52,53 @@ exports.testExactMatching = function(test) {
resetLocale();
}
+exports.testHtmlLocalization = function(test) {
+ test.waitUntilDone();
+
+ // Change the locale before loading new l10n modules in order to load
+ // the right .properties file
+ setLocale("en-GB");
+ let loader = Loader(module);
+
+ // Ensure initing html component that watch document creations
+ // Note that this module is automatically initialized in
+ // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
+ let loaderHtmlL10n = loader.require("api-utils/l10n/html");
+ loaderHtmlL10n.enable();
+
+ let uri = require("self").data.url("test-localization.html");
+ let worker = loader.require("page-worker").Page({
+ contentURL: uri,
+ contentScript: "new " + function ContentScriptScope() {
+ let nodes = document.body.querySelectorAll("*[data-l10n-id]");
+ self.postMessage([nodes[0].innerHTML,
+ nodes[1].innerHTML,
+ nodes[2].innerHTML,
+ nodes[3].innerHTML]);
+ },
+ onMessage: function (data) {
+ test.assertEqual(
+ data[0],
+ "Kept as-is",
+ "Nodes with unknown id in .properties are kept 'as-is'"
+ );
+ test.assertEqual(data[1], "Yes", "HTML is translated");
+ test.assertEqual(
+ data[2],
+ "no &lt;b&gt;HTML&lt;/b&gt; injection",
+ "Content from .properties is text content; HTML can't be injected."
+ );
+ test.assertEqual(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
+
+ loader.unload();
+ resetLocale();
+
+ test.done();
+ }
+ });
+
+}
+
exports.testEnUsLocaleName = function(test) {
let loader = Loader(module);
setLocale("en-US");
11 packages/api-utils/lib/addon/runner.js
View
@@ -62,6 +62,17 @@ function startup(reason, options) {
if (reason === 'startup')
return wait(reason, options);
+ // Try initializing localization module before running main module. Just print
+ // an exception in case of error, instead of preventing addon to be run.
+ try {
+ // Do not enable HTML localization while running test as it is hard to
+ // disable. Because unit tests are evaluated in a another Loader who
+ // doesn't have access to this current loader.
+ if (options.loader.main.id !== "test-harness/run-tests")
+ require("api-utils/l10n/html").enable();
+ } catch(error) {
+ console.exception(error);
+ }
try {
// TODO: When bug 564675 is implemented this will no longer be needed
// Always set the default prefs, because they disappear on restart
89 packages/api-utils/lib/l10n/core.js
View
@@ -0,0 +1,89 @@
+/* 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 { Cc, Ci } = require("chrome");
+const { getPreferedLocales, findClosestLocale } = require("api-utils/l10n/locale");
+
+// Get URI for the addon root folder:
+const { rootURI } = require("@packaging");
+
+let globalHash = {};
+let bestMatchingLocale = null;
+
+exports.get = function get(k) {
+ return k in globalHash ? globalHash[k] : null;
+}
+
+// Returns the full length locale code: ja-JP-mac, en-US or fr
+exports.locale = function locale() {
+ return bestMatchingLocale;
+}
+// Returns the short locale code: ja, en, fr
+exports.language = function language() {
+ return bestMatchingLocale.split("-")[0].toLowerCase();
+}
+
+function readURI(uri) {
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ request.open('GET', uri, false);
+ request.overrideMimeType('text/plain');
+ request.send();
+ return request.responseText;
+}
+
+function readJsonUri(uri) {
+ try {
+ return JSON.parse(readURI(uri));
+ }
+ catch(e) {
+ console.error("Error while reading locale file:\n" + uri + "\n" + e);
+ }
+ return {};
+}
+
+// Returns the array stored in `locales.json` manifest that list available
+// locales files
+function getAvailableLocales() {
+ let uri = rootURI + "locales.json";
+ let manifest = readJsonUri(uri);
+
+ return "locales" in manifest &&
+ Array.isArray(manifest.locales) ?
+ manifest.locales : [];
+}
+
+// Returns URI of the best locales file to use from the XPI
+function getBestLocaleFile() {
+
+ // Read localization manifest file that contains list of available languages
+ let availableLocales = getAvailableLocales();
+
+ // Retrieve list of prefered locales to use
+ let preferedLocales = getPreferedLocales();
+
+ // Compute the most preferable locale to use by using these two lists
+ bestMatchingLocale = findClosestLocale(availableLocales, preferedLocales);
+
+ // It may be null if the addon doesn't have any locale file
+ if (!bestMatchingLocale)
+ return null;
+
+ return rootURI + "locale/" + bestMatchingLocale + ".json";
+}
+
+function init() {
+ // First, search for a locale file:
+ let localeURI = getBestLocaleFile();
+ if (!localeURI)
+ return;
+
+ // Locale files only contains one big JSON object that is used as
+ // an hashtable of: "key to translate" => "translated key"
+ // TODO: We are likely to change this in order to be able to overload
+ // a specific key translation. For a specific package, module or line?
+ globalHash = readJsonUri(localeURI);
+}
+init();
83 packages/api-utils/lib/l10n/html.js
View
@@ -0,0 +1,83 @@
+/* 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/. */
+
+const { Ci } = require("chrome");
+const events = require("api-utils/system/events");
+const core = require("api-utils/l10n/core");
+const { prefixURI } = require("@packaging");
+
+// Taken from Gaia:
+// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
+function translateElement(element) {
+ element = element || document;
+
+ // check all translatable children (= w/ a `data-l10n-id' attribute)
+ var children = element.querySelectorAll('*[data-l10n-id]');
+ var elementCount = children.length;
+ for (var i = 0; i < elementCount; i++) {
+ var child = children[i];
+
+ // translate the child
+ var key = child.dataset.l10nId;
+ var data = core.get(key);
+ if (data)
+ child.textContent = data;
+ }
+}
+exports.translateElement = translateElement;
+
+function onDocumentReady2Translate(event) {
+ let document = event.target;
+ document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
+ false);
+
+ translateElement(document);
+
+ // Finally display document when we finished replacing all text content
+ document.documentElement.style.visibility = "visible";
+}
+
+function onContentWindow(event) {
+ let document = event.subject;
+
+ // Accept only HTML documents
+ if (!(document instanceof Ci.nsIDOMHTMLDocument))
+ return;
+
+ // Accept only document from this addon
+ if (document.location.href.indexOf(prefixURI) !== 0)
+ return;
+
+ // First hide content of the document in order to have content blinking
+ // between untranslated and translated states
+ // TODO: use result of bug 737003 discussion in order to avoid any conflict
+ // with document CSS
+ document.documentElement.style.visibility = "hidden";
+
+ // Wait for DOM tree to be built before applying localization
+ document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
+ false);
+}
+
+// Listen to creation of content documents in order to translate them as soon
+// as possible in their loading process
+const ON_CONTENT = "document-element-inserted";
+let enabled = false;
+function enable() {
+ if (!enabled) {
+ events.on(ON_CONTENT, onContentWindow);
+ enabled = true;
+ }
+}
+exports.enable = enable;
+
+function disable() {
+ if (enabled) {
+ events.off(ON_CONTENT, onContentWindow);
+ enabled = false;
+ }
+}
+exports.disable = disable;
+
+require("api-utils/unload").when(disable);
Please sign in to comment.
Something went wrong with that request. Please try again.