diff --git a/external/webL10n/README.md b/external/webL10n/README.md new file mode 100644 index 000000000..52995522c --- /dev/null +++ b/external/webL10n/README.md @@ -0,0 +1,3 @@ +The source code for the library can be found at + + https://github.com/fabi1cazenave/webL10n diff --git a/external/webL10n/l10n.js b/external/webL10n/l10n.js new file mode 100644 index 000000000..fedf70cde --- /dev/null +++ b/external/webL10n/l10n.js @@ -0,0 +1,304 @@ +/* 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. + */ + +'use strict'; + +(function(window) { + var gL10nData = {}; + var gTextData = ''; + var gLanguage = ''; + + // parser + + function evalString(text) { + return text.replace(/\\\\/g, '\\') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\b/g, '\b') + .replace(/\\f/g, '\f') + .replace(/\\{/g, '{') + .replace(/\\}/g, '}') + .replace(/\\"/g, '"') + .replace(/\\'/g, "'"); + } + + function parseProperties(text, lang) { + var reBlank = /^\s*|\s*$/; + var reComment = /^\s*#|^\s*$/; + var reSection = /^\s*\[(.*)\]\s*$/; + var reImport = /^\s*@import\s+url\((.*)\)\s*$/i; + + // parse the *.properties file into an associative array + var currentLang = '*'; + var supportedLang = []; + var skipLang = false; + var data = []; + var match = ''; + var entries = text.replace(reBlank, '').split(/[\r\n]+/); + for (var i = 0; i < entries.length; i++) { + var line = entries[i]; + + // comment or blank line? + if (reComment.test(line)) + continue; + + // section start? + if (reSection.test(line)) { + match = reSection.exec(line); + currentLang = match[1]; + skipLang = (currentLang != lang) && (currentLang != '*'); + continue; + } else if (skipLang) { + continue; + } + + // @import rule? + if (reImport.test(line)) { + match = reImport.exec(line); + } + + // key-value pair + var tmp = line.split('='); + if (tmp.length > 1) + data[tmp[0]] = evalString(tmp[1]); + } + + // find the attribute descriptions, if any + for (var key in data) { + var id, prop, index = key.lastIndexOf('.'); + if (index > 0) { // attribute + id = key.substring(0, index); + prop = key.substr(index + 1); + } else { // textContent, could be innerHTML as well + id = key; + prop = 'textContent'; + } + if (!gL10nData[id]) + gL10nData[id] = {}; + gL10nData[id][prop] = data[key]; + } + } + + function parse(text, lang) { + gTextData += text; + // we only support *.properties files at the moment + return parseProperties(text, lang); + } + + // load and parse the specified resource file + function loadResource(href, lang, onSuccess, onFailure) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', href, true); + xhr.overrideMimeType('text/plain; charset=utf-8'); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200 || xhr.status == 0) { + parse(xhr.responseText, lang); + if (onSuccess) + onSuccess(); + } else { + if (onFailure) + onFailure(); + } + } + }; + xhr.send(null); + } + + // load and parse all resources for the specified locale + function loadLocale(lang, callback) { + clear(); + + // check all nodes + // and load the resource files + var langLinks = document.querySelectorAll('link[type="application/l10n"]'); + var langCount = langLinks.length; + + // start the callback when all resources are loaded + var onResourceLoaded = null; + var gResourceCount = 0; + onResourceLoaded = function() { + gResourceCount++; + if (gResourceCount >= langCount) { + // execute the [optional] callback + if (callback) + callback(); + // fire a 'localized' DOM event + var evtObject = document.createEvent('Event'); + evtObject.initEvent('localized', false, false); + evtObject.language = lang; + window.dispatchEvent(evtObject); + } + } + + // load all resource files + function l10nResourceLink(link) { + var href = link.href; + var type = link.type; + this.load = function(lang, callback) { + var applied = lang; + loadResource(href, lang, callback, function() { + console.warn(href + ' not found.'); + applied = ''; + }); + return applied; // return lang if found, an empty string if not found + }; + } + + gLanguage = lang; + for (var i = 0; i < langCount; i++) { + var resource = new l10nResourceLink(langLinks[i]); + var rv = resource.load(lang, onResourceLoaded); + if (rv != lang) // lang not found, used default resource instead + gLanguage = ''; + } + } + + // fetch an l10n object, warn if not found + function getL10nData(key) { + var data = gL10nData[key]; + if (!data) + console.warn('[l10n] #' + key + ' missing for [' + gLanguage + ']'); + return data; + } + + // replace {{arguments}} with their values + function substArguments(str, args) { + var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/; + var match = reArgs.exec(str); + while (match) { + if (!match || match.length < 2) + return str; // argument key not found + + var arg = match[1]; + var sub = ''; + if (arg in args) { + sub = args[arg]; + } else if (arg in gL10nData) { + sub = gL10nData[arg].textContent; + } else { + console.warn('[l10n] could not find argument {{' + arg + '}}'); + return str; + } + + str = str.substring(0, match.index) + sub + + str.substr(match.index + match[0].length); + match = reArgs.exec(str); + } + return str; + } + + // translate a string + function translateString(key, args) { + var data = getL10nData(key); + if (!data) + return '{{' + key + '}}'; + return substArguments(data.textContent, args); + } + + // translate an HTML element + function translateElement(element) { + if (!element || !element.dataset) + return; + + // get the related l10n object + var key = element.dataset.l10nId; + var data = getL10nData(key); + if (!data) + return; + + // get arguments (if any) + // TODO: more flexible parser? + var args; + if (element.dataset.l10nArgs) try { + args = JSON.parse(element.dataset.l10nArgs); + } catch (e) { + console.warn('[l10n] could not parse arguments for #' + key + ''); + } + + // translate element + // TODO: security check? + for (var k in data) + element[k] = substArguments(data[k], args); + } + + // translate an HTML subtree + function translateFragment(element) { + element = element || document.querySelector('html'); + + // 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++) + translateElement(children[i]); + + // translate element itself if necessary + if (element.dataset.l10nId) + translateElement(element); + } + + // clear all l10n data + function clear() { + gL10nData = {}; + gTextData = ''; + gLanguage = ''; + } + + // load the default locale on startup + window.addEventListener('DOMContentLoaded', function() { + var lang = navigator.language; + if (navigator.mozSettings) { + var req = navigator.mozSettings.getLock().get('language.current'); + req.onsuccess = function() { + loadLocale(req.result['language.current'] || lang, translateFragment); + }; + req.onerror = function() { + loadLocale(lang, translateFragment); + }; + } else { + loadLocale(lang, translateFragment); + } + }); + + // Public API + document.mozL10n = { + // get a localized string + get: translateString, + + // get|set the document language and direction + get language() { + return { + // get|set the document language (ISO-639-1) + get code() { return gLanguage; }, + set code(lang) { loadLocale(lang, translateFragment); }, + + // get the direction (ltr|rtl) of the current language + get direction() { + // http://www.w3.org/International/questions/qa-scripts + // Arabic, Hebrew, Farsi, Pashto, Urdu + var rtlList = ['ar', 'he', 'fa', 'ps', 'ur']; + return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr'; + } + }; + } + }; +})(this);