Update webL10n to latest version + features

Base version of l10n:
- https://github.com/fabi1cazenave/webL10n/tree/b18c753c6f
4 extra commits (expected to be accepted):
- https://github.com/fabi1cazenave/webL10n/pull/38

New features compared to mozL10n:
- Support for getting translated attributes in get()

The previous version of mozL10n was based on:
- https://github.com/fabi1cazenave/webL10n/commits/0c06867a75
- diff: http://pastebin.mozilla.org/3061694

To make it easier to update webL10n in the future, I will apply
the PDF.js-specific changes in a separate commit.
This commit is contained in:
Rob Wu 2013-09-15 14:56:36 +02:00
parent 4f243b39b4
commit d0d3b071ec

View File

@ -1,30 +1,25 @@
/** Copyright (c) 2011-2012 Fabien Cazenave, Mozilla.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/*
Additional modifications for PDF.js project:
- Disables language initialization on page loading;
- Adds fallback argument to the getL10nData;
- Removes consoleLog and simplifies consoleWarn;
- Removes window._ assignment.
*/
/**
* Copyright (c) 2011-2013 Fabien Cazenave, Mozilla.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/*jshint browser: true, devel: true, es5: true, globalstrict: true */
'use strict';
@ -36,13 +31,44 @@ document.webL10n = (function(window, document, undefined) {
var gMacros = {};
var gReadyState = 'loading';
// read-only setting -- we recommend to load l10n resources synchronously
var gAsyncResourceLoading = true;
// debug helpers
/**
* Synchronously loading l10n resources significantly minimizes flickering
* from displaying the app with non-localized strings and then updating the
* strings. Although this will block all script execution on this page, we
* expect that the l10n resources are available locally on flash-storage.
*
* As synchronous XHR is generally considered as a bad idea, we're still
* loading l10n resources asynchronously -- but we keep this in a setting,
* just in case... and applications using this library should hide their
* content until the `localized' event happens.
*/
var gAsyncResourceLoading = true; // read-only
/**
* Debug helpers
*
* gDEBUG == 0: don't display any console message
* gDEBUG == 1: display only warnings, not logs
* gDEBUG == 2: display all console messages
*/
var gDEBUG = 1;
function consoleLog(message) {
if (gDEBUG >= 2) {
console.log('[l10n] ' + message);
}
}
function consoleWarn(message) {
console.log('[l10n] ' + message);
};
if (gDEBUG) {
console.warn('[l10n] ' + message);
}
}
/**
* DOM helpers for the so-called "HTML API".
@ -55,6 +81,12 @@ document.webL10n = (function(window, document, undefined) {
return document.querySelectorAll('link[type="application/l10n"]');
}
function getL10nDictionary() {
var script = document.querySelector('script[type="application/l10n"]');
// TODO: support multiple and external JSON dictionaries
return script ? JSON.parse(script.innerHTML) : null;
}
function getTranslatableChildren(element) {
return element ? element.querySelectorAll('*[data-l10n-id]') : [];
}
@ -78,9 +110,41 @@ document.webL10n = (function(window, document, undefined) {
function fireL10nReadyEvent(lang) {
var evtObject = document.createEvent('Event');
evtObject.initEvent('localized', false, false);
evtObject.initEvent('localized', true, false);
evtObject.language = lang;
window.dispatchEvent(evtObject);
document.dispatchEvent(evtObject);
}
function xhrLoadText(url, onSuccess, onFailure, asynchronous) {
onSuccess = onSuccess || function _onSuccess(data) {};
onFailure = onFailure || function _onFailure() {
consoleWarn(url + ' not found.');
};
var xhr = new XMLHttpRequest();
xhr.open('GET', url, asynchronous);
if (xhr.overrideMimeType) {
xhr.overrideMimeType('text/plain; charset=utf-8');
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) {
onSuccess(xhr.responseText);
} else {
onFailure();
}
}
};
xhr.onerror = onFailure;
xhr.ontimeout = onFailure;
// in Firefox OS with the app:// protocol, trying to XHR a non-existing
// URL will raise an exception here -- hence this ugly try...catch.
try {
xhr.send(null);
} catch (e) {
onFailure();
}
}
@ -108,7 +172,7 @@ document.webL10n = (function(window, document, undefined) {
*/
function parseResource(href, lang, successCallback, failureCallback) {
var baseURL = href.replace(/\/[^\/]*$/, '/');
var baseURL = href.replace(/[^\/]*$/, '') || './';
// handle escaped characters (backslashes) in a string
function evalString(text) {
@ -171,16 +235,17 @@ document.webL10n = (function(window, document, undefined) {
// key-value pair
var tmp = line.match(reSplit);
if (tmp && tmp.length == 3)
if (tmp && tmp.length == 3) {
dictionary[tmp[1]] = evalString(tmp[2]);
}
}
}
// import another *.properties file
function loadImport(url) {
loadResource(url, function(content) {
xhrLoadText(url, function(content) {
parseRawLines(content, false); // don't allow recursive imports
}, false, false); // load synchronously
}, null, false); // load synchronously
}
// fill the dictionary
@ -188,29 +253,8 @@ document.webL10n = (function(window, document, undefined) {
return dictionary;
}
// load the specified resource file
function loadResource(url, onSuccess, onFailure, asynchronous) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, asynchronous);
if (xhr.overrideMimeType) {
xhr.overrideMimeType('text/plain; charset=utf-8');
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) {
if (onSuccess)
onSuccess(xhr.responseText);
} else {
if (onFailure)
onFailure();
}
}
};
xhr.send(null);
}
// load and parse l10n data (warning: global variables are used here)
loadResource(href, function(response) {
xhrLoadText(href, function(response) {
gTextData += response; // mostly for debug
// parse *.properties text data into an l10n dictionary
@ -233,13 +277,16 @@ document.webL10n = (function(window, document, undefined) {
}
// trigger callback
if (successCallback)
if (successCallback) {
successCallback();
}
}, failureCallback, gAsyncResourceLoading);
};
}
// load and parse all resources for the specified locale
function loadLocale(lang, callback) {
callback = callback || function _callback() {};
clear();
gLanguage = lang;
@ -247,8 +294,17 @@ document.webL10n = (function(window, document, undefined) {
// and load the resource files
var langLinks = getL10nResourceLinks();
var langCount = langLinks.length;
if (langCount == 0) {
consoleWarn('no resource to load, early way out');
if (langCount === 0) {
// we might have a pre-compiled dictionary instead
var dict = getL10nDictionary();
if (dict && dict.locales && dict.default_locale) {
consoleLog('using the embedded JSON directory, early way out');
gL10nData = dict.locales[lang] || dict.locales[dict.default_locale];
callback();
} else {
consoleLog('no resource to load, early way out');
}
// early way out
fireL10nReadyEvent(lang);
gReadyState = 'complete';
return;
@ -260,15 +316,14 @@ document.webL10n = (function(window, document, undefined) {
onResourceLoaded = function() {
gResourceCount++;
if (gResourceCount >= langCount) {
if (callback) // execute the [optional] callback
callback();
callback();
fireL10nReadyEvent(lang);
gReadyState = 'complete';
}
};
// load all resource files
function l10nResourceLink(link) {
function L10nResourceLink(link) {
var href = link.href;
var type = link.type;
this.load = function(lang, callback) {
@ -282,7 +337,7 @@ document.webL10n = (function(window, document, undefined) {
}
for (var i = 0; i < langCount; i++) {
var resource = new l10nResourceLink(langLinks[i]);
var resource = new L10nResourceLink(langLinks[i]);
var rv = resource.load(lang, onResourceLoaded);
if (rv != lang) { // lang not found, used default resource instead
consoleWarn('"' + lang + '" resource not found');
@ -723,8 +778,9 @@ document.webL10n = (function(window, document, undefined) {
return str;
// initialize _pluralRules
if (!gMacros._pluralRules)
if (!gMacros._pluralRules) {
gMacros._pluralRules = getPluralRules(gLanguage);
}
var index = '[' + gMacros._pluralRules(n) + ']';
// try to find a [zero|one|two] key if it's defined
@ -736,6 +792,8 @@ document.webL10n = (function(window, document, undefined) {
str = gL10nData[key + '[two]'][prop];
} else if ((key + index) in gL10nData) {
str = gL10nData[key + index][prop];
} else if ((key + '[other]') in gL10nData) {
str = gL10nData[key + '[other]'][prop];
}
return str;
@ -750,7 +808,7 @@ document.webL10n = (function(window, document, undefined) {
function getL10nData(key, args, fallback) {
var data = gL10nData[key];
if (!data) {
consoleWarn('#' + key + ' missing for [' + gLanguage + ']');
consoleWarn('#' + key + ' is undefined.');
if (!fallback) {
return null;
}
@ -766,7 +824,7 @@ document.webL10n = (function(window, document, undefined) {
for (var prop in data) {
var str = data[prop];
str = substIndexes(str, args, key, prop);
str = substArguments(str, args);
str = substArguments(str, args, key);
rv[prop] = str;
}
return rv;
@ -799,8 +857,8 @@ document.webL10n = (function(window, document, undefined) {
}
// replace {{arguments}} with their values
function substArguments(str, args) {
var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/;
function substArguments(str, args, key) {
var reArgs = /\{\{\s*(.+?)\s*\}\}/;
var match = reArgs.exec(str);
while (match) {
if (!match || match.length < 2)
@ -808,12 +866,12 @@ document.webL10n = (function(window, document, undefined) {
var arg = match[1];
var sub = '';
if (arg in args) {
if (args && arg in args) {
sub = args[arg];
} else if (arg in gL10nData) {
sub = gL10nData[arg][gTextProp];
} else {
consoleWarn('could not find argument {{' + arg + '}}');
consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.');
return str;
}
@ -833,23 +891,21 @@ document.webL10n = (function(window, document, undefined) {
// get the related l10n object
var data = getL10nData(l10n.id, l10n.args);
if (!data) {
consoleWarn('#' + l10n.id + ' missing for [' + gLanguage + ']');
consoleWarn('#' + l10n.id + ' is undefined.');
return;
}
// translate element (TODO: security checks?)
// for the node content, replace the content of the first child textNode
// and clear other child textNodes
if (data[gTextProp]) { // XXX
if (element.children.length === 0) {
if (getChildElementCount(element) === 0) {
element[gTextProp] = data[gTextProp];
} else {
var children = element.childNodes,
found = false;
// this element has element children: replace the content of the first
// (non-empty) child textNode and clear other child textNodes
var children = element.childNodes;
var found = false;
for (var i = 0, l = children.length; i < l; i++) {
if (children[i].nodeType === 3 &&
/\S/.test(children[i].textContent)) { // XXX
// using nodeValue seems cross-browser
if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
if (found) {
children[i].nodeValue = '';
} else {
@ -858,8 +914,11 @@ document.webL10n = (function(window, document, undefined) {
}
}
}
// if no (non-empty) textNode is found, insert a textNode before the
// first element child.
if (!found) {
consoleWarn('unexpected error, could not translate element content');
var textNode = document.createTextNode(data[gTextProp]);
element.insertBefore(textNode, element.firstChild);
}
}
delete data[gTextProp];
@ -870,6 +929,21 @@ document.webL10n = (function(window, document, undefined) {
}
}
// webkit browsers don't currently support 'children' on SVG elements...
function getChildElementCount(element) {
if (element.children) {
return element.children.length;
}
if (typeof element.childElementCount !== 'undefined') {
return element.childElementCount;
}
var count = 0;
for (var i = 0; i < element.childNodes.length; i++) {
count += element.nodeType === 1 ? 1 : 0;
}
return count;
}
// translate an HTML subtree
function translateFragment(element) {
element = element || document.documentElement;
@ -885,13 +959,169 @@ document.webL10n = (function(window, document, undefined) {
translateElement(element);
}
/**
* Startup & Public API
*
* Warning: this part of the code contains browser-specific chunks --
* that's where obsolete browsers, namely IE8 and earlier, are handled.
*
* Unlike the rest of the lib, this section is not shared with FirefoxOS/Gaia.
*/
// load the default locale on startup
function l10nStartup() {
gReadyState = 'interactive';
// most browsers expose the UI language as `navigator.language'
// but IE uses `navigator.userLanguage' instead
var userLocale = navigator.language || navigator.userLanguage;
consoleLog('loading [' + userLocale + '] resources, ' +
(gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
// load the default locale and translate the document if required
if (document.documentElement.lang === userLocale) {
loadLocale(userLocale);
} else {
loadLocale(userLocale, translateFragment);
}
}
// browser-specific startup
if (document.addEventListener) { // modern browsers and IE9+
if (document.readyState === 'loading') {
// the document is not fully loaded yet: wait for DOMContentLoaded.
document.addEventListener('DOMContentLoaded', l10nStartup);
} else {
// l10n.js is being loaded with <script defer> or <script async>,
// the DOM is ready for parsing.
window.setTimeout(l10nStartup);
}
} else if (window.attachEvent) { // IE8 and before (= oldIE)
// TODO: check if jQuery is loaded (CSS selector + JSON + events)
// dummy `console.log' and `console.warn' functions
if (!window.console) {
consoleLog = function(message) {}; // just ignore console.log calls
consoleWarn = function(message) {
if (gDEBUG) {
alert('[l10n] ' + message); // vintage debugging, baby!
}
};
}
// XMLHttpRequest for IE6
if (!window.XMLHttpRequest) {
xhrLoadText = function(url, onSuccess, onFailure, asynchronous) {
onSuccess = onSuccess || function _onSuccess(data) {};
onFailure = onFailure || function _onFailure() {
consoleWarn(url + ' not found.');
};
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
xhr.open('GET', url, asynchronous);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
onSuccess(xhr.responseText);
} else {
onFailure();
}
}
};
xhr.send(null);
};
}
// worst hack ever for IE6 and IE7
if (!window.JSON) {
getL10nAttributes = function(element) {
if (!element)
return {};
var l10nId = element.getAttribute('data-l10n-id'),
l10nArgs = element.getAttribute('data-l10n-args'),
args = {};
if (l10nArgs) try {
args = eval(l10nArgs); // XXX yeah, I know...
} catch (e) {
consoleWarn('could not parse arguments for #' + l10nId);
}
return { id: l10nId, args: args };
};
}
// override `getTranslatableChildren' and `getL10nResourceLinks'
if (!document.querySelectorAll) {
getTranslatableChildren = function(element) {
if (!element)
return [];
var nodes = element.getElementsByTagName('*'),
l10nElements = [],
n = nodes.length;
for (var i = 0; i < n; i++) {
if (nodes[i].getAttribute('data-l10n-id'))
l10nElements.push(nodes[i]);
}
return l10nElements;
};
getL10nResourceLinks = function() {
var links = document.getElementsByTagName('link'),
l10nLinks = [],
n = links.length;
for (var i = 0; i < n; i++) {
if (links[i].type == 'application/l10n')
l10nLinks.push(links[i]);
}
return l10nLinks;
};
}
// override `getL10nDictionary'
if (!window.JSON || !document.querySelectorAll) {
getL10nDictionary = function() {
var scripts = document.getElementsByName('script');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].type == 'application/l10n') {
return eval(scripts[i].innerHTML);
}
}
return null;
};
}
// fire non-standard `localized' DOM events
if (document.createEventObject && !document.createEvent) {
fireL10nReadyEvent = function(lang) {
// hack to simulate a custom event in IE:
// to catch this event, add an event handler to `onpropertychange'
document.documentElement.localized = 1;
};
}
// startup for IE<9
window.attachEvent('onload', function() {
gTextProp = document.body.textContent ? 'textContent' : 'innerText';
l10nStartup();
});
}
// cross-browser API (sorry, oldIE doesn't support getters & setters)
return {
// get a localized string
get: function(key, args, fallback) {
var data = getL10nData(key, args, {textContent: fallback});
if (data) { // XXX double-check this
return 'textContent' in data ? data.textContent : '';
get: function(key, args, fallbackString) {
var index = key.lastIndexOf('.');
var prop = gTextProp;
if (index > 0) { // An attribute has been specified
prop = key.substr(index + 1);
key = key.substring(0, index);
}
var fallback;
if (fallbackString) {
fallback = {};
fallback[prop] = fallbackString;
}
var data = getL10nData(key, args, fallback);
if (data && prop in data) {
return data[prop];
}
return '{{' + key + '}}';
},
@ -916,7 +1146,27 @@ document.webL10n = (function(window, document, undefined) {
translate: translateFragment,
// this can be used to prevent race conditions
getReadyState: function() { return gReadyState; }
getReadyState: function() { return gReadyState; },
ready: function(callback) {
if (!callback) {
return;
} else if (gReadyState == 'complete' || gReadyState == 'interactive') {
window.setTimeout(callback);
} else if (document.addEventListener) {
document.addEventListener('localized', callback);
} else if (document.attachEvent) {
document.documentElement.attachEvent('onpropertychange', function(e) {
if (e.propertyName === 'localized') {
callback();
}
});
}
}
};
}) (window, document);
// gettext-like shortcut for document.webL10n.get
if (window._ === undefined) {
var _ = document.webL10n.get;
}