You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1210 lines
34 KiB
1210 lines
34 KiB
/** |
|
* 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 <link type="application/l10n" href="..." /> 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 <script defer> or <script async>, |
|
// the DOM is ready for parsing. |
|
window.setTimeout(l10nStartup); |
|
} |
|
} 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! |
|
} |
|
}; |
|
} |
|
|
|
// XMLHttpRequest for IE6 |
|
if (!window.XMLHttpRequest) { |
|
xhrLoadText = function(url, onSuccess, onFailure) { |
|
onSuccess = onSuccess || function _onSuccess(data) {}; |
|
onFailure = onFailure || function _onFailure() { |
|
consoleWarn(url + ' not found.'); |
|
}; |
|
var xhr = new ActiveXObject('Microsoft.XMLHTTP'); |
|
xhr.open('GET', url, gAsyncResourceLoading); |
|
xhr.onreadystatechange = function() { |
|
if (xhr.readyState == 4) { |
|
if (xhr.status == 200) { |
|
onSuccess(xhr.responseText); |
|
} else { |
|
onFailure(); |
|
} |
|
} |
|
}; |
|
xhr.send(null); |
|
}; |
|
} |
|
|
|
// worst hack ever for IE6 and IE7 |
|
if (!window.JSON) { |
|
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('could not parse arguments for #' + l10nId); |
|
} |
|
return { id: l10nId, args: args }; |
|
}; |
|
} |
|
|
|
// override `getTranslatableChildren' and `getL10nResourceLinks' |
|
if (!document.querySelectorAll) { |
|
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; |
|
}; |
|
} |
|
|
|
// override `getL10nDictionary' |
|
if (!window.JSON || !document.querySelectorAll) { |
|
getL10nDictionary = function() { |
|
var scripts = document.getElementsByName('script'); |
|
for (var i = 0; i < scripts.length; i++) { |
|
if (scripts[i].type == 'application/l10n') { |
|
return eval(scripts[i].innerHTML); |
|
} |
|
} |
|
return null; |
|
}; |
|
} |
|
|
|
// 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.textContent === null ? 'textContent' : 'innerText'; |
|
l10nStartup(); |
|
}); |
|
} |
|
|
|
// cross-browser API (sorry, oldIE doesn't support getters & setters) |
|
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(); |
|
}); |
|
} else if (document.attachEvent) { |
|
document.documentElement.attachEvent('onpropertychange', function once(e) { |
|
if (e.propertyName === 'localized') { |
|
document.documentElement.detachEvent('onpropertychange', once); |
|
callback(); |
|
} |
|
}); |
|
} |
|
} |
|
}; |
|
}) (window, document); |
|
|
|
// gettext-like shortcut for document.webL10n.get |
|
if (window._ === undefined) { |
|
var _ = document.webL10n.get; |
|
} |
|
|
|
|