Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 authored April 23, 2012
23  packages/addon-kit/data/test-localization.html
... ...
@@ -0,0 +1,23 @@
  1
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
  2
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
  3
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
  4
+
  5
+<html>
  6
+  <head>
  7
+    <title>HTML Localization</title>
  8
+  </head>
  9
+  <body>
  10
+    <div data-l10n-id="Not translated">Kept as-is</div>
  11
+    <ul data-l10n-id="Translated">
  12
+      <li>Inner html content is replaced,</li>
  13
+      <li data-l10n-id="text-content">
  14
+        Elements with data-l10n-id attribute whose parent element is translated
  15
+        will be replaced by the content of the translation.
  16
+      </li>
  17
+    </ul>
  18
+    <div data-l10n-id="text-content">No</div>
  19
+    <div data-l10n-id="Translated">
  20
+      A data-l10n-id value can be used in multiple elements
  21
+    </div>
  22
+  </body>
  23
+</html
80  packages/addon-kit/lib/l10n.js
@@ -3,25 +3,21 @@
3 3
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 4
 "use strict";
5 5
 
6  
-const { Cc, Ci } = require("chrome");
7  
-const { getPreferedLocales, findClosestLocale } = require("api-utils/l10n/locale");
  6
+const core = require("api-utils/l10n/core");
8 7
 const { getRulesForLocale } = require("api-utils/l10n/plural-rules");
9 8
 
10  
-// Get URI for the addon root folder:
11  
-const { rootURI } = require("@packaging");
12  
-
13  
-let globalHash = {};
14  
-let pluralMappingFunction = getRulesForLocale("en");
  9
+// Retrieve the plural mapping function
  10
+let pluralMappingFunction = getRulesForLocale(core.language()) ||
  11
+                            getRulesForLocale("en");
15 12
 
16 13
 exports.get = function get(k) {
17  
-
18 14
   // For now, we only accept a "string" as first argument
19 15
   // TODO: handle plural forms in gettext pattern
20 16
   if (typeof k !== "string")
21 17
     throw new Error("First argument of localization method should be a string");
22 18
 
23 19
   // Get translation from big hashmap or default to hard coded string:
24  
-  let localized = globalHash[k] || k;
  20
+  let localized = core.get(k) || k;
25 21
 
26 22
   // # Simplest usecase:
27 23
   //   // String hard coded in source code:
@@ -81,69 +77,3 @@ exports.get = function get(k) {
81 77
 
82 78
   return localized;
83 79
 }
84  
-
85  
-function readURI(uri) {
86  
-  let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
87  
-                createInstance(Ci.nsIXMLHttpRequest);
88  
-  request.open('GET', uri, false);
89  
-  request.overrideMimeType('text/plain');
90  
-  request.send();
91  
-  return request.responseText;
92  
-}
93  
-
94  
-function readJsonUri(uri) {
95  
-  try {
96  
-    return JSON.parse(readURI(uri));
97  
-  }
98  
-  catch(e) {
99  
-    console.error("Error while reading locale file:\n" + uri + "\n" + e);
100  
-  }
101  
-  return {};
102  
-}
103  
-
104  
-// Returns the array stored in `locales.json` manifest that list available
105  
-// locales files
106  
-function getAvailableLocales() {
107  
-  let uri = rootURI + "locales.json";
108  
-  let manifest = readJsonUri(uri);
109  
-
110  
-  return "locales" in manifest && Array.isArray(manifest.locales) ?
111  
-         manifest.locales : [];
112  
-}
113  
-
114  
-// Returns URI of the best locales file to use from the XPI
115  
-function getBestLocaleFile() {
116  
-
117  
-  // Read localization manifest file that contains list of available languages
118  
-  let availableLocales = getAvailableLocales();
119  
-
120  
-  // Retrieve list of prefered locales to use
121  
-  let preferedLocales = getPreferedLocales();
122  
-
123  
-  // Compute the most preferable locale to use by using these two lists
124  
-  let bestMatchingLocale = findClosestLocale(availableLocales, preferedLocales);
125  
-
126  
-  // It may be null if the addon doesn't have any locale file
127  
-  if (!bestMatchingLocale)
128  
-    return null;
129  
-
130  
-  // Retrieve the related plural mapping function
131  
-  let shortLocaleCode = bestMatchingLocale.split("-")[0].toLowerCase();
132  
-  pluralMappingFunction = getRulesForLocale(shortLocaleCode);
133  
-
134  
-  return rootURI + "locale/" + bestMatchingLocale + ".json";
135  
-}
136  
-
137  
-function init() {
138  
-  // First, search for a locale file:
139  
-  let localeURI = getBestLocaleFile();
140  
-  if (!localeURI)
141  
-    return;
142  
-
143  
-  // Locale files only contains one big JSON object that is used as
144  
-  // an hashtable of: "key to translate" => "translated key"
145  
-  // TODO: We are likely to change this in order to be able to overload
146  
-  //       a specific key translation. For a specific package, module or line?
147  
-  globalHash = readJsonUri(localeURI);
148  
-}
149  
-init();
2  packages/addon-kit/locale/en-GB.properties
@@ -4,6 +4,8 @@
4 4
 
5 5
 Translated= Yes
6 6
 
  7
+text-content=no <b>HTML</b> injection
  8
+
7 9
 downloadsCount=%d downloads
8 10
 downloadsCount[one]=one download
9 11
 
47  packages/addon-kit/tests/test-l10n.js
@@ -52,6 +52,53 @@ exports.testExactMatching = function(test) {
52 52
   resetLocale();
53 53
 }
54 54
 
  55
+exports.testHtmlLocalization = function(test) {
  56
+  test.waitUntilDone();
  57
+
  58
+  // Change the locale before loading new l10n modules in order to load
  59
+  // the right .properties file
  60
+  setLocale("en-GB");
  61
+  let loader = Loader(module);
  62
+
  63
+  // Ensure initing html component that watch document creations
  64
+  // Note that this module is automatically initialized in
  65
+  // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
  66
+  let loaderHtmlL10n = loader.require("api-utils/l10n/html");
  67
+  loaderHtmlL10n.enable();
  68
+
  69
+  let uri = require("self").data.url("test-localization.html");
  70
+  let worker = loader.require("page-worker").Page({
  71
+    contentURL: uri,
  72
+    contentScript: "new " + function ContentScriptScope() {
  73
+      let nodes = document.body.querySelectorAll("*[data-l10n-id]");
  74
+      self.postMessage([nodes[0].innerHTML,
  75
+                        nodes[1].innerHTML,
  76
+                        nodes[2].innerHTML,
  77
+                        nodes[3].innerHTML]);
  78
+    },
  79
+    onMessage: function (data) {
  80
+      test.assertEqual(
  81
+        data[0],
  82
+        "Kept as-is",
  83
+        "Nodes with unknown id in .properties are kept 'as-is'"
  84
+      );
  85
+      test.assertEqual(data[1], "Yes", "HTML is translated");
  86
+      test.assertEqual(
  87
+        data[2],
  88
+        "no &lt;b&gt;HTML&lt;/b&gt; injection",
  89
+        "Content from .properties is text content; HTML can't be injected."
  90
+      );
  91
+      test.assertEqual(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
  92
+
  93
+      loader.unload();
  94
+      resetLocale();
  95
+
  96
+      test.done();
  97
+    }
  98
+  });
  99
+
  100
+}
  101
+
55 102
 exports.testEnUsLocaleName = function(test) {
56 103
   let loader = Loader(module);
57 104
   setLocale("en-US");
11  packages/api-utils/lib/addon/runner.js
@@ -62,6 +62,17 @@ function startup(reason, options) {
62 62
   if (reason === 'startup')
63 63
     return wait(reason, options);
64 64
 
  65
+  // Try initializing localization module before running main module. Just print
  66
+  // an exception in case of error, instead of preventing addon to be run.
  67
+  try {
  68
+    // Do not enable HTML localization while running test as it is hard to
  69
+    // disable. Because unit tests are evaluated in a another Loader who
  70
+    // doesn't have access to this current loader.
  71
+    if (options.loader.main.id !== "test-harness/run-tests")
  72
+      require("api-utils/l10n/html").enable();
  73
+  } catch(error) {
  74
+    console.exception(error);
  75
+  }
65 76
   try {
66 77
     // TODO: When bug 564675 is implemented this will no longer be needed
67 78
     // Always set the default prefs, because they disappear on restart
89  packages/api-utils/lib/l10n/core.js
... ...
@@ -0,0 +1,89 @@
  1
+/* This Source Code Form is subject to the terms of the Mozilla Public
  2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
  3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4
+"use strict";
  5
+
  6
+const { Cc, Ci } = require("chrome");
  7
+const { getPreferedLocales, findClosestLocale } = require("api-utils/l10n/locale");
  8
+
  9
+// Get URI for the addon root folder:
  10
+const { rootURI } = require("@packaging");
  11
+
  12
+let globalHash = {};
  13
+let bestMatchingLocale = null;
  14
+
  15
+exports.get = function get(k) {
  16
+  return k in globalHash ? globalHash[k] : null;
  17
+}
  18
+
  19
+// Returns the full length locale code: ja-JP-mac, en-US or fr
  20
+exports.locale = function locale() {
  21
+  return bestMatchingLocale;
  22
+}
  23
+// Returns the short locale code: ja, en, fr
  24
+exports.language = function language() {
  25
+  return bestMatchingLocale.split("-")[0].toLowerCase();
  26
+}
  27
+
  28
+function readURI(uri) {
  29
+  let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  30
+                createInstance(Ci.nsIXMLHttpRequest);
  31
+  request.open('GET', uri, false);
  32
+  request.overrideMimeType('text/plain');
  33
+  request.send();
  34
+  return request.responseText;
  35
+}
  36
+
  37
+function readJsonUri(uri) {
  38
+  try {
  39
+    return JSON.parse(readURI(uri));
  40
+  }
  41
+  catch(e) {
  42
+    console.error("Error while reading locale file:\n" + uri + "\n" + e);
  43
+  }
  44
+  return {};
  45
+}
  46
+
  47
+// Returns the array stored in `locales.json` manifest that list available
  48
+// locales files
  49
+function getAvailableLocales() {
  50
+  let uri = rootURI + "locales.json";
  51
+  let manifest = readJsonUri(uri);
  52
+
  53
+  return "locales" in manifest &&
  54
+          Array.isArray(manifest.locales) ?
  55
+         manifest.locales : [];
  56
+}
  57
+
  58
+// Returns URI of the best locales file to use from the XPI
  59
+function getBestLocaleFile() {
  60
+
  61
+  // Read localization manifest file that contains list of available languages
  62
+  let availableLocales = getAvailableLocales();
  63
+
  64
+  // Retrieve list of prefered locales to use
  65
+  let preferedLocales = getPreferedLocales();
  66
+
  67
+  // Compute the most preferable locale to use by using these two lists
  68
+  bestMatchingLocale = findClosestLocale(availableLocales, preferedLocales);
  69
+
  70
+  // It may be null if the addon doesn't have any locale file
  71
+  if (!bestMatchingLocale)
  72
+    return null;
  73
+
  74
+  return rootURI + "locale/" + bestMatchingLocale + ".json";
  75
+}
  76
+
  77
+function init() {
  78
+  // First, search for a locale file:
  79
+  let localeURI = getBestLocaleFile();
  80
+  if (!localeURI)
  81
+    return;
  82
+
  83
+  // Locale files only contains one big JSON object that is used as
  84
+  // an hashtable of: "key to translate" => "translated key"
  85
+  // TODO: We are likely to change this in order to be able to overload
  86
+  //       a specific key translation. For a specific package, module or line?
  87
+  globalHash = readJsonUri(localeURI);
  88
+}
  89
+init();
83  packages/api-utils/lib/l10n/html.js
... ...
@@ -0,0 +1,83 @@
  1
+/* This Source Code Form is subject to the terms of the Mozilla Public
  2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
  3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4
+
  5
+const { Ci } = require("chrome");
  6
+const events = require("api-utils/system/events");
  7
+const core = require("api-utils/l10n/core");
  8
+const { prefixURI } = require("@packaging");
  9
+
  10
+// Taken from Gaia:
  11
+// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
  12
+function translateElement(element) {
  13
+  element = element || document;
  14
+
  15
+  // check all translatable children (= w/ a `data-l10n-id' attribute)
  16
+  var children = element.querySelectorAll('*[data-l10n-id]');
  17
+  var elementCount = children.length;
  18
+  for (var i = 0; i < elementCount; i++) {
  19
+    var child = children[i];
  20
+
  21
+    // translate the child
  22
+    var key = child.dataset.l10nId;
  23
+    var data = core.get(key);
  24
+    if (data)
  25
+      child.textContent = data;
  26
+  }
  27
+}
  28
+exports.translateElement = translateElement;
  29
+
  30
+function onDocumentReady2Translate(event) {
  31
+  let document = event.target;
  32
+  document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
  33
+                               false);
  34
+
  35
+  translateElement(document);
  36
+
  37
+  // Finally display document when we finished replacing all text content
  38
+  document.documentElement.style.visibility = "visible";
  39
+}
  40
+
  41
+function onContentWindow(event) {
  42
+  let document = event.subject;
  43
+
  44
+  // Accept only HTML documents
  45
+  if (!(document instanceof Ci.nsIDOMHTMLDocument))
  46
+    return;
  47
+
  48
+  // Accept only document from this addon
  49
+  if (document.location.href.indexOf(prefixURI) !== 0)
  50
+    return;
  51
+
  52
+  // First hide content of the document in order to have content blinking
  53
+  // between untranslated and translated states
  54
+  // TODO: use result of bug 737003 discussion in order to avoid any conflict
  55
+  // with document CSS
  56
+  document.documentElement.style.visibility = "hidden";
  57
+
  58
+  // Wait for DOM tree to be built before applying localization
  59
+  document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
  60
+                            false);
  61
+}
  62
+
  63
+// Listen to creation of content documents in order to translate them as soon
  64
+// as possible in their loading process
  65
+const ON_CONTENT = "document-element-inserted";
  66
+let enabled = false;
  67
+function enable() {
  68
+  if (!enabled) {
  69
+    events.on(ON_CONTENT, onContentWindow);
  70
+    enabled = true;
  71
+  }
  72
+}
  73
+exports.enable = enable;
  74
+
  75
+function disable() {
  76
+  if (enabled) {
  77
+    events.off(ON_CONTENT, onContentWindow);
  78
+    enabled = false;
  79
+  }
  80
+}
  81
+exports.disable = disable;
  82
+
  83
+require("api-utils/unload").when(disable);

0 notes on commit 9bd8d3b

Please sign in to comment.
Something went wrong with that request. Please try again.