Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Bug 739249: Implement HTML Localization. #387

Merged
merged 1 commit into from almost 2 years ago

2 participants

ochameau Irakli Gozalishvili
ochameau
Collaborator

HTML localization feature described here:
https://github.com/mozilla/addon-sdk/wiki/HTML-Page-Localization
And discussed there:
https://groups.google.com/d/msg/mozilla-labs-jetpack/-/Jc-YQjf8tiQJ
Related bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=739249

This pull request is not completely ready, I'd like to get some feedback on the new method based on DOM instead of templates.
This patch needs some more unit tests, but is considered closed to what can land.

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, that are a (in)direct child of an element
  15
+        with data-l10n-id, are lost.
4
Irakli Gozalishvili Collaborator
Gozala added a note April 11, 2012

Do you mean are not localized ? Lost sounds quite brutal.

Irakli Gozalishvili Collaborator
Gozala added a note April 11, 2012

Ok it looks like the owner element will be localized replacing all the content, but I think it's still better to rephrase this. Maybe @wbamberg can help.

ochameau Collaborator
ochameau added a note April 12, 2012

What do you think about: Elements with data-l10n-id attribute whose parent element is translated will be replaced by the content of the translation.

Irakli Gozalishvili Collaborator
Gozala added a note April 12, 2012

I think it's better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/addon-kit/lib/l10n.js
((7 lines not shown))
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 gPluralMappingFunction = null;
  11
+if (core.locale) {
  12
+  let shortLocaleCode = core.locale.split("-")[0].toLowerCase();
  13
+  gPluralMappingFunction = getRulesForLocale(shortLocaleCode);
3
Irakli Gozalishvili Collaborator
Gozala added a note April 11, 2012

It feels to me that api-utils/l10n/core should directly export short local.

ochameau Collaborator
ochameau added a note April 12, 2012

I've added language attribute to core.
It looks as the best matching name for such attribute:
http://tools.ietf.org/html/rfc4646#page-5
But we can use shortLocale instead?

-or-

Rename locale to language and add shortLanguage in order to better match navigator.language (which is equal to current locale attribute)

Irakli Gozalishvili Collaborator
Gozala added a note April 12, 2012

language sounds good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/addon-kit/lib/l10n.js
((7 lines not shown))
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 gPluralMappingFunction = null;
1
Irakli Gozalishvili Collaborator
Gozala added a note April 11, 2012

Why prefixing it with g ? In jetpack we don't use g prefixing pattern, as we don't have **g**lobals. Also in this case it does not looks like a global.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/cuddlefish.js
... ...
@@ -231,6 +231,17 @@ const Loader = {
231 231
   // in the addon's context. This function loads and executes the addon's
232 232
   // entry point module.
233 233
   main: function main(id, path) {
  234
+    // Try initializing localization module before running main module
  235
+    try {
  236
+      // Do not enable HTML localization while running test as it is hard to
  237
+      // disable. Because unit tests are evaluated in a another Loader who
  238
+      // doesn't have access to this current loader.
  239
+      if (path !== "test-harness/lib/run-tests.js")
  240
+        this.require("api-utils/l10n/html").enable();
3
Irakli Gozalishvili Collaborator
Gozala added a note April 11, 2012

I don't like this direction as it makes landing loader to firefox more complicated. As matter of fact in bug 743359 we're working on making loader independent of other modules in order to be able to land it to firefox. This on the other hand introduces new dependency. Unfortunately I do not have any good suggestions yet.

ochameau Collaborator
ochameau added a note April 12, 2012

I totally agree with you and I don't think there is a better place for this. We are missing something for bug 743359. We just need to factors out this main method out of cuddlefish. We should not try to get rid of it. It just need to find another place.
Please see my comment in this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=743359#c5
Can you just tell me if I should hold this during this discussion or can I go ahead and land this ?

Irakli Gozalishvili Collaborator
Gozala added a note April 12, 2012

I will try to submit patch that would refactor parts of loader out in a similar way you've suggested. I'd rather see this change land on top of that one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/l10n/html.js
((34 lines not shown))
  34
+  // Accept only HTML documents
  35
+  if (!(document instanceof Ci.nsIDOMHTMLDocument))
  36
+    return;
  37
+
  38
+  // Accept only document from this addon
  39
+  if (document.location.href.indexOf(uriPrefix) !== 0)
  40
+    return;
  41
+
  42
+  // First hide content of the document in order to have content blinking
  43
+  // between untranslated and translated states
  44
+  // TODO: use result of bug 737003 discussion in order to avoid any conflict
  45
+  // with document CSS
  46
+  document.documentElement.style.visibility = "hidden";
  47
+
  48
+  // Wait for DOM tree to be built before applying localization
  49
+  document.addEventListener("DOMContentLoaded", function listener() {
1
Irakli Gozalishvili Collaborator
Gozala added a note April 11, 2012

Nit: would be nice if we could reuse same listener, which I think should be possible if you use event.target in the listener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Irakli Gozalishvili
Collaborator

Overall it looks good! Not quite sure why this g prefix conventions all over the place.
Only real concern is a loader change, which would require landing all of this to firefox along with loader which I don't think is a good idea. We need to find a way to decouple add-on bootstrap stuff form loader itself.

ochameau
Collaborator

I rebased against last master. So that html localization is now initialized in addon/runner.js.

packages/addon-kit/locale/en-GB.properties
... ...
@@ -4,6 +4,8 @@
4 4
 
5 5
 Translated= Yes
6 6
 
  7
+text-content=no <span>HTML</span> injection
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

Maybe <strong> or <italic> is better so it's visually distinct ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/addon/runner.js
... ...
@@ -62,6 +62,16 @@ 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
  66
+  try {
  67
+    // Do not enable HTML localization while running test as it is hard to
  68
+    // disable. Because unit tests are evaluated in a another Loader who
  69
+    // doesn't have access to this current loader.
  70
+    if (options.loader.main.path !== "test-harness/lib/run-tests.js")
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

Nit: I think it would be better off assertinng against main.id instead!! (Assuming value there is `'test-harness/run-tests')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/addon/runner.js
... ...
@@ -62,6 +62,16 @@ 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
  66
+  try {
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

This looks like if we fail to enable localization we will just log error. Could explain why in the comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/l10n/core.js
((17 lines not shown))
  17
+}
  18
+
  19
+Object.defineProperties(exports, {
  20
+  // Returns the full length locale code: ja-JP-mac, en-US or fr
  21
+  locale: {
  22
+    get: function () {
  23
+      return bestMatchingLocale;
  24
+    }
  25
+  },
  26
+  // Returns the short locale code: ja, en, fr
  27
+  language: {
  28
+    get: function () {
  29
+      return bestMatchingLocale.split("-")[0].toLowerCase();
  30
+    }
  31
+  }
  32
+});
3
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

I don't want to be picky, but is there reason why not us just simple functions instead, like locale() or getLocale() ? You can ignore it if you like as we do this in other places as well, but it looks like we won't be able to do it with ES.next modules so I'd suggest use of plain functions in new code so we don't increase surface for incompatibilities with harmony.

ochameau Collaborator
ochameau added a note April 22, 2012

That's unfortunate! It is defined as getter just because I won't be able to get locale value synchronously in the long run.
Converting them to functions just sounds like a workaround ES.next limitation :(
I have in mind to convert l10n files access to asynchronous access,
We will have to delay addon main module evaluation before l10n files are loaded.
(We have to do that, as require("l10n").get is synchronous.)
Something like this in addon/runner.js:

require("api-utils/l10n/core").load().then(function () {
  // load main
  ...
  let program = load(loader, loader.main).exports;
  ...
});

But then, the unecessary init method in l10n will become this load method and be asynchronous.
So that we won't be able to set exports.locale during module loading
Do you have any idea how to define an attribute in ES.next that can't be defined during module load?
Is this a usecase where we have to end up with functions?

Irakli Gozalishvili Collaborator
Gozala added a note April 23, 2012

ES.next won't allow dynamic properties / accessors as there is no exports object and imported modules are not an objects. You import objects / functions that were marked for export. Solution there is to export an object with such getters, which may be an option here as well.

As for async API that you've outlined above, I think it would be better to have it from day one, since usually users don't like switching from sync APIs to async. Also implications of such change may be bigger than it usually is.

Finally your then callback can return object with getters / properties l10n which may be a good alternative.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Irakli Gozalishvili Gozala commented on the diff April 19, 2012
packages/api-utils/lib/l10n/core.js
((26 lines not shown))
  26
+  // Returns the short locale code: ja, en, fr
  27
+  language: {
  28
+    get: function () {
  29
+      return bestMatchingLocale.split("-")[0].toLowerCase();
  30
+    }
  31
+  }
  32
+});
  33
+
  34
+function readURI(uri) {
  35
+  let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  36
+                createInstance(Ci.nsIXMLHttpRequest);
  37
+  request.open('GET', uri, false);
  38
+  request.overrideMimeType('text/plain');
  39
+  request.send();
  40
+  return request.responseText;
  41
+}
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

Now that's yet another place we implement readURI I remember talking with @ZER0 about factoring it out to a separate module, don't know if he end up doing it, but maybe you could if he did not ?

If you prefer not to then please create a follow up bug to do that and put a comment referring to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/l10n/core.js
((45 lines not shown))
  45
+    return JSON.parse(readURI(uri));
  46
+  }
  47
+  catch(e) {
  48
+    console.error("Error while reading locale file:\n" + uri + "\n" + e);
  49
+  }
  50
+  return {};
  51
+}
  52
+
  53
+// Returns the array stored in `locales.json` manifest that list available
  54
+// locales files
  55
+function getAvailableLocales() {
  56
+  let uri = rootURI + "locales.json";
  57
+  let manifest = readJsonUri(uri);
  58
+
  59
+  return "locales" in manifest && Array.isArray(manifest.locales) ?
  60
+         manifest.locales : [];
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

Nit: It would be easier to read if you head line break just before Array.isArray in order to have complete condition in one line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Irakli Gozalishvili Gozala commented on the diff April 19, 2012
packages/api-utils/lib/l10n/core.js
((79 lines not shown))
  79
+  return rootURI + "locale/" + bestMatchingLocale + ".json";
  80
+}
  81
+
  82
+function init() {
  83
+  // First, search for a locale file:
  84
+  let localeURI = getBestLocaleFile();
  85
+  if (!localeURI)
  86
+    return;
  87
+
  88
+  // Locale files only contains one big JSON object that is used as
  89
+  // an hashtable of: "key to translate" => "translated key"
  90
+  // TODO: We are likely to change this in order to be able to overload
  91
+  //       a specific key translation. For a specific package, module or line?
  92
+  globalHash = readJsonUri(localeURI);
  93
+}
  94
+init();
2
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

It looks like you don't really need this function. In fact I would prefer following instead:

const globalHash = new function GlobalHash() {
   // your init function body here
}
ochameau Collaborator
ochameau added a note April 22, 2012

It does not only set globalHash but inderectly set bestMatchingLocale.
I can just remove this method and put all this code in top level?
(but this method is meant to be exported, be asynchronous and be called by addon runner)
(See #387 (comment) for more info)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/l10n/html.js
... ...
@@ -0,0 +1,84 @@
  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 observers = require("api-utils/observer-service");
  7
+const core = require("api-utils/l10n/core");
  8
+const { uriPrefix } = require("@packaging");
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

could you please use prefixURI instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/l10n/html.js
((9 lines not shown))
  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)
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

why not ?

if (data)
  child.textContent = data;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
packages/api-utils/lib/l10n/html.js
... ...
@@ -0,0 +1,84 @@
  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 observers = require("api-utils/observer-service");
1
Irakli Gozalishvili Collaborator
Gozala added a note April 19, 2012

Please use api-utils/system/events instead. I plan to deprecate this soonish!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Irakli Gozalishvili
Collaborator

Looks good r+ but please make sure to address following comments before landing:

All others are optional and I'd leave it up to you.

Thanks!

Irakli Gozalishvili
Collaborator

BTW do we really need to read all this file sync here ? Can't we do it async instead to avoid blocking firefox (specially since this happens at startup per add-on) ?

ochameau
Collaborator

I fixed prefixURI and used new observer event API.
I'd like to keep readURI refactoring in a followup patch as it won't be specific to html localization.
And yes, synchronous file access is bad and can be avoided. Especially now that we have addon/runner!
I can open another bug to start discussion how we should do that.
(see #387 (comment))

I didn't changed locale/language defined as getter. I'd like to avoid ending with function if possible.
There might be a way to avoid using getters while implementing asynchronous version!

May I land in these conditions?

Irakli Gozalishvili
Collaborator

@ochameau I'm not able to catch on IRC, so I'm writing here. I can leave with locale and language being getters (as we already do it in other places), but would really encourage to use simple functions instead, that way we can always improve API by switching to getters, which is much better than breaking API in a future and making it less pleasing.

Also I added comment on getters and ES.next modules above. Feel free to land as is if you feel strong about it. Also if you end up with a change no need for another review.

ochameau
Collaborator

I've changed these attributes to functions. It isn't that big deal for me, especially if we can find ways to improve it latter if we decide to give more visibility to these attributes.

ochameau ochameau merged commit 9bd8d3b into from April 23, 2012
ochameau ochameau closed this April 23, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Apr 23, 2012
ochameau Bug 739249: Implement HTML Localization. 9bddccd
This page is out of date. Refresh to see the latest.
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);
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.