/** * Copyright (c) 2012 Marcel Klehr * 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. */ window.html10n = (function(window, document, undefined) { // fix console (function() { var noop = function() {}; var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; var console = (window.console = window.console || {}); for (var i = 0; i < names.length; ++i) { if (!console[names[i]]) { console[names[i]] = noop; } } }()); // fix Array#forEach in IE // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach if (!Array.prototype.forEach) { Array.prototype.forEach = function(fn, scope) { for(var i = 0, len = this.length; i < len; ++i) { if (i in this) { fn.call(scope, this[i], i, this); } } }; } // fix Array#indexOf in, guess what, IE! <3 // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { "use strict"; if (this == null) { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; if (len === 0) { return -1; } var n = 0; if (arguments.length > 1) { n = Number(arguments[1]); if (n != n) { // shortcut for verifying if it's NaN n = 0; } else if (n != 0 && n != Infinity && n != -Infinity) { n = (n > 0 || -1) * Math.floor(Math.abs(n)); } } if (n >= len) { return -1; } var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); for (; k < len; k++) { if (k in t && t[k] === searchElement) { return k; } } return -1; } } /** * MicroEvent - to make any js object an event emitter (server or browser) */ var MicroEvent = function(){} MicroEvent.prototype = { bind: function(event, fct){ this._events = this._events || {}; this._events[event] = this._events[event] || []; this._events[event].push(fct); }, unbind: function(event, fct){ this._events = this._events || {}; if( event in this._events === false ) return; this._events[event].splice(this._events[event].indexOf(fct), 1); }, trigger: function(event /* , args... */){ this._events = this._events || {}; if( event in this._events === false ) return; for(var i = 0; i < this._events[event].length; i++){ this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)) } } }; /** * mixin will delegate all MicroEvent.js function in the destination object * @param {Object} the object which will support MicroEvent */ MicroEvent.mixin = function(destObject){ var props = ['bind', 'unbind', 'trigger']; if(!destObject) return; for(var i = 0; i < props.length; i ++){ destObject[props[i]] = MicroEvent.prototype[props[i]]; } } /** * Loader * The loader is responsible for loading * and caching all necessary resources */ function Loader(resources) { this.resources = resources this.cache = {} // file => contents this.langs = {} // lang => strings } Loader.prototype.load = function(lang, cb) { if(this.langs[lang]) return cb() if (this.resources.length > 0) { var reqs = 0; for (var i=0, n=this.resources.length; i < n; i++) { this.fetch(this.resources[i], lang, function(e) { reqs++; if(e) console.warn(e) if (reqs < n) return;// Call back once all reqs are completed cb && cb() }) } } } Loader.prototype.fetch = function(href, lang, cb) { var that = this if (this.cache[href]) { this.parse(lang, href, this.cache[href], cb) return; } var xhr = new XMLHttpRequest() xhr.open('GET', href, /*async: */true) if (xhr.overrideMimeType) { xhr.overrideMimeType('application/json; charset=utf-8'); } xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status === 0) { var data = JSON.parse(xhr.responseText) that.cache[href] = data // Pass on the contents for parsing that.parse(lang, href, data, cb) } else { cb(new Error('Failed to load '+href)) } } }; xhr.send(null); } Loader.prototype.parse = function(lang, currHref, data, cb) { if ('object' != typeof data) { cb(new Error('A file couldn\'t be parsed as json.')) return } // dat alng ain't here, man! if (!data[lang]) { var msg = 'Couldn\'t find translations for '+lang , l if(~lang.indexOf('-')) lang = lang.split('-')[0] // then let's try related langs for(l in data) { if(lang != l && l.indexOf(lang) === 0 && data[l]) { lang = l break; } } if(lang != l) return cb(new Error(msg)) } if ('string' == typeof data[lang]) { // Import rule // absolute path var importUrl = data[lang] // relative path if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) { importUrl = currHref+"/../"+data[lang] } this.fetch(importUrl, lang, cb) return } if ('object' != typeof data[lang]) { cb(new Error('Translations should be specified as JSON objects!')) return } this.langs[lang] = data[lang] // TODO: Also store accompanying langs cb() } /** * The html10n object */ var html10n = { language : null } MicroEvent.mixin(html10n) html10n.macros = {} html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"] /** * Get rules for plural forms (shared with JetPack), see: * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p * * @param {string} lang * locale (language) used. * * @return {Function} * returns a function that gives the plural form name for a given integer: * var fun = getPluralRules('en'); * fun(1) -> 'one' * fun(0) -> 'other' * fun(1000) -> 'other'. */ function getPluralRules(lang) { var locales2rules = { 'af': 3, 'ak': 4, 'am': 4, 'ar': 1, 'asa': 3, 'az': 0, 'be': 11, 'bem': 3, 'bez': 3, 'bg': 3, 'bh': 4, 'bm': 0, 'bn': 3, 'bo': 0, 'br': 20, 'brx': 3, 'bs': 11, 'ca': 3, 'cgg': 3, 'chr': 3, 'cs': 12, 'cy': 17, 'da': 3, 'de': 3, 'dv': 3, 'dz': 0, 'ee': 3, 'el': 3, 'en': 3, 'eo': 3, 'es': 3, 'et': 3, 'eu': 3, 'fa': 0, 'ff': 5, 'fi': 3, 'fil': 4, 'fo': 3, 'fr': 5, 'fur': 3, 'fy': 3, 'ga': 8, 'gd': 24, 'gl': 3, 'gsw': 3, 'gu': 3, 'guw': 4, 'gv': 23, 'ha': 3, 'haw': 3, 'he': 2, 'hi': 4, 'hr': 11, 'hu': 0, 'id': 0, 'ig': 0, 'ii': 0, 'is': 3, 'it': 3, 'iu': 7, 'ja': 0, 'jmc': 3, 'jv': 0, 'ka': 0, 'kab': 5, 'kaj': 3, 'kcg': 3, 'kde': 0, 'kea': 0, 'kk': 3, 'kl': 3, 'km': 0, 'kn': 0, 'ko': 0, 'ksb': 3, 'ksh': 21, 'ku': 3, 'kw': 7, 'lag': 18, 'lb': 3, 'lg': 3, 'ln': 4, 'lo': 0, 'lt': 10, 'lv': 6, 'mas': 3, 'mg': 4, 'mk': 16, 'ml': 3, 'mn': 3, 'mo': 9, 'mr': 3, 'ms': 0, 'mt': 15, 'my': 0, 'nah': 3, 'naq': 7, 'nb': 3, 'nd': 3, 'ne': 3, 'nl': 3, 'nn': 3, 'no': 3, 'nr': 3, 'nso': 4, 'ny': 3, 'nyn': 3, 'om': 3, 'or': 3, 'pa': 3, 'pap': 3, 'pl': 13, 'ps': 3, 'pt': 3, 'rm': 3, 'ro': 9, 'rof': 3, 'ru': 11, 'rwk': 3, 'sah': 0, 'saq': 3, 'se': 7, 'seh': 3, 'ses': 0, 'sg': 0, 'sh': 11, 'shi': 19, 'sk': 12, 'sl': 14, 'sma': 7, 'smi': 7, 'smj': 7, 'smn': 7, 'sms': 7, 'sn': 3, 'so': 3, 'sq': 3, 'sr': 11, 'ss': 3, 'ssy': 3, 'st': 3, 'sv': 3, 'sw': 3, 'syr': 3, 'ta': 3, 'te': 3, 'teo': 3, 'th': 0, 'ti': 4, 'tig': 3, 'tk': 3, 'tl': 4, 'tn': 3, 'to': 0, 'tr': 0, 'ts': 3, 'tzm': 22, 'uk': 11, 'ur': 3, 've': 3, 'vi': 0, 'vun': 3, 'wa': 4, 'wae': 3, 'wo': 0, 'xh': 3, 'xog': 3, 'yo': 0, 'zh': 0, 'zu': 3 }; // utility functions for plural rules methods function isIn(n, list) { return list.indexOf(n) !== -1; } function isBetween(n, start, end) { return start <= n && n <= end; } // list of all plural rules methods: // map an integer to the plural form name to use var pluralRules = { '0': function(n) { return 'other'; }, '1': function(n) { if ((isBetween((n % 100), 3, 10))) return 'few'; if (n === 0) return 'zero'; if ((isBetween((n % 100), 11, 99))) return 'many'; if (n == 2) return 'two'; if (n == 1) return 'one'; return 'other'; }, '2': function(n) { if (n !== 0 && (n % 10) === 0) return 'many'; if (n == 2) return 'two'; if (n == 1) return 'one'; return 'other'; }, '3': function(n) { if (n == 1) return 'one'; return 'other'; }, '4': function(n) { if ((isBetween(n, 0, 1))) return 'one'; return 'other'; }, '5': function(n) { if ((isBetween(n, 0, 2)) && n != 2) return 'one'; return 'other'; }, '6': function(n) { if (n === 0) return 'zero'; if ((n % 10) == 1 && (n % 100) != 11) return 'one'; return 'other'; }, '7': function(n) { if (n == 2) return 'two'; if (n == 1) return 'one'; return 'other'; }, '8': function(n) { if ((isBetween(n, 3, 6))) return 'few'; if ((isBetween(n, 7, 10))) return 'many'; if (n == 2) return 'two'; if (n == 1) return 'one'; return 'other'; }, '9': function(n) { if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19))) return 'few'; if (n == 1) return 'one'; return 'other'; }, '10': function(n) { if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) return 'few'; if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19))) return 'one'; return 'other'; }, '11': function(n) { if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) return 'few'; if ((n % 10) === 0 || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 11, 14))) return 'many'; if ((n % 10) == 1 && (n % 100) != 11) return 'one'; return 'other'; }, '12': function(n) { if ((isBetween(n, 2, 4))) return 'few'; if (n == 1) return 'one'; return 'other'; }, '13': function(n) { if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) return 'few'; if (n != 1 && (isBetween((n % 10), 0, 1)) || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 12, 14))) return 'many'; if (n == 1) return 'one'; return 'other'; }, '14': function(n) { if ((isBetween((n % 100), 3, 4))) return 'few'; if ((n % 100) == 2) return 'two'; if ((n % 100) == 1) return 'one'; return 'other'; }, '15': function(n) { if (n === 0 || (isBetween((n % 100), 2, 10))) return 'few'; if ((isBetween((n % 100), 11, 19))) return 'many'; if (n == 1) return 'one'; return 'other'; }, '16': function(n) { if ((n % 10) == 1 && n != 11) return 'one'; return 'other'; }, '17': function(n) { if (n == 3) return 'few'; if (n === 0) return 'zero'; if (n == 6) return 'many'; if (n == 2) return 'two'; if (n == 1) return 'one'; return 'other'; }, '18': function(n) { if (n === 0) return 'zero'; if ((isBetween(n, 0, 2)) && n !== 0 && n != 2) return 'one'; return 'other'; }, '19': function(n) { if ((isBetween(n, 2, 10))) return 'few'; if ((isBetween(n, 0, 1))) return 'one'; return 'other'; }, '20': function(n) { if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !( isBetween((n % 100), 10, 19) || isBetween((n % 100), 70, 79) || isBetween((n % 100), 90, 99) )) return 'few'; if ((n % 1000000) === 0 && n !== 0) return 'many'; if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92])) return 'two'; if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91])) return 'one'; return 'other'; }, '21': function(n) { if (n === 0) return 'zero'; if (n == 1) return 'one'; return 'other'; }, '22': function(n) { if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) return 'one'; return 'other'; }, '23': function(n) { if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) return 'one'; return 'other'; }, '24': function(n) { if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) return 'few'; if (isIn(n, [2, 12])) return 'two'; if (isIn(n, [1, 11])) return 'one'; return 'other'; } }; // return a function that gives the plural form name for a given integer var index = locales2rules[lang.replace(/-.*$/, '')]; if (!(index in pluralRules)) { console.warn('plural form unknown for [' + lang + ']'); return function() { return 'other'; }; } return pluralRules[index]; } /** * pre-defined 'plural' macro */ html10n.macros.plural = function(key, param, opts) { var str , n = parseFloat(param); if (isNaN(n)) return; // initialize _pluralRules if (!this._pluralRules) this._pluralRules = getPluralRules(html10n.language); var index = this._pluralRules(n); // try to find a [zero|one|two] key if it's defined if (n === 0 && ('zero') in opts) { str = opts['zero']; } else if (n == 1 && ('one') in opts) { str = opts['one']; } else if (n == 2 && ('two') in opts) { str = opts['two']; } else if (index in opts) { str = opts[index]; } return str; }; /** * Localize a document * @param langs An array of lang codes defining fallbacks */ html10n.localize = function(langs) { var that = this // if only one string => create an array if ('string' == typeof langs) langs = [langs] // Expand two-part locale specs var i=0 langs.forEach(function(lang) { if(!lang) return langs[i++] = lang if(~lang.indexOf('-')) langs[i++] = lang.substr(0, lang.indexOf('-')) }) this.build(langs, function(er, translations) { html10n.translations = translations html10n.translateElement(translations) that.trigger('localized') }) } /** * Triggers the translation process * for an element * @param translations A hash of all translation strings * @param element A DOM element, if omitted, the document element will be used */ html10n.translateElement = function(translations, element) { element = element || document.documentElement var children = element? getTranslatableChildren(element) : document.childNodes; for (var i=0, n=children.length; i < n; i++) { this.translateNode(translations, children[i]) } // translate element itself if necessary this.translateNode(translations, element) } function asyncForEach(list, iterator, cb) { var i = 0 , n = list.length iterator(list[i], i, function each(err) { if(err) console.error(err) i++ if (i < n) return iterator(list[i],i, each); cb() }) } function getTranslatableChildren(element) { if(!document.querySelectorAll) { if (!element) return [] var nodes = element.getElementsByTagName('*') , l10nElements = [] for (var i=0, n=nodes.length; i < n; i++) { if (nodes[i].getAttribute('data-l10n-id')) l10nElements.push(nodes[i]); } return l10nElements } return element.querySelectorAll('*[data-l10n-id]') } html10n.get = function(id, args) { var translations = html10n.translations if(!translations) return console.warn('No translations available (yet)') if(!translations[id]) return console.warn('Could not find string '+id) // apply macros var str = translations[id] str = substMacros(id, str, args) // apply args str = substArguments(str, args) return str } // replace {{arguments}} with their values or the // associated translation string (based on its key) function substArguments(str, args) { var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/ , match while (match = reArgs.exec(str)) { if (!match || match.length < 2) return str // argument key not found var arg = match[1] , sub = '' if (arg in args) { sub = args[arg] } else if (arg in translations) { sub = translations[arg] } else { console.warn('Could not find argument {{' + arg + '}}') return str } str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length) } return str } // replace {[macros]} with their values function substMacros(key, str, args) { var regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}') , match while(match = regex.exec(str)) { // a macro has been found // Note: at the moment, only one parameter is supported var macroName = match[1] , paramName = match[2] , optv = match[3] , opts = {} if (!(macroName in html10n.macros)) continue if(optv) { optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g).forEach(function(arg) { var parts = arg.split(':') , name = parts[0] , value = parts[1].trim() opts[name] = value }) } var param if (args && paramName in args) { param = args[paramName] } else if (paramName in html10n.translations) { param = translations[paramName] } // there's no macro parser: it has to be defined in html10n.macros var macro = html10n.macros[macroName] str = str.substr(0, match.index) + macro(key, param, opts) + str.substr(match.index+match[0].length) } return str } /** * Applies translations to a DOM node (recursive) */ html10n.translateNode = function(translations, node) { var str = {} // get id str.id = node.getAttribute('data-l10n-id') if (!str.id) return if(!translations[str.id]) return console.warn('Couldn\'t find translation key '+str.id) // get args if(window.JSON) { str.args = JSON.parse(node.getAttribute('data-l10n-args')) }else{ try{ str.args = eval(node.getAttribute('data-l10n-args')) }catch(e) { console.warn('Couldn\'t parse args for '+str.id) } } str.str = html10n.get(str.id, str.args) // get attribute name to apply str to var prop , index = str.id.lastIndexOf('.') , attrList = // allowed attributes { "title": 1 , "innerHTML": 1 , "alt": 1 , "textContent": 1 } if (index > 0 && str.id.substr(index + 1) in attrList) { // an attribute has been specified prop = str.id.substr(index + 1) } else { // no attribute: assuming text content by default prop = document.body.textContent ? 'textContent' : 'innerText' } // Apply translation if (node.children.length === 0 || prop != 'textContent') { node[prop] = str.str } else { var children = node.childNodes, found = false for (var i=0, n=children.length; i < n; i++) { if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) { if (!found) { children[i].nodeValue = str.str found = true } else { children[i].nodeValue = '' } } } if (!found) { console.warn('Unexpected error: could not translate element content for key '+str.id, node) } } } /** * Builds a translation object from a list of langs (loads the necessary translations) * @param langs Array - a list of langs sorted by priority (default langs should go last) */ html10n.build = function(langs, cb) { var that = this , build = {} asyncForEach(langs, function (lang, i, next) { if(!lang) return next(); that.loader.load(lang, next) }, function() { var lang langs.reverse() // loop through the priority array... for (var i=0, n=langs.length; i < n; i++) { lang = langs[i] if(!lang) continue; if(!(lang in that.loader.langs)) {// uh, we don't have this lang availbable.. // then check for related langs if(~lang.indexOf('-')) lang = lang.split('-')[0]; for(var l in that.loader.langs) { if(lang != l && l.indexOf(lang) === 0) { lang = l break; } } if(lang != l) continue; } // ... and apply all strings of the current lang in the list // to our build object for (var string in that.loader.langs[lang]) { build[string] = that.loader.langs[lang][string] } // the last applied lang will be exposed as the // lang the page was translated to that.language = lang } cb(null, build) }) } /** * Returns the language that was last applied to the translations hash * thus overriding most of the formerly applied langs */ html10n.getLanguage = function() { return this.language; } /** * Returns the direction of the language returned be html10n#getLanguage */ html10n.getDirection = function() { if(!this.language) return var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-')) return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl' } /** * Index all s */ html10n.index = function () { // Find all s var links = document.getElementsByTagName('link') , resources = [] for (var i=0, n=links.length; i < n; i++) { if (links[i].type != 'application/l10n+json') continue; resources.push(links[i].href) } this.loader = new Loader(resources) this.trigger('indexed') } if (document.addEventListener) // modern browsers and IE9+ document.addEventListener('DOMContentLoaded', function() { html10n.index() }, false) else if (window.attachEvent) window.attachEvent('onload', function() { html10n.index() }, false) // gettext-like shortcut if (window._ === undefined) window._ = html10n.get; return html10n })(window, document)