/** * 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. */ /* additional modifications for pdf.js project: - disables language initialization on page loading; - removes consolewarn and consolelog and use console.log/warn directly. - removes window._ assignment. - remove compatibility code for oldie. */ /*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 /** * 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) { console.warn('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() { console.warn(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) { console.log('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 { console.log('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() { console.warn(href + ' not found.'); // lang not found, used default resource instead console.warn('"' + 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)) { console.warn('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) { console.warn('#' + 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]; } console.log('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) { console.warn('#' + 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 translateelement(element); } return { // get a localized string 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 + '}}'; }, // debug getdata: function() { return gl10ndata; }, gettext: function() { return gtextdata; }, // get|set the document language getlanguage: function() { return glanguage; }, setlanguage: function(lang, callback) { loadlocale(lang, function() { if (callback) callback(); translatefragment(); }); }, // get the direction (ltr|rtl) of the current language getdirection: function() { // http://www.w3.org/international/questions/qa-scripts // arabic, hebrew, farsi, pashto, urdu var rtllist = ['ar', 'he', 'fa', 'ps', 'ur']; var shortcode = glanguage.split('-', 1)[0]; return (rtllist.indexof(shortcode) >= 0) ? 'rtl' : 'ltr'; }, // translate an element or document fragment translate: translatefragment, // this can be used to prevent race conditions 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(); }); } } }; }) (window, document);