diff --git a/index.html b/index.html index 8c95106..7a95ac8 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ Invisible Internet Protocol Daemon + @@ -40,7 +41,7 @@

I2P allows people from all around the world to communicate and share information without restrictions.

If you are interested in deeper technical details, visit - this page.

@@ -54,18 +55,19 @@

BTC: 3MDoGJW9TLMTCDGrR9bLgWXfm6sjmgy86f

LTC: LKQirrYrDeTuAPnpYq5y7LVKtywfkkHi59

ETH: 0x9e5bac70d20d1079ceaa111127f4fb3bccce379d

-

GST: GbD2JSQHBHCKLa9WTHmigJRpyFgmBj4woG

+

GST: GbD2JSQHBHCKLa9WTHmigJRpyFgmBj4woG

DASH: Xw8YUrQpYzP9tZBmbjqxS3M97Q7v3vJKUF

ZEC: t1cTckLuXsr1dwVrK4NDzfhehss4NvMadAJ

-

ANC: AQJYweYYUqM1nVfLqfoSMpUMfzxvS4Xd7z

+

ANC: AQJYweYYUqM1nVfLqfoSMpUMfzxvS4Xd7z

+

XMR: 497pJc7X4xqKvcLBLpSUtRgWqMMyo24u4btCos3cak6gbMkpobgSU6492ztUcUBghyeHpYeczB55s38NpuHoH5WGNSPDRMH

diff --git a/js/app.js b/js/app.js index 249e91a..32151f6 100644 --- a/js/app.js +++ b/js/app.js @@ -1,6 +1,3 @@ -/* https://github.com/fabi1cazenave/webL10n */ -"use strict";document.webL10n=(function(window,document,undefined){var gL10nData={};var gTextData="";var gTextProp="textContent";var gLanguage="";var gMacros={};var gReadyState="loading";var gAsyncResourceLoading=true;var gDEBUG=1;function consoleLog(message){if(gDEBUG>=2){console.log("[l10n] "+message)}}function consoleWarn(message){if(gDEBUG){console.warn("[l10n] "+message)}}function getL10nResourceLinks(){return document.querySelectorAll('link[type="application/l10n"]')}function getL10nDictionary(){var script=document.querySelector('script[type="application/l10n"]');return script?JSON.parse(script.innerHTML):null}function getTranslatableChildren(element){return element?element.querySelectorAll("*[data-l10n-id]"):[]}function getL10nAttributes(element){if(!element){return{}}var l10nId=element.getAttribute("data-l10n-id");var l10nArgs=element.getAttribute("data-l10n-args");var args={};if(l10nArgs){try{args=JSON.parse(l10nArgs)}catch(e){consoleWarn("could not parse arguments for #"+l10nId)}}return{id:l10nId,args:args}}function fireL10nReadyEvent(lang){var evtObject=document.createEvent("Event");evtObject.initEvent("localized",true,false);evtObject.language=lang;document.dispatchEvent(evtObject)}function xhrLoadText(url,onSuccess,onFailure){onSuccess=onSuccess||function _onSuccess(data){};onFailure=onFailure||function _onFailure(){consoleWarn(url+" not found.")};var xhr=new XMLHttpRequest();xhr.open("GET",url,gAsyncResourceLoading);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;try{xhr.send(null)}catch(e){onFailure()}}function parseResource(href,lang,successCallback,failureCallback){var baseURL=href.replace(/[^\/]*$/,"")||"./";function evalString(text){if(text.lastIndexOf("\\")<0){return 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,parsedPropertiesCallback){var dictionary={};var reBlank=/^\s*|\s*$/;var reComment=/^\s*#|^\s*$/;var reSection=/^\s*\[(.*)\]\s*$/;var reImport=/^\s*@import\s+url\((.*)\)\s*$/i;var reSplit=/^([^=\s]*)\s*=\s*(.+)$/;function parseRawLines(rawText,extendedSyntax,parsedRawLinesCallback){var entries=rawText.replace(reBlank,"").split(/[\r\n]+/);var currentLang="*";var genericLang=lang.split("-",1)[0];var skipLang=false;var match="";function nextEntry(){while(true){if(!entries.length){parsedRawLinesCallback();return}var line=entries.shift();if(reComment.test(line)){continue}if(extendedSyntax){match=reSection.exec(line);if(match){currentLang=match[1].toLowerCase();skipLang=(currentLang!=="*")&&(currentLang!==lang)&&(currentLang!==genericLang);continue}else{if(skipLang){continue}}match=reImport.exec(line);if(match){loadImport(baseURL+match[1],nextEntry);return}}var tmp=line.match(reSplit);if(tmp&&tmp.length==3){dictionary[tmp[1]]=evalString(tmp[2])}}}nextEntry()}function loadImport(url,callback){xhrLoadText(url,function(content){parseRawLines(content,false,callback)},null)}parseRawLines(text,true,function(){parsedPropertiesCallback(dictionary)})}xhrLoadText(href,function(response){gTextData+=response;parseProperties(response,function(data){for(var key in data){var id,prop,index=key.lastIndexOf(".");if(index>0){id=key.substring(0,index);prop=key.substr(index+1)}else{id=key;prop=gTextProp}if(!gL10nData[id]){gL10nData[id]={}}gL10nData[id][prop]=data[key]}if(successCallback){successCallback()}})},failureCallback)}function loadLocale(lang,callback){if(lang){lang=lang.toLowerCase()}callback=callback||function _callback(){};clear();gLanguage=lang;var langLinks=getL10nResourceLinks();var langCount=langLinks.length;if(langCount===0){var dict=getL10nDictionary();if(dict&&dict.locales&&dict.default_locale){consoleLog("using the embedded JSON directory, early way out");gL10nData=dict.locales[lang];if(!gL10nData){var defaultLocale=dict.default_locale.toLowerCase();for(var anyCaseLang in dict.locales){anyCaseLang=anyCaseLang.toLowerCase();if(anyCaseLang===lang){gL10nData=dict.locales[lang];break}else{if(anyCaseLang===defaultLocale){gL10nData=dict.locales[defaultLocale]}}}}callback()}else{consoleLog("no resource to load, early way out")}fireL10nReadyEvent(lang);gReadyState="complete";return}var onResourceLoaded=null;var gResourceCount=0;onResourceLoaded=function(){gResourceCount++;if(gResourceCount>=langCount){callback();fireL10nReadyEvent(lang);gReadyState="complete"}};function L10nResourceLink(link){var href=link.href;this.load=function(lang,callback){parseResource(href,lang,callback,function(){consoleWarn(href+" not found.");consoleWarn('"'+lang+'" resource not found');gLanguage="";callback()})}}for(var i=0;i0){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+"}}"},getData:function(){return gL10nData},getText:function(){return gTextData},getLanguage:function(){return gLanguage},setLanguage:function(lang,callback){loadLocale(lang,function(){if(callback){callback()}translateFragment()})},getDirection:function(){var rtlList=["ar","he","fa","ps","ur"];var shortCode=gLanguage.split("-",1)[0];return(rtlList.indexOf(shortCode)>=0)?"rtl":"ltr"},translate:translateFragment,getReadyState:function(){return gReadyState},ready:function(callback){if(!callback){return}else{if(gReadyState=="complete"||gReadyState=="interactive"){window.setTimeout(function(){callback()})}else{if(document.addEventListener){document.addEventListener("localized",function once(){document.removeEventListener("localized",once);callback()})}else{if(document.attachEvent){document.documentElement.attachEvent("onpropertychange",function once(e){if(e.propertyName==="localized"){document.documentElement.detachEvent("onpropertychange",once);callback()}})}}}}}}})(window,document);if(window._===undefined){var _=document.webL10n.get}; - var url = "https://api.github.com/repos/PurpleI2P/i2pdbrowser/releases/latest"; //var url = "http://127.0.0.1:9889/latest.json"; @@ -20,7 +17,7 @@ function renderReleaseInfo(data) { }); if (win_asset.length > 0) { - dl_btn.innerText = document.webL10n.get("header-dl-btn-windows"); + dl_btn.innerText = document.webL10n.get("header-dl-btn-windows", null, "Download for Windows"); dl_btn.setAttribute("href", win_asset[0].browser_download_url); all_dls.style.display = "block"; } @@ -37,7 +34,7 @@ function renderReleaseInfo(data) { } */ } else { - dl_btn.innerText = document.webL10n.get("header-dl-btn"); + dl_btn.innerText = document.webL10n.get("header-dl-btn", null, "Downloads"); } } diff --git a/js/l10n.js b/js/l10n.js new file mode 100644 index 0000000..e911b08 --- /dev/null +++ b/js/l10n.js @@ -0,0 +1,1210 @@ +/** + * 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'; + +document.webL10n = (function(window, document, undefined) { + var gL10nData = {}; + var gTextData = ''; + var gTextProp = 'textContent'; + var gLanguage = ''; + var gMacros = {}; + var gReadyState = 'loading'; + + + /** + * 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) { + if (gDEBUG) { + console.warn('[l10n] ' + message); + } + } + + + /** + * DOM helpers for the so-called "HTML API". + * + * These functions are written for modern browsers. For old versions of IE, + * they're overridden in the 'startup' section at the end of this file. + */ + + function getL10nResourceLinks() { + 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]') : []; + } + + function getL10nAttributes(element) { + if (!element) + return {}; + + var l10nId = element.getAttribute('data-l10n-id'); + var l10nArgs = element.getAttribute('data-l10n-args'); + var args = {}; + if (l10nArgs) { + try { + args = JSON.parse(l10nArgs); + } catch (e) { + consoleWarn('could not parse arguments for #' + l10nId); + } + } + return { id: l10nId, args: args }; + } + + function fireL10nReadyEvent(lang) { + var evtObject = document.createEvent('Event'); + evtObject.initEvent('localized', true, false); + evtObject.language = lang; + document.dispatchEvent(evtObject); + } + + function xhrLoadText(url, onSuccess, onFailure) { + onSuccess = onSuccess || function _onSuccess(data) {}; + onFailure = onFailure || function _onFailure() { + consoleWarn(url + ' not found.'); + }; + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, gAsyncResourceLoading); + 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(); + } + } + + + /** + * l10n resource parser: + * - reads (async XHR) the l10n resource matching `lang'; + * - imports linked resources (synchronously) when specified; + * - parses the text data (fills `gL10nData' and `gTextData'); + * - triggers success/failure callbacks when done. + * + * @param {string} href + * URL of the l10n resource to parse. + * + * @param {string} lang + * locale (language) to parse. Must be a lowercase string. + * + * @param {Function} successCallback + * triggered when the l10n resource has been successully parsed. + * + * @param {Function} failureCallback + * triggered when the an error has occured. + * + * @return {void} + * uses the following global variables: gL10nData, gTextData, gTextProp. + */ + + function parseResource(href, lang, successCallback, failureCallback) { + var baseURL = href.replace(/[^\/]*$/, '') || './'; + + // handle escaped characters (backslashes) in a string + function evalString(text) { + if (text.lastIndexOf('\\') < 0) + return 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, "'"); + } + + // parse *.properties text data into an l10n dictionary + // If gAsyncResourceLoading is false, then the callback will be called + // synchronously. Otherwise it is called asynchronously. + function parseProperties(text, parsedPropertiesCallback) { + var dictionary = {}; + + // token expressions + var reBlank = /^\s*|\s*$/; + var reComment = /^\s*#|^\s*$/; + var reSection = /^\s*\[(.*)\]\s*$/; + var reImport = /^\s*@import\s+url\((.*)\)\s*$/i; + var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\' + + // parse the *.properties file into an associative array + function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) { + var entries = rawText.replace(reBlank, '').split(/[\r\n]+/); + var currentLang = '*'; + var genericLang = lang.split('-', 1)[0]; + var skipLang = false; + var match = ''; + + function nextEntry() { + // Use infinite loop instead of recursion to avoid reaching the + // maximum recursion limit for content with many lines. + while (true) { + if (!entries.length) { + parsedRawLinesCallback(); + return; + } + var line = entries.shift(); + + // comment or blank line? + if (reComment.test(line)) + continue; + + // the extended syntax supports [lang] sections and @import rules + if (extendedSyntax) { + match = reSection.exec(line); + if (match) { // section start? + // RFC 4646, section 4.4, "All comparisons MUST be performed + // in a case-insensitive manner." + + currentLang = match[1].toLowerCase(); + skipLang = (currentLang !== '*') && + (currentLang !== lang) && (currentLang !== genericLang); + continue; + } else if (skipLang) { + continue; + } + match = reImport.exec(line); + if (match) { // @import rule? + loadImport(baseURL + match[1], nextEntry); + return; + } + } + + // key-value pair + var tmp = line.match(reSplit); + if (tmp && tmp.length == 3) { + dictionary[tmp[1]] = evalString(tmp[2]); + } + } + } + nextEntry(); + } + + // import another *.properties file + function loadImport(url, callback) { + xhrLoadText(url, function(content) { + parseRawLines(content, false, callback); // don't allow recursive imports + }, null); + } + + // fill the dictionary + parseRawLines(text, true, function() { + parsedPropertiesCallback(dictionary); + }); + } + + // load and parse l10n data (warning: global variables are used here) + xhrLoadText(href, function(response) { + gTextData += response; // mostly for debug + + // parse *.properties text data into an l10n dictionary + parseProperties(response, function(data) { + + // find attribute descriptions, if any + for (var key in data) { + var id, prop, index = key.lastIndexOf('.'); + if (index > 0) { // an attribute has been specified + id = key.substring(0, index); + prop = key.substr(index + 1); + } else { // no attribute: assuming text content by default + id = key; + prop = gTextProp; + } + if (!gL10nData[id]) { + gL10nData[id] = {}; + } + gL10nData[id][prop] = data[key]; + } + + // trigger callback + if (successCallback) { + successCallback(); + } + }); + }, failureCallback); + } + + // load and parse all resources for the specified locale + function loadLocale(lang, callback) { + // RFC 4646, section 2.1 states that language tags have to be treated as + // case-insensitive. Convert to lowercase for case-insensitive comparisons. + if (lang) { + lang = lang.toLowerCase(); + } + + callback = callback || function _callback() {}; + + clear(); + gLanguage = lang; + + // check all nodes + // and load the resource files + var langLinks = getL10nResourceLinks(); + var langCount = langLinks.length; + 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]; + if (!gL10nData) { + var defaultLocale = dict.default_locale.toLowerCase(); + for (var anyCaseLang in dict.locales) { + anyCaseLang = anyCaseLang.toLowerCase(); + if (anyCaseLang === lang) { + gL10nData = dict.locales[lang]; + break; + } else if (anyCaseLang === defaultLocale) { + gL10nData = dict.locales[defaultLocale]; + } + } + } + callback(); + } else { + consoleLog('no resource to load, early way out'); + } + // early way out + fireL10nReadyEvent(lang); + gReadyState = 'complete'; + return; + } + + // start the callback when all resources are loaded + var onResourceLoaded = null; + var gResourceCount = 0; + onResourceLoaded = function() { + gResourceCount++; + if (gResourceCount >= langCount) { + callback(); + fireL10nReadyEvent(lang); + gReadyState = 'complete'; + } + }; + + // load all resource files + function L10nResourceLink(link) { + var href = link.href; + // Note: If |gAsyncResourceLoading| is false, then the following callbacks + // are synchronously called. + this.load = function(lang, callback) { + parseResource(href, lang, callback, function() { + consoleWarn(href + ' not found.'); + // lang not found, used default resource instead + consoleWarn('"' + lang + '" resource not found'); + gLanguage = ''; + // Resource not loaded, but we still need to call the callback. + callback(); + }); + }; + } + + for (var i = 0; i < langCount; i++) { + var resource = new L10nResourceLink(langLinks[i]); + resource.load(lang, onResourceLoaded); + } + } + + // clear all l10n data + function clear() { + gL10nData = {}; + gTextData = ''; + gLanguage = ''; + // TODO: clear all non predefined macros. + // There's no such macro /yet/ but we're planning to have some... + } + + + /** + * 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)) { + consoleWarn('plural form unknown for [' + lang + ']'); + return function() { return 'other'; }; + } + return pluralRules[index]; + } + + // pre-defined 'plural' macro + gMacros.plural = function(str, param, key, prop) { + var n = parseFloat(param); + if (isNaN(n)) + return str; + + // TODO: support other properties (l20n still doesn't...) + if (prop != gTextProp) + return str; + + // initialize _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 + if (n === 0 && (key + '[zero]') in gL10nData) { + str = gL10nData[key + '[zero]'][prop]; + } else if (n == 1 && (key + '[one]') in gL10nData) { + str = gL10nData[key + '[one]'][prop]; + } else if (n == 2 && (key + '[two]') in gL10nData) { + 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; + }; + + + /** + * l10n dictionary functions + */ + + // fetch an l10n object, warn if not found, apply 'args' if possible + function getL10nData(key, args, fallback) { + var data = gL10nData[key]; + if (!data) { + consoleWarn('#' + key + ' is undefined.'); + if (!fallback) { + return null; + } + data = fallback; + } + + /** This is where l10n expressions should be processed. + * The plan is to support C-style expressions from the l20n project; + * until then, only two kinds of simple expressions are supported: + * {[ index ]} and {{ arguments }}. + */ + var rv = {}; + for (var prop in data) { + var str = data[prop]; + str = substIndexes(str, args, key, prop); + str = substArguments(str, args, key); + rv[prop] = str; + } + return rv; + } + + // replace {[macros]} with their values + function substIndexes(str, args, key, prop) { + var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/; + var reMatch = reIndex.exec(str); + if (!reMatch || !reMatch.length) + return str; + + // an index/macro has been found + // Note: at the moment, only one parameter is supported + var macroName = reMatch[1]; + var paramName = reMatch[2]; + var param; + if (args && paramName in args) { + param = args[paramName]; + } else if (paramName in gL10nData) { + param = gL10nData[paramName]; + } + + // there's no macro parser yet: it has to be defined in gMacros + if (macroName in gMacros) { + var macro = gMacros[macroName]; + str = macro(str, param, key, prop); + } + return str; + } + + // replace {{arguments}} with their values + function substArguments(str, args, key) { + var reArgs = /\{\{\s*(.+?)\s*\}\}/g; + return str.replace(reArgs, function(matched_text, arg) { + if (args && arg in args) { + return args[arg]; + } + if (arg in gL10nData) { + return gL10nData[arg]; + } + consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.'); + return matched_text; + }); + } + + // translate an HTML element + function translateElement(element) { + var l10n = getL10nAttributes(element); + if (!l10n.id) + return; + + // get the related l10n object + var data = getL10nData(l10n.id, l10n.args); + if (!data) { + consoleWarn('#' + l10n.id + ' is undefined.'); + return; + } + + // translate element (TODO: security checks?) + if (data[gTextProp]) { // XXX + if (getChildElementCount(element) === 0) { + element[gTextProp] = data[gTextProp]; + } else { + // 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].nodeValue)) { + if (found) { + children[i].nodeValue = ''; + } else { + children[i].nodeValue = data[gTextProp]; + found = true; + } + } + } + // if no (non-empty) textNode is found, insert a textNode before the + // first element child. + if (!found) { + var textNode = document.createTextNode(data[gTextProp]); + element.insertBefore(textNode, element.firstChild); + } + } + delete data[gTextProp]; + } + + for (var k in data) { + element[k] = data[k]; + } + } + + // 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; + + // check all translatable children (= w/ a `data-l10n-id' attribute) + var children = getTranslatableChildren(element); + var elementCount = children.length; + for (var i = 0; i < elementCount; i++) { + translateElement(children[i]); + } + + // translate element itself if necessary + if (element.nodeType === 1) { + 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