applitude
Version:
Simple Module Management
596 lines (502 loc) • 19.6 kB
JavaScript
(function() {
// add indexOf to non ECMA-262 standard compliant browsers
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
"use strict";
if (this == null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 0) {
n = Number(arguments[1]);
if (n != n) { // shortcut for verifying if it's NaN
n = 0;
} else if (n != 0 && n != Infinity && n != -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
for (; k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
}
}
var root = this
, $ = root.jQuery
, i18n = {};
// Export the i18next object for **CommonJS**.
// If we're not in CommonJS, add `i18n` to the
// global object or to jquery.
if (typeof module !== 'undefined' && module.exports) {
module.exports = i18n;
} else {
if ($) {
$.i18n = $.i18n || i18n;
} else {
root.i18n = root.i18n || i18n;
}
}
// defaults
var o = {
lng: undefined,
lowerCaseLng: false,
fallbackLng: 'dev',
ns: 'translation',
nsseparator: ':',
keyseparator: '.',
debug: false,
resGetPath: 'locales/__lng__/__ns__.json',
resPostPath: 'locales/add/__lng__/__ns__',
resStore: undefined,
useLocalStorage: true,
dynamicLoad: false,
sendMissing: false,
interpolationPrefix: '__',
interpolationSuffix: '__',
reusePrefix: '$t(',
reuseSuffix: ')',
pluralSuffix: '_plural',
pluralNotFound: ['plural_not_found', Math.random()].join(''),
contextNotFound: ['context_not_found', Math.random()].join(''),
setJqueryExt: true
};
// move dependent functions to a container so that
// they can be overriden easier in no jquery environment (node.js)
var f = {
extend: $ ? $.extend : undefined,
each: $ ? $.each : undefined,
ajax: $ ? $.ajax : undefined,
detectLanguage: detectLanguage,
log: function(str) {
if (o.debug) console.log(str);
},
cookie: {
create: function(name,value,minutes) {
var expires;
if (minutes) {
var date = new Date();
date.setTime(date.getTime()+(minutes*60*1000));
expires = "; expires="+date.toGMTString();
}
else expires = "";
document.cookie = name+"="+value+expires+"; path=/";
},
read: function(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length);
}
return null;
},
remove: function(name) {
this.create(name,"",-1);
}
}
};
var resStore
, currentLng
, replacementCounter = 0
, languages = [];
function init(options, cb) {
if (typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
// override defaults with passed in options
f.extend(o, options);
// create namespace object if namespace is passed in as string
if (typeof o.ns == 'string') {
o.ns = { namespaces: [o.ns], defaultNs: o.ns};
}
if (!o.lng) {
o.lng = f.detectLanguage();
} else {
// set cookie with lng set (as detectLanguage will set cookie on need)
f.cookie.create('i18next', o.lng);
}
languages = [];
if (o.lng.indexOf('-') === 2 && o.lng.length === 5) {
var parts = o.lng.split('-');
o.lng = o.lowerCaseLng ?
parts[0].toLowerCase() + '-' + parts[1].toLowerCase() :
parts[0].toLowerCase() + '-' + parts[1].toUpperCase();
languages.push(o.lng);
languages.push(o.lng.substr(0, 2));
} else {
languages.push(o.lng);
}
if (languages.indexOf(o.fallbackLng) === -1) languages.push(o.fallbackLng);
currentLng = o.lng;
f.log('currentLng set to: ' + currentLng);
// add JQuery extensions
if ($ && o.setJqueryExt) addJqueryFunct();
// return immidiatly if res are passed in
if (o.resStore) {
resStore = o.resStore;
if (cb) cb(translate);
return;
}
// else load them
i18n.sync.load(languages, o, function(err, store) {
resStore = store;
if (cb) cb(translate);
});
}
function setLng(lng, cb) {
init({lng: lng}, cb);
}
function addJqueryFunct() {
// $.t shortcut
$.t = $.t || translate;
function parse(ele, key, options) {
if (key.length === 0) return;
var attr = 'text';
if (key.indexOf('[') === 0) {
var parts = key.split(']');
key = parts[1];
attr = parts[0].substr(1, parts[0].length-1);
}
if (key.indexOf(';') === key.length-1) {
key = key.substr(0, key.length-2);
}
var optionsToUse;
if (attr === 'text') {
optionsToUse = $.extend({ defaultValue: ele.text() }, options);
ele.text($.t(key, optionsToUse));
} else {
optionsToUse = $.extend({ defaultValue: ele.attr(attr) }, options);
ele.attr(attr, $.t(key, optionsToUse));
}
}
function localize(ele, options) {
var key = ele.attr('data-i18n');
if (!key) return;
if (key.indexOf(';') <= key.length-1) {
var keys = key.split(';');
$.each(keys, function(m, k) {
parse(ele, k, options);
});
} else {
parse(ele, key, options);
}
}
// fn
$.fn.i18n = function (options) {
return this.each(function() {
// localize element itself
localize($(this), options);
// localize childs
var elements = $(this).find('[data-i18n]');
elements.each(function() {
localize($(this), options);
});
});
};
}
function applyReplacement(str, replacementHash, nestedKey) {
f.each(replacementHash, function(key, value) {
if (typeof value === 'object') {
str = applyReplacement(str, value, key);
} else {
str = str.replace([o.interpolationPrefix, nestedKey ? nestedKey + '.' + key : key, o.interpolationSuffix].join(''), value);
}
});
return str;
}
function applyReuse(translated, options){
while (translated.indexOf(o.reusePrefix) != -1) {
replacementCounter++;
if (replacementCounter > o.maxRecursion) { break; } // safety net for too much recursion
var index_of_opening = translated.indexOf(o.reusePrefix);
var index_of_end_of_closing = translated.indexOf(o.reuseSuffix, index_of_opening) + o.reuseSuffix.length;
var token = translated.substring(index_of_opening, index_of_end_of_closing);
var token_sans_symbols = token.replace(o.reusePrefix, '').replace(o.reuseSuffix, '');
var translated_token = _translate(token_sans_symbols,options);
translated = translated.replace(token,translated_token);
}
return translated;
}
function hasContext(options) {
return (options.context && typeof options.context == 'string');
}
function needsPlural(options) {
return (options.count !== undefined && typeof options.count != 'string' && options.count !== 1);
}
function translate(key, options){
replacementCounter = 0;
return _translate(key, options);
}
function _translate(key, options){
options = options || {};
var optionsSansCount, translated
, notfound = options.defaultValue || key;
if (!resStore) { return notfound; } // no resStore to translate from
var ns = o.ns.defaultNs;
if (key.indexOf(o.nsseparator) > -1) {
var parts = key.split(o.nsseparator);
ns = parts[0];
key = parts[1];
}
if (hasContext(options)) {
optionsSansCount = f.extend({}, options);
delete optionsSansCount.context;
optionsSansCount.defaultValue = o.contextNotFound;
var contextKey = key + '_' + options.context;
translated = translate(contextKey, optionsSansCount);
if (translated != o.contextNotFound) {
return applyReplacement(translated, { context: options.context }); // apply replacement for context only
} // else continue translation with original/nonContext key
}
if (needsPlural(options)) {
optionsSansCount = f.extend({}, options);
delete optionsSansCount.count;
optionsSansCount.defaultValue = o.pluralNotFound;
var pluralKey = key + o.pluralSuffix;
var pluralExtension = pluralExtensions.get(currentLng, options.count);
if (pluralExtension !== 'other') { pluralKey = pluralKey + '_' + pluralExtension; }
translated = translate(pluralKey, optionsSansCount);
if (translated != o.pluralNotFound) {
return applyReplacement(translated, { count: options.count }); // apply replacement for count only
} // else continue translation with original/singular key
}
var found;
var keys = key.split(o.keyseparator);
for (var i = 0, len = languages.length; i < len; i++ ) {
if (found) break;
var l = languages[i];
var x = 0;
var value = resStore[l][ns];
while (keys[x]) {
value = value && value[keys[x]];
x++;
}
if (value) {
if (typeof value !== 'string') {
value = 'key \'' + ns + ':' + key + ' (' + l + ')\' ' +
'returned a object instead of string.';
f.log(value);
}
value = applyReplacement(value, options);
value = applyReuse(value, options);
found = value;
}
}
if (!found && o.sendMissing) {
sync.postMissing(ns, key, notfound);
}
return (found) ? found : notfound;
}
function detectLanguage() {
var detectedLng;
// get from qs
var qsParm = [];
(function() {
var query = window.location.search.substring(1);
var parms = query.split('&');
for (var i=0; i<parms.length; i++) {
var pos = parms[i].indexOf('=');
if (pos > 0) {
var key = parms[i].substring(0,pos);
var val = parms[i].substring(pos+1);
qsParm[key] = val;
}
}
})();
if (qsParm.setLng) {
detectedLng = qsParm.setLng;
// set cookie
f.cookie.create('i18next', detectedLng);
}
// get from cookie
if (!detectedLng) {
var c = f.cookie.read('i18next');
if (c) detectedLng = c;
}
// get from navigator
if (!detectedLng && navigator) {
detectedLng = (navigator.language) ? navigator.language : navigator.userLanguage;
}
// no luck so use fallback
if (!detectedLng) {
detectedLng = o.fallbackLng;
}
return detectedLng;
}
function lng() {
return currentLng;
}
var sync = {
load: function(lngs, options, cb) {
if (options.useLocalStorage) {
sync._loadLocal(lngs, function(err, store) {
var missingLngs = [];
for (var i = 0, len = lngs.length; i < len; i++) {
if (!store[lngs[i]]) missingLngs.push(lngs[i]);
}
if (missingLngs.length > 0) {
sync._fetch(missingLngs, options, function(err, fetched){
f.extend(store, fetched);
sync._storeLocal(fetched);
cb(null, store);
});
} else {
cb(null, store);
}
});
} else {
sync._fetch(lngs, options, function(err, store){
cb(null, store);
});
}
},
_loadLocal: function(lngs, cb) {
var store = {};
if(window.localStorage) {
var todo = lngs.length;
f.each(lngs, function(key, lng) {
var local = window.localStorage.getItem('res_' + lng);
if (local) {
store[lng] = JSON.parse(local);
}
todo--; // wait for all done befor callback
if (todo === 0) cb(null, store);
});
}
},
_storeLocal: function(store) {
if(window.localStorage) {
for (var m in store) {
window.localStorage.setItem('res_' + m, JSON.stringify(store[m]));
}
}
return;
},
_fetch: function(lngs, options, cb) {
var ns = options.ns
, store = {};
if (!options.dynamicLoad) {
var todo = ns.namespaces.length * lngs.length
, errors;
// load each file individual
f.each(ns.namespaces, function(nsIndex, nsValue) {
f.each(lngs, function(lngIndex, lngValue) {
sync._fetchOne(lngValue, nsValue, function(err, data) {
if (err) {
errors = errors || [];
errors.push(err);
}
store[lngValue] = store[lngValue] || {};
store[lngValue][nsValue] = data;
todo--; // wait for all done befor callback
if (todo === 0) cb(errors, store);
});
});
});
} else {
var url = applyReplacement(o.resGetPath, { lng: lngs.join('+'), ns: ns.namespaces.join('+') });
// load all needed stuff once
f.ajax({
url: url,
success: function(data, status, xhr) {
f.log('loaded: ' + url);
cb(null, data);
},
error : function(xhr, status, error) {
f.log('failed loading: ' + url);
cb('failed loading resource.json error: ' + error);
},
dataType: "json"
});
}
},
_fetchOne: function(lng, ns, done) {
var url = applyReplacement(o.resGetPath, { lng: lng, ns: ns });
f.ajax({
url: url,
success: function(data, status, xhr) {
f.log('loaded: ' + url);
done(null, data);
},
error : function(xhr, status, error) {
f.log('failed loading: ' + url);
done(error, {});
},
dataType: "json"
});
},
postMissing: function(ns, key, defaultValue) {
var payload = {};
payload[key] = defaultValue;
var url = applyReplacement(o.resPostPath, { lng: o.fallbackLng, ns: ns });
f.ajax({
url: url,
type: 'POST',
data: payload,
success: function(data, status, xhr) {
f.log('posted missing key \'' + key + '\' to: ' + url);
resStore[o.fallbackLng][ns][key] = defaultValue;
},
error : function(xhr, status, error) {
f.log('failed posting missing key \'' + key + '\' to: ' + url);
},
dataType: "json"
});
}
};
// definition http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
var pluralExtensions = {
rules: {
'sl': function (n) {
return n % 100 === 1 ? 'one' : n % 100 === 2 ? 'two' : n % 100 === 3 || n % 100 === 4 ? 'few' : 'other';
},
'ar': function (n) {
return n === 0 ? 'zero' : n === 1 ? 'one' : n === 2 ? 'two' : n % 100 >= 3 && n % 100 <= 10 ? 'few' : n % 100 >= 11 && n % 100 <= 99 ? 'many' : 'other';
}
},
// for demonstration only sl and ar is added but you can add your own pluralExtensions
addRule: function(lng, fc) {
pluralExtensions.rules[lng] = fc;
},
get: function(lng, count) {
var parts = lng.split('-');
function getResult(l, c) {
if (pluralExtensions.rules[l]) {
return pluralExtensions.rules[l](c);
} else {
return c === 1 ? 'one' : 'other';
}
}
if (parts.length === 2) {
return getResult(parts[0], count);
} else {
return getResult(lng, count);
}
}
};
// public api interface
i18n.init = init;
i18n.setLng = setLng;
i18n.t = translate;
i18n.translate = translate;
i18n.detectLanguage = f.detectLanguage;
i18n.pluralExtensions = pluralExtensions;
i18n.sync = sync;
i18n.functions = f;
i18n.lng = lng;
i18n.options = o;
})();