From d0d3b071eca5a8bbee66eb11d3d188ce72ea420b Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Sun, 15 Sep 2013 14:56:36 +0200 Subject: [PATCH 1/2] 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. --- external/webL10n/l10n.js | 432 ++++++++++++++++++++++++++++++--------- 1 file changed, 341 insertions(+), 91 deletions(-) diff --git a/external/webL10n/l10n.js b/external/webL10n/l10n.js index 543563165..1c4ab6a93 100644 --- a/external/webL10n/l10n.js +++ b/external/webL10n/l10n.js @@ -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