mirror of
				https://github.com/ether/etherpad-lite.git
				synced 2025-10-31 16:21:11 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1028 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1028 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /** 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.
 | |
|   */
 | |
| /*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';
 | |
| 
 | |
|   // read-only setting -- we recommend to load l10n resources synchronously
 | |
|   var gAsyncResourceLoading = true;
 | |
| 
 | |
|   // debug helpers
 | |
|   var gDEBUG = false;
 | |
|   function consoleLog(message) {
 | |
|     if (gDEBUG)
 | |
|       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 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', false, false);
 | |
|     evtObject.language = lang;
 | |
|     window.dispatchEvent(evtObject);
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    *
 | |
|    * @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
 | |
|     function parseProperties(text) {
 | |
|       var dictionary = {};
 | |
| 
 | |
|       // token expressions
 | |
|       var reBlank = /^\s*|\s*$/;
 | |
|       var reComment = /^\s*;|^\s*$/;// Use ; for comments!
 | |
|       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) {
 | |
|         var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
 | |
|         var currentLang = '*';
 | |
|         var genericLang = lang.replace(/-[a-z]+$/i, '');
 | |
|         var skipLang = false;
 | |
|         var match = '';
 | |
| 
 | |
|         for (var i = 0; i < entries.length; i++) {
 | |
|           var line = entries[i];
 | |
| 
 | |
|           // comment or blank line?
 | |
|           if (reComment.test(line))
 | |
|             continue;
 | |
| 
 | |
|           // the extended syntax supports [lang] sections and @import rules
 | |
|           if (extendedSyntax) {
 | |
|             if (reSection.test(line)) { // section start?
 | |
|               match = reSection.exec(line);
 | |
|               currentLang = match[1];
 | |
|               skipLang = (currentLang !== '*') &&
 | |
|                   (currentLang !== lang) && (currentLang !== genericLang);
 | |
|               continue;
 | |
|             } else if (skipLang) {
 | |
|               continue;
 | |
|             }
 | |
|             if (reImport.test(line)) { // @import rule?
 | |
|               match = reImport.exec(line);
 | |
|               loadImport(baseURL + match[1]); // load the resource synchronously
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           // key-value pair
 | |
|           consoleLog(tmp)
 | |
|           var tmp = line.match(reSplit);
 | |
|           if (tmp && tmp.length == 3)
 | |
|             dictionary[tmp[1]] = evalString(tmp[2]);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // import another *.properties file
 | |
|       function loadImport(url) {
 | |
|         loadResource(url, function(content) {
 | |
|           parseRawLines(content, false); // don't allow recursive imports
 | |
|         }, false, false); // load synchronously
 | |
|       }
 | |
| 
 | |
|       // fill the dictionary
 | |
|       parseRawLines(text, true);
 | |
|       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) {
 | |
|       gTextData += response; // mostly for debug
 | |
| 
 | |
|       // parse *.properties text data into an l10n dictionary
 | |
|       var data = parseProperties(response);
 | |
|       
 | |
|       // allowed attributes
 | |
|       var attrList =
 | |
|       { "title": 1
 | |
|       , "innerHTML": 1
 | |
|       , "alt": 1
 | |
|       , "textContent": 1
 | |
|       }
 | |
| 
 | |
|       // find attribute descriptions, if any
 | |
|       for (var key in data) {
 | |
|         var id, prop, index = key.lastIndexOf('.');
 | |
|         if (index > 0 && key.substr(index + 1) in attrList) { // 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, gAsyncResourceLoading);
 | |
|   };
 | |
| 
 | |
|   // load and parse all resources for the specified locale
 | |
|   function loadLocale(lang, callback) {
 | |
|     clear();
 | |
|     gLanguage = lang;
 | |
| 
 | |
|     // check all <link type="application/l10n" href="..." /> nodes
 | |
|     // and load the resource files
 | |
|     var langLinks = getL10nResourceLinks();
 | |
|     var langCount = langLinks.length;
 | |
|     if (langCount == 0) {
 | |
|       consoleLog('no resource to load, 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) {
 | |
|         if (callback) // execute the [optional] callback
 | |
|           callback();
 | |
|         fireL10nReadyEvent(lang);
 | |
|         gReadyState = 'complete';
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // load all resource files
 | |
|     function l10nResourceLink(link) {
 | |
|       var href = link.href;
 | |
|       var type = link.type;
 | |
|       this.load = function(lang, callback) {
 | |
|         var applied = lang;
 | |
|         parseResource(href, lang, callback, function() {
 | |
|           consoleWarn(href + ' not found.');
 | |
|           applied = '';
 | |
|         });
 | |
|         return applied; // return lang if found, an empty string if not found
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     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
 | |
|         consoleWarn('"' + lang + '" resource not found');
 | |
|         gLanguage = '';
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // 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];
 | |
|     }
 | |
| 
 | |
|     return str;
 | |
|   };
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * l10n dictionary functions
 | |
|    */
 | |
| 
 | |
|   // fetch an l10n object, warn if not found, apply `args' if possible
 | |
|   function getL10nData(key, args) {
 | |
|     var data = gL10nData[key];
 | |
|     if (!data) {
 | |
|       consoleWarn('#' + key + ' missing for [' + gLanguage + ']');
 | |
|     }
 | |
| 
 | |
|     /** 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);
 | |
|       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) {
 | |
|     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][gTextProp];
 | |
|       } else {
 | |
|         consoleWarn('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 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 + ' missing for [' + gLanguage + ']');
 | |
|       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) {
 | |
|         element[gTextProp] = data[gTextProp];
 | |
|       } else {
 | |
|         var children = element.childNodes,
 | |
|             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 (found) {
 | |
|               children[i].nodeValue = '';
 | |
|             } else {
 | |
|               children[i].nodeValue = data[gTextProp];
 | |
|               found = true;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         if (!found) {
 | |
|           consoleWarn('unexpected error, could not translate element content');
 | |
|         }
 | |
|       }
 | |
|       delete data[gTextProp];
 | |
|     }
 | |
| 
 | |
|     for (var k in data) {
 | |
|       element[k] = data[k];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // 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);
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
| 
 | |
|   // browser-specific startup
 | |
|   if (document.addEventListener) { // modern browsers and IE9+
 | |
|     document.addEventListener('DOMContentLoaded', function() {
 | |
|       var lang = document.documentElement.lang || navigator.language || navigator.userLanguage || 'en';
 | |
|       loadLocale(lang, translateFragment);
 | |
|     }, false);
 | |
|   } else if (window.attachEvent) { // IE8 and before (= oldIE)
 | |
|     // TODO: check if jQuery is loaded (CSS selector + JSON + events)
 | |
| 
 | |
|     // dummy `console.log' and `console.warn' functions
 | |
|     if (!window.console) {
 | |
|       consoleLog = function(message) {}; // just ignore console.log calls
 | |
|       consoleWarn = function(message) {
 | |
|         if (gDEBUG)
 | |
|           alert('[l10n] ' + message); // vintage debugging, baby!
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     // worst hack ever for IE6 and IE7
 | |
|     if (!window.JSON) {
 | |
|       consoleWarn('[l10n] no JSON support');
 | |
| 
 | |
|       getL10nAttributes = function(element) {
 | |
|         if (!element)
 | |
|           return {};
 | |
|         var l10nId = element.getAttribute('data-l10n-id'),
 | |
|             l10nArgs = element.getAttribute('data-l10n-args'),
 | |
|             args = {};
 | |
|         if (l10nArgs) try {
 | |
|           args = eval(l10nArgs); // XXX yeah, I know...
 | |
|         } catch (e) {
 | |
|           consoleWarn('[l10n] could not parse arguments for #' + l10nId);
 | |
|         }
 | |
|         return { id: l10nId, args: args };
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     // override `getTranslatableChildren' and `getL10nResourceLinks'
 | |
|     if (!document.querySelectorAll) {
 | |
|       consoleWarn('[l10n] no "querySelectorAll" support');
 | |
| 
 | |
|       getTranslatableChildren = function(element) {
 | |
|         if (!element)
 | |
|           return [];
 | |
|         var nodes = element.getElementsByTagName('*'),
 | |
|             l10nElements = [],
 | |
|             n = nodes.length;
 | |
|         for (var i = 0; i < n; i++) {
 | |
|           if (nodes[i].getAttribute('data-l10n-id'))
 | |
|             l10nElements.push(nodes[i]);
 | |
|         }
 | |
|         return l10nElements;
 | |
|       };
 | |
| 
 | |
|       getL10nResourceLinks = function() {
 | |
|         var links = document.getElementsByTagName('link'),
 | |
|             l10nLinks = [],
 | |
|             n = links.length;
 | |
|         for (var i = 0; i < n; i++) {
 | |
|           if (links[i].type == 'application/l10n')
 | |
|             l10nLinks.push(links[i]);
 | |
|         }
 | |
|         return l10nLinks;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     // fire non-standard `localized' DOM events
 | |
|     if (document.createEventObject && !document.createEvent) {
 | |
|       fireL10nReadyEvent = function(lang) {
 | |
|         // hack to simulate a custom event in IE:
 | |
|         // to catch this event, add an event handler to `onpropertychange'
 | |
|         document.documentElement.localized = 1;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     // startup for IE<9
 | |
|     window.attachEvent('onload', function() {
 | |
|       gTextProp = document.body.textContent ? 'textContent' : 'innerText';
 | |
|       var lang = document.documentElement.lang || navigator.language || navigator.userLanguage || 'en';
 | |
|       loadLocale(lang, translateFragment);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // cross-browser API (sorry, oldIE doesn't support getters & setters)
 | |
|   return {
 | |
|     // get a localized string
 | |
|     get: function(key, args, fallback) {
 | |
|       var data = getL10nData(key, args) || fallback;
 | |
|       if (data) { // XXX double-check this
 | |
|         return 'textContent' in data ? data.textContent : '';
 | |
|       }
 | |
|       return '{{' + key + '}}';
 | |
|     },
 | |
| 
 | |
|     // debug
 | |
|     getData: function() { return gL10nData; },
 | |
|     getText: function() { return gTextData; },
 | |
| 
 | |
|     // get|set the document language
 | |
|     getLanguage: function() { return gLanguage; },
 | |
|     setLanguage: function(lang) { loadLocale(lang, 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'];
 | |
|       return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
 | |
|     },
 | |
| 
 | |
|     // translate an element or document fragment
 | |
|     translate: translateFragment,
 | |
| 
 | |
|     // this can be used to prevent race conditions
 | |
|     getReadyState: function() { return gReadyState; }
 | |
|   };
 | |
| 
 | |
| }) (window, document);
 | |
| 
 | |
| // gettext-like shortcut for navigator.webL10n.get
 | |
| if (window._ === undefined)
 | |
|   var _ = document.webL10n.get;
 | |
| 
 | |
| // CommonJS
 | |
| try {
 | |
|   exports = document.webL10n;
 | |
| }catch(e){} |