/**
 * 
 * All content on this website (including text, images, source
 * code and any other original works), unless otherwise noted,
 * is licensed under a Creative Commons License.
 * 
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 * 
 * Copyright (C) 2004-2010 Open-Xchange, Inc.
 * Mail: info@open-xchange.com 
 * 
 * @author Viktor Pracht <viktor.pracht@open-xchange.com>
 * 
 */

/**
 * A class for translated strings.
 * Each I18n object has a toString() method which returns the translation based
 * on the current language. All user-visible widgets expect instances of this
 * class, and convert them to strings when the user changes the GUI language.
 * @param {function()} toString A callback function which returns a translated
 * text using the current GUI language.
 */
function I18nString(toString) { this.toString = toString; }
I18nString.prototype = new String();
I18nString.prototype.valueOf = function() { return this.toString(); };
// TODO print warnings if one of the inherited methods is used.

/**
 * Translates a string
 * @function
 * @param {String} text The original English text to translate.
 * @type I18nString
 * @return The translated text.
 * @ignore
 */
var _;

/**
 * Converts a string to a translated string without translation.
 * Use only for user-entered data.
 * @param {String} text The text which should not be translated.
 * @type I18nString
 * @return The text as an I18nString object.
 */
var noI18n;

/**
 * Translates a string
 * @function
 * @param {String} text The original English text to translate.
 * @type I18nString
 * @return The translated text.
 * @ignore
 */
var gettext;

/**
 * Translates a string
 * @function
 * @param {String} context A context to differentiate multiple identical texts
 * with different translations.
 * @param {String} text The original English text to translate.
 * @type I18nString
 * @return The translated text.
 * @ignore
 */
var pgettext;

/**
 * Translates a string
 * @function
 * @param {String} domain An i18n domain to use for the translation.
 * @param {String} context A context to differentiate multiple identical texts
 * with different translations.
 * @param {String} text The original English text to translate.
 * @type I18nString
 * @return The translated text.
 */
var dpgettext;

/**
 * Translates a string containing numbers.
 * @function
 * @param {String} singular The original English text for the singular form.
 * @param {String} plural The original English text for the plural form.
 * @param {Number} n The number which determines which text form is used.
 * @param {String} context An optional context to differentiate multiple
 * identical texts with different translations.
 * @param {String} domain An optional i18n domain to use for the translation.
 * @type I18nString
 * @return The translated text.
 * @ignore
 */
var ngettext;

/**
 * Translates a string containing numbers.
 * @function
 * @param {String} context A context to differentiate multiple identical texts
 * with different translations.
 * @param {String} singular The original English text for the singular form.
 * @param {String} plural The original English text for the plural form.
 * @param {Number} n The number which determines which text form is used.
 * @type I18nString
 * @return The translated text.
 * @ignore
 */
var npgettext;

/**
 * Translates a string containing numbers.
 * @function
 * @param {String} domain An i18n domain to use for the translation.
 * @param {String} context A context to differentiate multiple identical texts
 * with different translations.
 * @param {String} singular The original English text for the singular form.
 * @param {String} plural The original English text for the plural form.
 * @param {Number} n The number which determines which text form is used.
 * @type I18nString
 * @return The translated text.
 */
var dnpgettext;

/**
 * Adds a new i18n domain, usually for a plugin.
 * @function
 * @param {String} domain A new domain name, usually the plugin name.
 * @param {String} pattern A Pattern which is used to find the PO or JS file on
 * the server. The pattern is processed by formatting it with the language ID
 * as the only parameter. The formatted result is used to download the file
 * from the server.
 */
var bindtextdomain;

/**
 * Changes the current language which is used for all subsequent translations.
 * Also translates all currently displayed strings.
 * @function
 * @param {String} name The ID of the new language.
 */
var setLanguage;

/**
 * Returns the translation dictionary for the specified language.
 * @private
 * @function
 * @param {String} name The language ID of the dictionary to return.
 * @type Object
 * @return The translation dictionary of the specified language.
 */
var getDictionary;

/**
 * Formats a string by replacing printf-style format specifiers in the string
 * with dynamic parameters. Flags, width, precision and length modifiers are
 * not supported. All type conversions are performed by the standard toString()
 * JavaScript method.
 * @param {String or I18nString} string The format string.
 * @param params Either an array with parameters or multiple separate
 * parameters.
 * @type String or I18nString
 * @return The formatted string.
 */
function format(string, params) {
	var param_array = params;
	if (Object.prototype.toString.call(params) != "[object Array]") {
		param_array = new Array(arguments.length - 1);
		for (var i = 1; i < arguments.length; i++)
			param_array[i - 1] = arguments[i];
	}
    if (string instanceof I18nString) {
        return new I18nString(function() {
            return formatRaw(string, param_array);
        });
    } else {
        return formatRaw(string, param_array);
    }
}

/**
 * @private
 * Formats a string by replacing printf-style format specifiers in the string
 * with dynamic parameters. Flags, width, precision and length modifiers are
 * not supported. All type conversions (except from I18nString) are performed
 * by the standard toString() JavaScript method.
 * @param {String} string The format string.
 * @param params An array with parameters.
 * @type String
 * @return The formatted string.
 * @ignore
 */
function formatRaw(string, params) {
    var index = 0;
    return String(string).replace(/%(([0-9]+)\$)?[A-Za-z]/g,
        function(match, pos, n) {
            if (pos) index = n - 1;
            return params[index++];
        }).replace(/%%/, "%");
}

/**
 * Formats and translates an error returned by the server.
 * @param {Object} result the JSON object as passed to a JSON callback function.
 * @param {String} formatString an optional format string with the replacement
 * parameters <dl><dt>%1$s</dt><dd>the error code,</dd>
 * <dt>%2$s</dt><dd>the fomratter error message,</dd>
 * <dt>%3$s</dt><dd>the unique error ID.</dd></dl>
 * @type String
 * @returns the formatted and translated error message.
 * @ignore
 */
function formatError(result, formatString) {
	//#. %1$s is the error code.
	//#. %2$s is the formatted error message.
    //#. %3$s is the unique error ID.
	//#, c-format
	return format(formatString || _("Error: %2$s (%1$s, %3$s)"), result.code,
                  format(_(result.error), result.error_params),
                  result.error_id);
}

/**
 * Utility function which checks for untranslated strings.
 * Should be used by widget implementations to convert I18nString to strings
 * immediately before displaying them.
 * @param {I18nString} text The translated text.
 * @type String
 * @return The current translation of the text as a string.
 */
function expectI18n(text) {
    expectI18n = debug ? function(text) {
        if (!(text instanceof I18nString)) {
            console.warn("Untranslated text:",
                typeof text == "function" ? text() : text, getStackTrace());
        }
        return String(text);
    } : String;
    return expectI18n(text);
}

(function() {
    var current, current_lang;
    var domains = { "": "lang/%s.js" };
    var languages = {};
    var originals = {};
	var counter = 0;

	_ = gettext = function(text) { return dpgettext("", "", text); };
	
	noI18n = function(text) { return new I18nString(constant(text)); };
    
    pgettext = function(context, text) { return dpgettext("", context, text); };
    
    function dpgettext_(domain, context, text) {
        return new I18nString(function() {
            var c = current && current[domain || ""];
            var key = context ? context + "\0" + text : text;
            return c && c.dictionary[key] || text;
        });
    }
    dpgettext = function() {
        dpgettext = debug ? function(domain, context, text) {
            if (text instanceof I18nString) {
                console.error("Retranslation", text);
            }
            return dpgettext_.apply(this, arguments);
        } : dpgettext_;
        return dpgettext.apply(this, arguments);
    };
    
    ngettext = function(singular, plural, n) {
        return dnpgettext("", "", singular, plural, n);
    };

	npgettext = function(context, singular, plural, n) {
        return dnpgettext("", context, singular, plural, n);
    };
    
    dnpgettext = function(domain, context, singular, plural, n) {
		var text = n != 1 ? plural : singular;
		return new I18nString(function() {
            var c = current && current[domain || ""];
            if (!c) return text;
            var key = context ?
                [context, "\0", singular, "\x01", plural].join("") :
                [               singular, "\x01", plural].join("");
    		var translation = c.dictionary[key];
    		if (!translation) return text;
    		return translation[Number(c.plural(n))] || text;
		});
	};

    function parse(pattern, file) {
        if (pattern.substring(pattern.length - 2) == "po") {
            return parsePO(file);
        } else {
            return (new Function("return " + file))();
        }
    }
    
    bindtextdomain = function(domain, pattern) {
        domains[domain] = pattern;
        if (languages[current_lang] === current) setLanguage(current_lang);
    };

	setLanguage = function (name, cont) {
        current_lang = name;
        var new_lang = languages[name];
		if (!new_lang) {
            loadLanguage(name, cont);
            return;
        }
        for (var i in domains) {
            if (!(i in new_lang)) {
                loadLanguage(name, cont);
                return;
            }
        }
		current = new_lang;
		for (var i in init.i18n) {
			var attrs = init.i18n[i].split(",");
			var node = $(i);
			if(node) {
				for (var j = 0; j < attrs.length; j++) {
					var attr = attrs[j];
					var id = attr + "," + i;
					var text = attr ? node.getAttributeNode(attr)
					                : node.firstChild;
                    var val = text && String(text.nodeValue);
					if (!val || val == "\xa0" )
                        alert(format('Invalid i18n for id="%s"', i));
					var original = originals[id];
					if (!original) original = originals[id] = val;
                    var context = "";
                    var pipe = original.indexOf("|");
                    if (pipe >= 0) {
                        context = original.substring(0, pipe);
                        original = original.substring(pipe + 1);
                    }
					text.nodeValue = dpgettext("", context, original);
				}
			}
		}
        triggerEvent("LanguageChangedInternal");
		triggerEvent("LanguageChanged");
		if (cont) { cont(name); }
    };
    
    function loadLanguage(name, cont) {
		// check the main window
        if (corewindow != window) {
            var core_dict = corewindow.getDictionary(name);
            if (core_dict) {
    			current = languages[name] = core_dict;
                setLanguage(name, cont);
                return;
    		}
        }
        var curr = languages[name];
        if (!curr) curr = languages[name] = {};
        var join = new Join(function() { setLanguage(name, cont); });
        var lock = join.add();
        for (var d in domains) {
            if (!(d in curr)) {
            	// get file name
            	var file = format(domains[d], name);
            	// add pre-compression (specific languages only)
            	file = file.replace(/(de_DE|en_GB|en_US)\.js/, "$1.jsz");
            	// inject version
            	var url = urlify(file);
            	// get language file
                (new JSON()).get(url, null,
                    join.add((function(domain) {
                        return function(file) {
                            try {
                                languages[name][domain] = parse(domains[domain], file);
                            } catch (e) {
                                triggerEvent("OX_New_Error", 4, e);
                                join.add(); // prevent setLanguage()
                            }
                        };
                    })(d)),
                    join.alt((function(domain) {
                        return function(result, status) {
                            languages[name][domain] = false;
                            return status == 404;
                        };
                    })(d)),
                    true
                );
            }
		}
        lock();
	}
	
	getDictionary = function(name) { return languages[name]; };	

})();

function parsePO(file) {
    parsePO.tokenizer.lastIndex = 0;
    var line_no = 1;
    
    function next() {
        while (parsePO.tokenizer.lastIndex < file.length) {
            var t = parsePO.tokenizer.exec(file);
            if (t[1]) continue;
            if (t[2]) {
                line_no++;
                continue;
            }
            if (t[3]) return t[3];
            if (t[4]) return t[4];
            if (t[5]) throw new Error(format(
                "Invalid character in line %s.", line_no));
        }
    }

    var lookahead = next();

    function clause(name, optional) {
        if (lookahead == name) {
            lookahead = next();
            var parts = [];
            while (lookahead && lookahead.charAt(0) == '"') {
                parts.push((new Function("return " + lookahead))());
                lookahead = next();
            }
            return parts.join("");
        } else if (!optional) {
            throw new Error(format(
                "Unexpected '%1$s' in line %3$s, expected '%2$s'.",
                lookahead, name, line_no));
        }
    }
    
    if (clause("msgid") != "") throw new Error("Missing PO file header");
    var header = clause("msgstr");
    if (parsePO.headerRegExp.exec(header)) {
        var po = (new Function("return " + header.replace(parsePO.headerRegExp,
            "{ nplurals: $1, plural: function(n) { return $2; }, dictionary: {} }"
            )))();
    } else {
        var po = { nplurals: 1, plural: function(n) { return 0; },
                   dictionary: {} };
    }
    while (lookahead) {
        var ctx = clause("msgctxt", true);
        var id = clause("msgid");
        var id_plural = clause("msgid_plural", true);
        var str;
        if (id_plural !== undefined) {
            id = id += "\x01" + id_plural;
            str = {};
            for (var i = 0; i < po.nplurals; i++) {
                str[i] = clause("msgstr[" + i + "]");
            }
        } else {
            str = clause("msgstr");
        }
        if (ctx) id = ctx + "\0" + id;
        po.dictionary[id] = str;
    }
    return po;
}

parsePO.tokenizer = new RegExp(
    '^(#.*|[ \\t\\v\\f]+)$' +                  // comment or empty line
    '|(\\r\\n|\\r|\\n)' +                      // linebreak (for line numbering)
    '|^(msg[\\[\\]\\w]+)(?:$|[ \\t\\v\\f]+)' + // keyword
    '|[ \\t\\v\\f]*(".*")\\s*$' +              // string
    '|(.)',                                    // anything else is an error
    "gm");

parsePO.headerRegExp = new RegExp(
    '^(?:[\\0-\\uffff]*\\n)?' +                         // ignored prefix
    'Plural-Forms:\\s*nplurals\\s*=\\s*([0-9]+)\\s*;' + // nplurals
                 '\\s*plural\\s*=\\s*([^;]*);' +        // plural
    '[\\0-\\uffff]*$'                                   // ignored suffix
);

/**
 * Encapsulation of a single translated text node which is created at runtime.
 * @param {Function} callback A function which is called as a method of
 * the created object and returns the current translated text.
 * @param {Object} template An optional object which is used for the initial
 * translation. All enumerable properties of the template will be copied to
 * the newly created object before the first call to callback.
 *
 * Fields of the created object:
 *
 * node: The DOM text node which is automatically translated.
 * @ignore
 */
function I18nNode(callback, template) {
    if (template) for (var i in template) this[i] = template[i];
    if (callback instanceof I18nString) {
        this.callback = function() { return String(callback); };
    } else {
        if (typeof callback != "function") {
            if (debug) {
                console.warn("Untranslated string:", callback, getStackTrace());
            }
            this.callback = function() { return _(callback); };
        } else {
            if (debug) {
                console.warn("Untranslated string:", callback(),
                             getStackTrace());
            }
            this.callback = callback;
        }
    }
    this.index = ++I18nNode.counter;
	this.node = document.createTextNode(this.callback());
	this.enable();
}

I18nNode.prototype = {
	/**
	 * Updates the node contents. Is called whenever the current language
	 * changes and should be also called when the displayed value changes.
	 * @ignore
     */
	update: function() {
        if (typeof this.callback != "function") {
            console.error(format(
                "The callback \"%s\" has type \"%s\".",
                this.callback, typeof this.callback));
        } else {
/**#nocode+*/
            this.node.data = this.callback();
/**#nocode-*/
        }
    },
	
	/**
	 * Disables automatic updates for this object.
	 * Should be called when the text node is removed from the DOM tree.
     * @ignore
	 */
	disable: function() { delete I18nNode.nodes[this.index]; },
	
	/**
	 * Reenables previously disabled updates.
     * @ignore
	 */
 	enable: function() { I18nNode.nodes[this.index] = this; }
};

I18nNode.nodes = {};
I18nNode.counter = 0;

register("LanguageChanged", function() {
	for (var i in I18nNode.nodes) I18nNode.nodes[i].update();
});

/**
 * Creates an automatically updated node from a static text. The node can not
 * be removed.
 * @param {I18nString} text The text to be translated. It must be marked with
 * the <code>&#57;*i18n*&#57;</code> comment.
 * @param {String} context An optional context to differentiate multiple
 * identical texts with different translations. It must be marked with
 * the <code>&#57;*i18n context*&#57;</code> comment.
 * @param {String} domain An optional i18n domain to use for the translation.
 * @type Object
 * @return The new DOM text node.
 * @ignore
 */
function addTranslated(text, context, domain) {
	return (new I18nNode(text instanceof I18nString ? text :
	    dpgettext(domain, context, text))).node;
}

/**
 * Returns whether a date is today.
 * @param utc The date. Any valid parameter to new Date() will do.
 * @type Boolean
 * @return true if the parameter has today's date, false otherwise.
 * @ignore
 */
function isToday(utc) {
    var today = new Date(now());
    today.setUTCHours(0, 0, 0, 0);
    var diff = (new Date(utc)).getTime() - today.getTime();
    return diff >= 0 && diff < 864e5; // ms/day
}

/**
 * The first week with at least daysInFirstWeek days in a given year is defined
 * as the first week of that year.
 * @ignore
 */
var daysInFirstWeek = 4;

/**
 * First day of the week.
 * 0 = Sunday, 1 = Monday and so on.
 * @ignore
 */
var weekStart = 1;

function getDays(d) { return Math.floor(d / 864e5); }

/**
 * Computes the week number of the specified Date object, taking into account
 * daysInFirstWeek and weekStart.
 * @param {Date} d The date for which to calculate the week number.
 * @param {Boolean} inMonth True to compute the week number in a month,
 * False for the week number in a year 
 * @type Number
 * @return Week number of the specified date.
 * @ignore
 */
function getWeek(d, inMonth) {
	var keyDay = getKeyDayOfWeek(d);
	var keyDate = new Date(keyDay * 864e5);
	var jan1st = Date.UTC(keyDate.getUTCFullYear(),
	                      inMonth ? keyDate.getUTCMonth() : 0);
	return Math.floor((keyDay - getDays(jan1st)) / 7) + 1;
}
 
/**
 * Returns the day of the week which decides the week number
 * @return Day of week
 */
function getKeyDayOfWeek(d) {
	var firstDay = getDayInSameWeek(d, weekStart);
	return (firstDay + 7 - daysInFirstWeek);
}

/**
 * Computes the number of the first day of the specified week, taking into
 * account weekStart.
 * @param  {Date} d The date for which to calculate the first day of week number.
 * type Number
 * @return First day in the week as the number of days since 1970-01-01.
 * @ignore
 */
function getDayInSameWeek(d, dayInWeek) {
	return getDays(d.getTime()) - (d.getUTCDay() - dayInWeek + 7) % 7; 
}

/**
 * Formats a Date object according to a format string.
 * @function
 * @param {String} format The format string. It has the same syntax as Java's
 * java.text.SimpleDateFormat, assuming a Gregorian calendar.
 * @param {Date} date The Date object to format. It must contain a Time value as
 * defined in the HTTP API specification.
 * @type String
 * @return The formatted date and/or time.
 */
var formatDateTime;

/**
 * Parses a date and time according to a format string.
 * @function
 * @param {String} format The format string. It has the same syntax as Java's
 * java.text.SimpleDateFormat, assuming a Gregorian calendar.
 * @param {String} string The string to parse.
 * @type Date
 * @return The parsed date as a Date object. It will contain a Time value as
 * defined in the HTTP API specification.
 */
var parseDateTime;

/**
 * An array with translated week day names.
 * @ignore
 */
var weekdays = [];

(function() {

    var regex = /(G+|y+|M+|w+|W+|D+|d+|F+|E+|a+|H+|k+|K+|h+|m+|s+|S+|z+|Z+)|\'(.+?)\'|(\'\')/g;

	function num(n, x) {
		var s = x.toString();
		n -= s.length;
		if (n <= 0) return s;
		var a = new Array(n);
		for (var i = 0; i < n; i++) a[i] = "0";
		a[n] = s;
		return a.join("");
	}
	function text(n, full, shrt) {
		return n >= 4 ? _(full) : _(shrt);
	}
	var months = [
		"January"/*i18n*/, "February"/*i18n*/,     "March"/*i18n*/,
		  "April"/*i18n*/,      "May"/*i18n*/,      "June"/*i18n*/,
		   "July"/*i18n*/,   "August"/*i18n*/, "September"/*i18n*/,
		"October"/*i18n*/, "November"/*i18n*/,  "December"/*i18n*/
	];
	var shortMonths = [
		"Jan"/*i18n*/, "Feb"/*i18n*/, "Mar"/*i18n*/, "Apr"/*i18n*/,
		"May"/*i18n*/, "Jun"/*i18n*/, "Jul"/*i18n*/, "Aug"/*i18n*/,
		"Sep"/*i18n*/, "Oct"/*i18n*/, "Nov"/*i18n*/, "Dec"/*i18n*/
	];
	var days = weekdays.untranslated = [
		   "Sunday"/*i18n*/,   "Monday"/*i18n*/, "Tuesday"/*i18n*/,
		"Wednesday"/*i18n*/, "Thursday"/*i18n*/,  "Friday"/*i18n*/,
		 "Saturday"/*i18n*/
	];
	var shortDays = [
		"Sun"/*i18n*/, "Mon"/*i18n*/, "Tue"/*i18n*/, "Wed"/*i18n*/,
		"Thu"/*i18n*/, "Fri"/*i18n*/, "Sat"/*i18n*/
	];
	var funs = {
		G: function(n, d) {
			return d.getTime() < -62135596800000 ? _("BC") : _("AD");
		},
		y: function(n, d) {
			var y = d.getUTCFullYear();
			if (y < 1) y = 1 - y;
			return num(n, n == 2 ? y % 100 : y);
		},
		M: function(n, d) {
			var m = d.getUTCMonth();
			if (n >= 3) {
				return text(n, months[m], shortMonths[m]);
			} else {
				return num(n, m + 1);
			}
		},
		w: function(n, d) { return num(n, getWeek(d)); },
		W: function(n, d) { return num(n, getWeek(d, true)); },
		D: function(n, d) {
			return num(n,
				getDays(d.getTime() - Date.UTC(d.getUTCFullYear(), 0)) + 1);
		},
		d: function(n, d) { return num(n, d.getUTCDate()); },
		F: function(n, d) {
			return num(n, Math.floor(d.getUTCDate() / 7) + 1);
		},
		E: function(n, d) {
			var m = d.getUTCDay();
			return text(n, days[m], shortDays[m]);
		},
		a: function(n, d) {
            return d.getUTCHours() < 12 ? _("AM") : _("PM");
        },
		H: function(n, d) { return num(n, d.getUTCHours()); },
		k: function(n, d) { return num(n, d.getUTCHours() || 24); },
		K: function(n, d) { return num(n, d.getUTCHours() % 12); },
		h: function(n, d) { return num(n, d.getUTCHours() % 12 || 12); },
		m: function(n, d) { return num(n, d.getUTCMinutes()); },
		s: function(n, d) { return num(n, d.getUTCSeconds()); },
		S: function(n, d) { return num(n, d.getMilliseconds()); },
        // TODO: z and Z 
		z: function() { return ""; },
		Z: function() { return ""; }
	};
	formatDateTime = function(format, date) {
        return format instanceof I18nString ? new I18nString(fmt) : fmt();
	    function fmt() {
            return String(format).replace(regex,
                function(match, fmt, text, quote) {
                    if (fmt) {
                        return funs[fmt.charAt(0)](fmt.length, date);
                    } else if (text) {
                        return text;
                    } else if (quote) {
                        return "'";
                    }
                });
	    }
	};
    
    var f = "G+|y+|M+|w+|W+|D+|d+|F+|E+|a+|H+|k+|K+|h+|m+|s+|S+|z+|Z+";
    var pregexStr = "(" + f + ")(?!" + f + ")|(" + f + ")(?=" + f +
        ")|\'(.+?)\'|(\'\')|([$^\\\\.*+?()[\\]{}|])";
    var pregex = new RegExp(pregexStr, "g");
    
    var monthRegex;
    var monthMap;
    function recreateMaps() {
        var names = months.concat(shortMonths);
        for (var i = 0; i < names.length; i++) names[i] = escape(_(names[i]));
        monthRegex = "(" + names.join("|") + ")";
        monthMap = {};
        for (var i = 0; i < months.length; i++) {
            monthMap[_(months[i])] = i;
            monthMap[_(shortMonths[i])] = i;
        }
        weekdays.length = days.length;
        for (var i = 0; i < days.length; i++) weekdays[i] = _(days[i]);
    }
    recreateMaps();
    register("LanguageChangedInternal", recreateMaps);
    
    function escape(rex) {
        return String(rex).replace(/[$^\\.*+?()[\]{}|]/g, "\\$");
    }

    var numRex = "([+-]?\\d+)";
    function number(n) { return numRex; }
        
    var prexs = {
        G: function(n) {
            return "(" + escape(_("BC")) + "|" + escape(_("AD")) + ")";
        },
        y: number,
        M: function(n) { return n >= 3 ? monthRegex : numRex; },
        w: number, W: number, D: number, d: number, F: number, E: number,
        a: function(n) {
            return "(" + escape(_("AM")) + "|" + escape(_("PM")) + ")";
        },
        H: number, k: number, K: number, h: number, m: number, s: number,
        S: number
        // TODO: z and Z
    };
    
    function mnum(n) {
        return n > 1 ? "([+-]\\d{1," + (n - 1) + "}|\\d{1," + n + "})"
                     :                           "(\\d{1," + n + "})"; }
    
    var mrexs = {
        G: prexs.G, y: mnum,
        M: function(n) { return n >= 3 ? monthRegex : mnum(n); },
        w: mnum, W: mnum, D: mnum, d: mnum, F: mnum, E: prexs.E, a: prexs.a,
        H: mnum, k: mnum, K: mnum, h: mnum, m: mnum, s: mnum, S: mnum
        // TODO: z and Z
    };
    
    var pfuns = {
        G: function(n) { return function(s, d) { d.bc = s == _("BC"); }; },
        y: function(n) {
            return function(s, d) {
                d.century = n <= 2 && s.match(/^\d\d$/);
                d.y = s;
            };
        },
        M: function(n) {
            return n >= 3 ? function (s, d) { d.m = monthMap[s]; }
                          : function(s, d) { d.m = s - 1; };
        },
        w: emptyFunction, W: emptyFunction, D: emptyFunction,
        d: function(n) { return function(s, d) { d.d = s }; },
        F: emptyFunction, E: emptyFunction,
        a: function(n) { return function(s, d) { d.pm = s == _("PM"); }; },
        H: function(n) { return function(s, d) { d.h = s; }; },
        k: function(n) { return function(s, d) { d.h = s == 24 ? 0 : s; }; },
        K: function(n) { return function(s, d) { d.h2 = s; }; },
        h: function(n) { return function(s, d) { d.h2 = s == 12 ? 0 : s; }; },
        m: function(n) { return function(s, d) { d.min = s; }; },
        s: function(n) { return function(s, d) { d.s = s; }; },
        S: function(n) { return function(s, d) { d.ms = s; }; }
        // TODO: z and Z
    };
    
    var threshold = new Date();
    var century = Math.floor((threshold.getUTCFullYear() + 20) / 100) * 100;
    
    parseDateTime = function(formatMatch, string) {
        var handlers = [];
        var rex = formatMatch.replace(pregex,
            function(match, pfmt, mfmt, text, quote, escape) {
                if (pfmt) {
                    handlers.push(pfuns[pfmt.charAt(0)](pfmt.length));
                    return prexs[pfmt.charAt(0)](pfmt.length);
                } else if (mfmt) {
                    handlers.push(pfuns[mfmt.charAt(0)](mfmt.length));
                    return mrexs[mfmt.charAt(0)](mfmt.length);
                } else if (text) {
                    return text;
                } else if (quote) {
                    return "'";
                } else if (escape) {
                    return "\\" + escape;
                }
            });
        var match = string.match(new RegExp("^\\s*" + rex + "\\s*$", "i"));
        if (!match) return null;
        var d = { bc: false, century: false, pm: false,
            y: 1970, m: 0, d: 1, h: 0, h2: 0, min: 0, s: 0, ms: 0 };
        for (var i = 0; i < handlers.length; i++)
            handlers[i](match[i + 1], d);
        if (d.century) {
            d.y = Number(d.y) + century;
            var date = new Date(0);
            date.setUTCFullYear(d.y - 20, d.m, d.d);
            date.setUTCHours(d.h, d.min, d.s, d.ms);
            if (date.getTime() > threshold.getTime()) d.y -= 100;
        }
        if (d.bc) d.y = 1 - d.y;
        if (!d.h) d.h = Number(d.h2) + (d.pm ? 12 : 0);
        var date = new Date(0);
        date.setUTCFullYear(d.y, d.m, d.d);
        date.setUTCHours(d.h, d.min, d.s, d.ms);
        return date;
    };

})();

/**
 * Format UTC into human readable date and time formats
 * @function
 * @param {Date} date The date and time as a Date object.
 * @param {String} format A string which selects one of the following predefined
 * formats: <dl>
 * <dt>date</dt><dd>only the date</dd>
 * <dt>time</dt><dd>only the time</dd>
 * <dt>datetime</dt><dd>date and time</dd>
 * <dt>dateday</dt><dd>date with the day of week</dd>
 * <dt>hour</dt><dd>hour (big font) for timescales in calendar views</dd>
 * <dt>suffix</dt><dd>suffix (small font) for timescales in calendar views</dd>
 * <dt>onlyhour</dt><dd>2-digit hour for timescales in team views</dd></dl>
 * @type String
 * @return The formatted string
 * @ignore
 */
var formatDate;

/**
 * Parse human readable date and time formats
 * @function
 * @param {String} string The string to parse
 * @param {String} format A string which selects one of the following predefined
 * formats:<dl>
 * <dt>date</dt><dd>only the date</dd>
 * <dt>time</dt><dd>only the time</dd></dl>
 * @type Date
 * @return The parsed Date object or null in case of errors.
 * @ignore
 */
var parseDateString;

(function() {
    var formats;
    function updateFormats() {
        var date_def = configGetKey("gui.global.region.date.predefined") != 0;
        var time_def = configGetKey("gui.global.region.time.predefined") != 0;
        var date = date_def ? _("yyyy-MM-dd")
                            : configGetKey("gui.global.region.date.format");
        var time = time_def ? _("HH:mm")
                            : configGetKey("gui.global.region.time.format");
        var hour = configGetKey("gui.global.region.time.format_hour");
        var suffix = configGetKey("gui.global.region.time.format_suffix");
        formats = {
            date: date,
            time: time,
            //#. Short date format (month and day only)
            //#. MM is month, dd is day of the month
            shortdate: _("MM/dd"),
            //#. The relative position of date and time.
            //#. %1$s is the date
            //#. %2$s is the time
            //#, c-format
            datetime: format(pgettext("datetime", "%1$s %2$s"), date, time),
            //#. The date with the day of the week.
            //#. EEEE is the full day of the week,
            //#. EEE is the short day of the week,
            //#. %s is the date.
            //#, c-format
            dateday: format(_("EEEE, %s"), date),
            //#. The date with the day of the week.
            //#. EEEE is the full day of the week,
            //#. EEE is the short day of the week,
            //#. %s is the date.
            //#, c-format
            dateshortday: format(_("EEE, %s"), date),
            dateshortdayreverse: format(_("%s, EEE"), date),
            //#. The format for calendar timescales
            //#. when the interval is at least one hour.
            //#. H is 1-24, HH is 01-24, h is 1-12, hh is 01-12, a is AM/PM,
            //#. mm is minutes.
            hour: time_def ? pgettext("dayview", "HH:mm") : hour,
            //#. The format for hours on calendar timescales
            //#. when the interval is less than one hour.
            prefix: time_def ? pgettext("dayview", "HH") : suffix ? "hh" : "HH",
            //#. The format for minutes on calendar timescales
            //#. when the interval is less than one hour.
            //#. 12h formats should use AM/PM ("a").
            //#. 24h formats should use minutes ("mm").
            suffix: time_def ? pgettext("dayview", "mm") : suffix ? "a" : "mm",
            //#. The format for team view timescales
            //#. HH is 01-24, hh is 01-12, H is 1-24, h 1-12, a is AM/PM
            onlyhour: time_def ? pgettext("teamview", "H") : suffix ? "ha" : "H"
        };
    }
    register("LanguageChangedInternal", updateFormats);
    register("OX_Configuration_Changed", updateFormats);
    register("OX_Configuration_Loaded", updateFormats);
    
    formatDate = function(date, format) {
        return formatDateTime(formats[format], new Date(date));
    };    

    parseDateString = function(string, format) {
        return parseDateTime(formats[format || "date"].replace("yyyy","yy"), string);
    };

})();

function formatNumbers(value,format_language) {
	var val;
	if(!format_language) {
		format_language=configGetKey("language");
	}
	switch(format_language) {
		case "en_US":
			return value;
			break;
		default:
			val = String(value).replace(/\./,"\,");
			return val;
			break;
	}
}

function round(val) {
	val = formatNumbers(Math.round(parseFloat(String(val).replace(/\,/,"\.")) * 100) / 100);
	return val;
}

/**
 * Formats an interval as a string
 * @param {Number} t The interval in milliseconds
 * @param {Boolean} until Specifies whether the returned text should be in
 * objective case (if true) or in nominative case (if false).
 * @type String
 * @return The formatted interval.
 */
function getInterval(t, until) {
    function minutes(m) {
        return format(until
            //#. Reminder (objective case): in X minutes
            //#. %d is the number of minutes
            //#, c-format
            ? npgettext("in", "%d minute", "%d minutes", m)
            //#. General duration (nominative case): X minutes
            //#. %d is the number of minutes
            //#, c-format
            :  ngettext("%d minute", "%d minutes", m),
            m);
    }
    function get_h(h) {
        return format(until
            //#. Reminder (objective case): in X hours
            //#. %d is the number of hours
            //#, c-format
            ? npgettext("in", "%d hour", "%d hours", h)
            //#. General duration (nominative case): X hours
            //#. %d is the number of hours
            //#, c-format
            :  ngettext(      "%d hour", "%d hours", h),
            h);
    }
    function get_hm(h, m) {
        return format(until
            //#. Reminder (objective case): in X hours and Y minutes
            //#. %1$d is the number of hours
            //#. %2$s is the text for the remainder of the last hour
            //#, c-format
            ? npgettext("in", "%1$d hour and %2$s", "%1$d hours and %2$s", h)
            //#. General duration (nominative case): X hours and Y minutes
            //#. %1$d is the number of hours
            //#. %2$s is the text for the remainder of the last hour
            //#, c-format
            :  ngettext("%1$d hour and %2$s", "%1$d hours and %2$s", h),
            h, minutes(m));
    }
    function hours(t) {
        if (t < 60) return minutes(t); // min/h
        var h = Math.floor(t / 60);
        var m = t % 60;
        return m ? get_hm(h, m) : get_h(h);
    }
    function get_d(d) {
        return format(until
            //#. Reminder (objective case): in X days
            //#. %d is the number of days
            //#, c-format
            ? npgettext("in", "%d day", "%d days", d)
            //#. General duration (nominative case): X days
            //#. %d is the number of days
            //#, c-format
            : ngettext("%d day", "%d days", d),
            d);
    }
    function get_dhm(d, t) {
        return format(until
            //#. Reminder (objective case): in X days, Y hours and Z minutes
            //#. %1$d is the number of days
            //#. %2$s is the text for the remainder of the last day
            //#, c-format
            ? npgettext("in", "%1$d day, %2$s", "%1$d days, %2$s", d)
            //#. General duration (nominative case): X days, Y hours and Z minutes
            //#. %1$d is the number of days
            //#. %2$s is the text for the remainder of the last day
            //#, c-format
            : ngettext("%1$d day, %2$s", "%1$d days, %2$s", d),
            d, hours(t));
    }
    function days(t) {
        if (t < 1440) return hours(t); // min/day
        var d = Math.floor(t / 1440);
        t = t % 1440;
        return t ? get_dhm(d, t) : get_d(d); 
    }
    function get_w(w) {
        return format(until
            //#. Reminder (objective case): in X weeks
            //#. %d is the number of weeks
            //#, c-format
            ? npgettext("in", "%d week", "%d weeks", w)
            //#. General duration (nominative case): X weeks
            //#. %d is the number of weeks
            //#, c-format
            : ngettext("%d week", "%d weeks", w),
            w);
    }

    t = Math.round(t / 60000); // ms/min
	if (t >= 10080 && t % 10080 == 0) { // min/week
        return get_w(Math.round(t / 10080));
	} else {
        return days(t);
    }
}

var currencies = [
            { iso: "CAD", name: "Canadian dollar", isoLangCodes: [ "CA" ] },
            { iso: "CHF", name: "Swiss franc", isoLangCodes: [ "CH" ] },
            { iso: "DKK", name: "Danish krone", isoLangCodes: [ "DK" ] },
            { iso: "EUR", name: "Euro", isoLangCodes: [ "AT", "BE", "CY", "FI", "FR", "DE", "GR", "IE", "IT", "LU", "MT", "NL", "PT", "SI", "ES" ] },
            { iso: "GBP", name: "Pound sterling", isoLangCodes: [ "GB" ] },
            { iso: "PLN", name: "Zloty", isoLangCodes: [ "PL" ] },
            { iso: "RUB", name: "Russian rouble", isoLangCodes: [ "RU" ] },
            { iso: "SEK", name: "Swedish krona", isoLangCodes: [ "SE" ] },
            { iso: "USD", name: "US dollar", isoLangCodes: [ "US" ] },
            { iso: "JPY", name: "Japanese Yen", isoLangCodes: [ "JP" ] }
        ];    
