1 /**
  2  * 
  3  * All content on this website (including text, images, source
  4  * code and any other original works), unless otherwise noted,
  5  * is licensed under a Creative Commons License.
  6  * 
  7  * http://creativecommons.org/licenses/by-nc-sa/2.5/
  8  * 
  9  * Copyright (C) 2004-2010 Open-Xchange, Inc.
 10  * Mail: info@open-xchange.com 
 11  * 
 12  * @author Viktor Pracht <viktor.pracht@open-xchange.com>
 13  * 
 14  */
 15 
 16 /**
 17  * Translates a string
 18  * @function
 19  * @param {String} text The original English text to translate.
 20  * @type String
 21  * @return The translated text.
 22  * @ignore
 23  */
 24 var _;
 25 
 26 /**
 27  * Translates a string
 28  * @function
 29  * @param {String} text The original English text to translate.
 30  * @type String
 31  * @return The translated text.
 32  * @ignore
 33  */
 34 var gettext;
 35 
 36 /**
 37  * Translates a string
 38  * @function
 39  * @param {String} context A context to differentiate multiple identical texts
 40  * with different translations.
 41  * @param {String} text The original English text to translate.
 42  * @type String
 43  * @return The translated text.
 44  * @ignore
 45  */
 46 var pgettext;
 47 
 48 /**
 49  * Translates a string
 50  * @function
 51  * @param {String} domain An i18n domain to use for the translation.
 52  * @param {String} context A context to differentiate multiple identical texts
 53  * with different translations.
 54  * @param {String} text The original English text to translate.
 55  * @type String
 56  * @return The translated text.
 57  */
 58 var dpgettext;
 59 
 60 /**
 61  * Translates a string containing numbers.
 62  * @function
 63  * @param {String} singular The original English text for the singular form.
 64  * @param {String} plural The original English text for the plural form.
 65  * @param {Number} n The number which determines which text form is used.
 66  * @param {String} context An optional context to differentiate multiple
 67  * identical texts with different translations.
 68  * @param {String} domain An optional i18n domain to use for the translation.
 69  * @type String
 70  * @return The translated text.
 71  * @ignore
 72  */
 73 var ngettext;
 74 
 75 /**
 76  * Translates a string containing numbers.
 77  * @function
 78  * @param {String} context A context to differentiate multiple identical texts
 79  * with different translations.
 80  * @param {String} singular The original English text for the singular form.
 81  * @param {String} plural The original English text for the plural form.
 82  * @param {Number} n The number which determines which text form is used.
 83  * @type String
 84  * @return The translated text.
 85  * @ignore
 86  */
 87 var npgettext;
 88 
 89 /**
 90  * Translates a string containing numbers.
 91  * @function
 92  * @param {String} domain An i18n domain to use for the translation.
 93  * @param {String} context A context to differentiate multiple identical texts
 94  * with different translations.
 95  * @param {String} singular The original English text for the singular form.
 96  * @param {String} plural The original English text for the plural form.
 97  * @param {Number} n The number which determines which text form is used.
 98  * @type String
 99  * @return The translated text.
100  */
101 var dnpgettext;
102 
103 /**
104  * Adds a new i18n domain, usually for a plugin.
105  * @function
106  * @param {String} domain A new domain name, usually the plugin name.
107  * @param {String} pattern A Pattern which is used to find the PO or JS file on
108  * the server. The pattern is processed by formatting it with the language ID
109  * as the only parameter. The formatted result is used to download the file
110  * from the server.
111  */
112 var bindtextdomain;
113 
114 /**
115  * Changes the current language which is used for all subsequent translations.
116  * Also translates all currently displayed strings.
117  * @function
118  * @param {String} name The ID of the new language.
119  */
120 var setLanguage;
121 
122 /**
123  * Returns the translation dictionary for the specified language.
124  * @private
125  * @function
126  * @param {String} name The language ID of the dictionary to return.
127  * @type Object
128  * @return The translation dictionary of the specified language.
129  */
130 var getDictionary;
131 
132 /**
133  * Formats a string by replacing printf-style format specifiers in the string
134  * with dynamic parameters. Flags, width, precision and length modifiers are
135  * not supported. All type conversions are performed by the standard toString()
136  * JavaScript method.
137  * @param {I18nString} string The format string.
138  * @param params Either an array with parameters or multiple separate
139  * parameters.
140  * @type I18nString
141  * @return The formatted string.
142  */
143 function format(string, params) {
144 	var param_array = params;
145 	if (typeof(params) != "object") {
146 		param_array = new Array(arguments.length - 1);
147 		for (var i = 1; i < arguments.length; i++)
148 			param_array[i - 1] = arguments[i];
149 	}
150     if (typeof string == "function") {
151         return function() { return formatRaw(string(), param_array); };
152     } else {
153         return formatRaw(string, param_array);
154     }
155 }
156 
157 /**
158  * Formats a string by replacing printf-style format specifiers in the string
159  * with dynamic parameters. Flags, width, precision and length modifiers are
160  * not supported. All type conversions (except from I18nString) are performed
161  * by the standard toString() JavaScript method.
162  * @param {String} string The format string.
163  * @param params An array with parameters.
164  * @type String
165  * @return The formatted string.
166  * @ignore
167  */
168 function formatRaw(string, params) {
169     var index = 0;
170     return String(string).replace(/%(([0-9]+)\$)?[A-Za-z]/g,
171         function(match, pos, n) {
172             if (pos) index = n - 1;
173             return params[index++];
174         }).replace(/%%/, "%");
175 }
176 
177 /**
178  * Formats and translates an error returned by the server.
179  * @param {Object} result the JSON object as passed to a JSON callback function.
180  * @param {String} formatString an optional format string with the replacement
181  * parameters <dl><dt>%1$s</dt><dd>the error code,</dd>
182  * <dt>%2$s</dt><dd>the fomratter error message,</dd>
183  * <dt>%3$s</dt><dd>the unique error ID.</dd></dl>
184  * @type String
185  * @returns the formatted and translated error message.
186  * @ignore
187  */
188 function formatError(result, formatString) {
189 	//#. %1$s is the error code.
190 	//#. %2$s is the formatted error message.
191     //#. %3$s is the unique error ID.
192 	//#, c-format
193 	return format(formatString || _("Error: %2$s (%1$s, %3$s)"), result.code,
194                   format(_(result.error), result.error_params),
195                   result.error_id);
196 }
197 
198 (function() {
199     var current, current_lang;
200     var domains = { "": "lang/%s.js" };
201     var languages = {};
202     var originals = {};
203 	var counter = 0;
204 
205 	_ = gettext = function(text) { return dpgettext("", "", text); };
206     
207     pgettext = function(context, text) { return dpgettext("", context, text); };
208     
209     dpgettext = function(domain, context, text) {
210         var c = current && current[domain || ""];
211         var key = context ? context + "\0" + text : text;
212         return c && c.dictionary[key] || text;
213     };
214     
215     ngettext = function(singular, plural, n) {
216         return dnpgettext("", "", singular, plural, n);
217     };
218 
219 	npgettext = function(context, singular, plural, n) {
220         return dnpgettext("", context, singular, plural, n);
221     };
222     
223     dnpgettext = function(domain, context, singular, plural, n) {
224 		var text = n != 1 ? plural : singular;
225         var c = current && current[domain || ""];
226         if (!c) return text;
227         var key = context ? [context, "\0", singular, "\x01", plural].join("")
228                           : [               singular, "\x01", plural].join("");
229 		var translation = c.dictionary[key];
230 		if (!translation) return text;
231 		return translation[Number(c.plural(n))] || text;
232 	};
233 
234     function parse(pattern, file) {
235         if (pattern.substring(pattern.length - 2) == "po") {
236             return parsePO(file);
237         } else {
238             return (new Function("return " + file))();
239         }
240     }
241     
242     bindtextdomain = function(domain, pattern) {
243         domains[domain] = pattern;
244         if (languages[current_lang] === current) setLanguage(current_lang);
245     };
246 
247 	setLanguage = function(name) {
248         current_lang = name;
249         var new_lang = languages[name];
250 		if (!new_lang) {
251             loadLanguage(name);
252             return;
253         }
254         for (var i in domains) {
255             if (!(i in new_lang)) {
256                 loadLanguage(name);
257                 return;
258             }
259         }
260 		current = new_lang;
261 		for (var i in init.i18n) {
262 			var attrs = init.i18n[i].split(",");
263 			var node = $(i);
264 			if(node) {
265 				for (var j = 0; j < attrs.length; j++) {
266 					var attr = attrs[j];
267 					var id = attr + "," + i;
268 					var text = attr ? node.getAttributeNode(attr)
269 					                : node.firstChild;
270                     var val = text && String(text.nodeValue);
271 					if (!val || val == "\xa0" )
272                         alert(format('Invalid i18n for id="%s"', i));
273 					var original = originals[id];
274 					if (!original) original = originals[id] = val;
275                     var context = "";
276                     var pipe = original.indexOf("|");
277                     if (pipe >= 0) {
278                         context = original.substring(0, pipe);
279                         original = original.substring(pipe + 1);
280                     }
281 					text.nodeValue = dpgettext("", context, original);
282 				}
283 			}
284 		}
285         triggerEvent("LanguageChangedInternal");
286 		triggerEvent("LanguageChanged");
287     };
288     
289     function loadLanguage(name) {
290 		// check the main window
291         if (corewindow != window) {
292             var core_dict = corewindow.getDictionary(name);
293             if (core_dict) {
294     			current = languages[name] = core_dict;
295                 setLanguage(name);
296                 return;
297     		}
298         }
299         var curr = languages[name];
300         if (!curr) curr = languages[name] = {};
301         var join = new Join(function() { setLanguage(name); });
302         for (var d in domains) {
303             if (!(d in curr)) {
304                 (new JSON()).get(format(domains[d], name), null,
305                     join.add((function(domain) {
306                         return function(file) {
307                             try {
308                                 languages[name][domain] =
309                                     parse(domains[domain], file);
310                             } catch (e) {
311                                 triggerEvent("OX_New_Error", 4, e);
312                                 join.add(); // prevent setLanguage()
313                             }
314                         };
315                     })(d)), join.alt((function(domain) {
316                         return function(result, status) {
317                             languages[name][domain] = false;
318                             return status == 404;
319                         };
320                     })(d)), true);
321             }
322 		}
323 	}
324 	
325 	getDictionary = function(name) { return languages[name]; };	
326 
327 })();
328 
329 function parsePO(file) {
330     parsePO.tokenizer.lastIndex = 0;
331     var line_no = 1;
332     
333     function next() {
334         while (parsePO.tokenizer.lastIndex < file.length) {
335             var t = parsePO.tokenizer.exec(file);
336             if (t[1]) continue;
337             if (t[2]) {
338                 line_no++;
339                 continue;
340             }
341             if (t[3]) return t[3];
342             if (t[4]) return t[4];
343             if (t[5]) throw new Error(format(
344                 "Invalid character in line %s.", line_no));
345         }
346     }
347 
348     var lookahead = next();
349 
350     function clause(name, optional) {
351         if (lookahead == name) {
352             lookahead = next();
353             var parts = [];
354             while (lookahead && lookahead.charAt(0) == '"') {
355                 parts.push((new Function("return " + lookahead))());
356                 lookahead = next();
357             }
358             return parts.join("");
359         } else if (!optional) {
360             throw new Error(format(
361                 "Unexpected '%1$s' in line %3$s, expected '%2$s'.",
362                 lookahead, name, line_no));
363         }
364     }
365     
366     if (clause("msgid") != "") throw new Error("Missing PO file header");
367     var header = clause("msgstr");
368     if (!parsePO.headerRegExp.exec(header))
369         throw new Error("Error in PO file header");
370     var po = (new Function("return " + header.replace(parsePO.headerRegExp,
371         "{ nplurals: $1, plural: function(n) { return $2; }, dictionary: {} }"
372         )))();
373     while (lookahead) {
374         var ctx = clause("msgctxt", true);
375         var id = clause("msgid");
376         var id_plural = clause("msgid_plural", true);
377         var str;
378         if (id_plural !== undefined) {
379             id = id += "\x01" + id_plural;
380             str = {};
381             for (var i = 0; i < po.nplurals; i++) {
382                 str[i] = clause("msgstr[" + i + "]");
383             }
384         } else {
385             str = clause("msgstr");
386         }
387         if (ctx) id = ctx + "\0" + id;
388         po.dictionary[id] = str;
389     }
390     return po;
391 }
392 
393 parsePO.tokenizer = new RegExp(
394     '^(#.*|[ \\t\\v\\f]+)$' +                  // comment or empty line
395     '|(\\r\\n|\\r|\\n)' +                      // linebreak (for line numbering)
396     '|^(msg[\\[\\]\\w]+)(?:$|[ \\t\\v\\f]+)' + // keyword
397     '|[ \\t\\v\\f]*(".*")\\s*$' +              // string
398     '|(.)',                                    // anything else is an error
399     "gm");
400 
401 parsePO.headerRegExp = new RegExp(
402     '^(?:[\\0-\\uffff]*\\n)?' +                         // ignored prefix
403     'Plural-Forms:\\s*nplurals\\s*=\\s*([0-9]+)\\s*;' + // nplurals
404                  '\\s*plural\\s*=\\s*([^;]*);' +        // plural
405     '[\\0-\\uffff]*$'                                   // ignored suffix
406 );
407 
408 /**
409  * Encapsulation of a single translated text node which is created at runtime.
410  * @param {Function} callback A function which is called as a method of
411  * the created object and returns the current translated text.
412  * @param {Object} template An optional object which is used for the initial
413  * translation. All enumerable properties of the template will be copied to
414  * the newly created object before the first call to callback.
415  *
416  * Fields of the created object:
417  *
418  * node: The DOM text node which is automatically translated.
419  * @ignore
420  */
421 function I18nNode(callback, template) {
422     if (template) for (var i in template) this[i] = template[i];
423     if (typeof callback != "function") {
424         if (debug) {
425             (console.warn || console.log || alert)(format(
426                 "The string \"%s\" is not internationalized!", callback));
427             debugger;
428         }
429         this.callback = function() { return _(callback); };
430     } else {
431         this.callback = callback;
432     }
433     this.index = ++I18nNode.counter;
434 	this.node = document.createTextNode(this.callback());
435 	this.enable();
436 }
437 
438 I18nNode.prototype = {
439 	/**
440 	 * Updates the node contents. Is called whenever the current language
441 	 * changes and should be also called when the displayed value changes.
442 	 * @ignore
443      */
444 	update: function() {
445         if (typeof this.callback != "function") {
446             console.error(format(
447                 "The callback \"%s\" has type \"%s\".",
448                 this.callback, typeof this.callback));
449         } else {
450 /**#nocode+*/
451             this.node.data = this.callback();
452 /**#nocode-*/
453         }
454     },
455 	
456 	/**
457 	 * Disables automatic updates for this object.
458 	 * Should be called when the text node is removed from the DOM tree.
459      * @ignore
460 	 */
461 	disable: function() { delete I18nNode.nodes[this.index]; },
462 	
463 	/**
464 	 * Reenables previously disabled updates.
465      * @ignore
466 	 */
467  	enable: function() { I18nNode.nodes[this.index] = this; }
468 };
469 
470 I18nNode.nodes = {};
471 I18nNode.counter = 0;
472 
473 register("LanguageChanged", function() {
474 	for (var i in I18nNode.nodes) I18nNode.nodes[i].update();
475 });
476 
477 /**
478  * Creates an automatically updated node from a static text. The node can not
479  * be removed.
480  * @param {I18nString} text The text to be translated. It must be marked with
481  * the <code>9*i18n*9</code> comment.
482  * @param {String} context An optional context to differentiate multiple
483  * identical texts with different translations. It must be marked with
484  * the <code>9*i18n context*9</code> comment.
485  * @param {String} domain An optional i18n domain to use for the translation.
486  * @type Object
487  * @return The new DOM text node.
488  * @ignore
489  */
490 function addTranslated(text, context, domain) {
491 	return (new I18nNode(typeof text == "function" ? text
492         : function() { return dpgettext(domain, context, text); })).node;
493 }
494 
495 /**
496  * Returns whether a date is today.
497  * @param utc The date. Any valid parameter to new Date() will do.
498  * @type Boolean
499  * @return true if the parameter has today's date, false otherwise.
500  * @ignore
501  */
502 function isToday(utc) {
503     var today = new Date(now());
504     today.setUTCHours(0, 0, 0, 0);
505     var diff = (new Date(utc)).getTime() - today.getTime();
506     return diff >= 0 && diff < 864e5; // ms/day
507 }
508 
509 /**
510  * The first week with at least daysInFirstWeek days in a given year is defined
511  * as the first week of that year.
512  * @ignore
513  */
514 var daysInFirstWeek = 4;
515 
516 /**
517  * First day of the week.
518  * 0 = Sunday, 1 = Monday and so on.
519  * @ignore
520  */
521 var weekStart = 1;
522 
523 function getDays(d) { return Math.floor(d / 864e5); }
524 
525 /**
526  * Computes the week number of the specified Date object, taking into account
527  * daysInFirstWeek and weekStart.
528  * @param {Date} d The date for which to calculate the week number.
529  * @param {Boolean} inMonth True to compute the week number in a month,
530  * False for the week number in a year 
531  * @type Number
532  * @return Week number of the specified date.
533  * @ignore
534  */
535 function getWeek(d, inMonth) {
536 	var keyDay = getKeyDayOfWeek(d);
537 	var keyDate = new Date(keyDay * 864e5);
538 	var jan1st = Date.UTC(keyDate.getUTCFullYear(),
539 	                      inMonth ? keyDate.getUTCMonth() : 0);
540 	return Math.floor((keyDay - getDays(jan1st)) / 7) + 1;
541 }
542  
543 /**
544  * Returns the day of the week which decides the week number
545  * @return Day of week
546  */
547 function getKeyDayOfWeek(d) {
548 	var firstDay = getDayInSameWeek(d, weekStart);
549 	return (firstDay + 7 - daysInFirstWeek);
550 }
551 
552 /**
553  * Computes the number of the first day of the specified week, taking into
554  * account weekStart.
555  * @param  {Date} d The date for which to calculate the first day of week number.
556  * type Number
557  * @return First day in the week as the number of days since 1970-01-01.
558  * @ignore
559  */
560 function getDayInSameWeek(d, dayInWeek) {
561 	return getDays(d.getTime()) - (d.getUTCDay() - dayInWeek + 7) % 7; 
562 }
563 
564 /**
565  * Formats a Date object according to a format string.
566  * @function
567  * @param {String} format The format string. It has the same syntax as Java's
568  * java.text.SimpleDateFormat, assuming a Gregorian calendar.
569  * @param {Date} date The Date object to format. It must contain a Time value as
570  * defined in the HTTP API specification.
571  * @type String
572  * @return The formatted date and/or time.
573  */
574 var formatDateTime;
575 
576 /**
577  * Parses a date and time according to a format string.
578  * @function
579  * @param {String} format The format string. It has the same syntax as Java's
580  * java.text.SimpleDateFormat, assuming a Gregorian calendar.
581  * @param {String} string The string to parse.
582  * @type Date
583  * @return The parsed date as a Date object. It will contain a Time value as
584  * defined in the HTTP API specification.
585  */
586 var parseDateTime;
587 
588 /**
589  * An array with translated week day names.
590  * @ignore
591  */
592 var weekdays = [];
593 
594 (function() {
595 
596     var regex = /(G+|y+|M+|w+|W+|D+|d+|F+|E+|a+|H+|k+|K+|h+|m+|s+|S+|z+|Z+)|\'(.+?)\'|(\'\')/g;
597 
598 	function num(n, x) {
599 		var s = x.toString();
600 		n -= s.length;
601 		if (n <= 0) return s;
602 		var a = new Array(n);
603 		for (var i = 0; i < n; i++) a[i] = "0";
604 		a[n] = s;
605 		return a.join("");
606 	}
607 	function text(n, full, shrt) {
608 		return n >= 4 ? _(full) : _(shrt);
609 	}
610 	var months = [
611 		"January"/*i18n*/, "February"/*i18n*/,     "March"/*i18n*/,
612 		  "April"/*i18n*/,      "May"/*i18n*/,      "June"/*i18n*/,
613 		   "July"/*i18n*/,   "August"/*i18n*/, "September"/*i18n*/,
614 		"October"/*i18n*/, "November"/*i18n*/,  "December"/*i18n*/
615 	];
616 	var shortMonths = [
617 		"Jan"/*i18n*/, "Feb"/*i18n*/, "Mar"/*i18n*/, "Apr"/*i18n*/,
618 		"May"/*i18n*/, "Jun"/*i18n*/, "Jul"/*i18n*/, "Aug"/*i18n*/,
619 		"Sep"/*i18n*/, "Oct"/*i18n*/, "Nov"/*i18n*/, "Dec"/*i18n*/
620 	];
621 	var days = weekdays.untranslated = [
622 		   "Sunday"/*i18n*/,   "Monday"/*i18n*/, "Tuesday"/*i18n*/,
623 		"Wednesday"/*i18n*/, "Thursday"/*i18n*/,  "Friday"/*i18n*/,
624 		 "Saturday"/*i18n*/
625 	];
626 	var shortDays = [
627 		"Sun"/*i18n*/, "Mon"/*i18n*/, "Tue"/*i18n*/, "Wed"/*i18n*/,
628 		"Thu"/*i18n*/, "Fri"/*i18n*/, "Sat"/*i18n*/
629 	];
630 	var funs = {
631 		G: function(n, d) {
632 			return d.getTime() < -62135596800000 ? _("BC") : _("AD");
633 		},
634 		y: function(n, d) {
635 			var y = d.getUTCFullYear();
636 			if (y < 1) y = 1 - y;
637 			return num(n, n == 2 ? y % 100 : y);
638 		},
639 		M: function(n, d) {
640 			var m = d.getUTCMonth();
641 			if (n >= 3) {
642 				return text(n, months[m], shortMonths[m]);
643 			} else {
644 				return num(n, m + 1);
645 			}
646 		},
647 		w: function(n, d) { return num(n, getWeek(d)); },
648 		W: function(n, d) { return num(n, getWeek(d, true)); },
649 		D: function(n, d) {
650 			return num(n,
651 				getDays(d.getTime() - Date.UTC(d.getUTCFullYear(), 0)) + 1);
652 		},
653 		d: function(n, d) { return num(n, d.getUTCDate()); },
654 		F: function(n, d) {
655 			return num(n, Math.floor(d.getUTCDate() / 7) + 1);
656 		},
657 		E: function(n, d) {
658 			var m = d.getUTCDay();
659 			return text(n, days[m], shortDays[m]);
660 		},
661 		a: function(n, d) {
662             return d.getUTCHours() < 12 ? _("AM") : _("PM");
663         },
664 		H: function(n, d) { return num(n, d.getUTCHours()); },
665 		k: function(n, d) { return num(n, d.getUTCHours() || 24); },
666 		K: function(n, d) { return num(n, d.getUTCHours() % 12); },
667 		h: function(n, d) { return num(n, d.getUTCHours() % 12 || 12); },
668 		m: function(n, d) { return num(n, d.getUTCMinutes()); },
669 		s: function(n, d) { return num(n, d.getUTCSeconds()); },
670 		S: function(n, d) { return num(n, d.getMilliseconds()); }
671 		// TODO: z and Z 
672 	};
673 	formatDateTime = function(format, date) {
674 		return format.replace(regex,
675 			function(match, fmt, text, quote) {
676 				if (fmt) {
677 					return funs[fmt.charAt(0)](fmt.length, date);
678 				} else if (text) {
679 					return text;
680 				} else if (quote) {
681 					return "'";
682 				}
683 			});
684 	};
685     
686     var f = "G+|y+|M+|w+|W+|D+|d+|F+|E+|a+|H+|k+|K+|h+|m+|s+|S+|z+|Z+";
687     var pregexStr = "(" + f + ")(?!" + f + ")|(" + f + ")(?=" + f +
688         ")|\'(.+?)\'|(\'\')|([$^\\\\.*+?()[\\]{}|])";
689     var pregex = new RegExp(pregexStr, "g");
690     
691     var monthRegex;
692     var monthMap;
693     function recreateMaps() {
694         var names = months.concat(shortMonths);
695         for (var i = 0; i < names.length; i++) names[i] = escape(_(names[i]));
696         monthRegex = "(" + names.join("|") + ")";
697         monthMap = {};
698         for (var i = 0; i < months.length; i++) {
699             monthMap[_(months[i])] = i;
700             monthMap[_(shortMonths[i])] = i;
701         }
702         weekdays.length = days.length;
703         for (var i = 0; i < days.length; i++) weekdays[i] = _(days[i]);
704     }
705     recreateMaps();
706     register("LanguageChangedInternal", recreateMaps);
707     
708     function escape(rex) {
709         return rex.replace(/[$^\\.*+?()[\]{}|]/g, "\\$");
710     }
711 
712     var numRex = "([+-]?\\d+)";
713     function number(n) { return numRex; }
714         
715     var prexs = {
716         G: function(n) {
717             return "(" + escape(_("BC")) + "|" + escape(_("AD")) + ")";
718         },
719         y: number,
720         M: function(n) { return n >= 3 ? monthRegex : numRex; },
721         w: number, W: number, D: number, d: number, F: number, E: number,
722         a: function(n) {
723             return "(" + escape(_("AM")) + "|" + escape(_("PM")) + ")";
724         },
725         H: number, k: number, K: number, h: number, m: number, s: number,
726         S: number
727         // TODO: z and Z
728     };
729     
730     function mnum(n) {
731         return n > 1 ? "([+-]\\d{1," + (n - 1) + "}|\\d{1," + n + "})"
732                      :                           "(\\d{1," + n + "})"; }
733     
734     var mrexs = {
735         G: prexs.G, y: mnum,
736         M: function(n) { return n >= 3 ? monthRegex : mnum(n); },
737         w: mnum, W: mnum, D: mnum, d: mnum, F: mnum, E: prexs.E, a: prexs.a,
738         H: mnum, k: mnum, K: mnum, h: mnum, m: mnum, s: mnum, S: mnum
739         // TODO: z and Z
740     };
741     
742     var pfuns = {
743         G: function(n) { return function(s, d) { d.bc = s == _("BC"); }; },
744         y: function(n) {
745             return function(s, d) {
746                 d.century = n <= 2 && s.match(/^\d\d$/);
747                 d.y = s;
748             };
749         },
750         M: function(n) {
751             return n >= 3 ? function (s, d) { d.m = monthMap[s]; }
752                           : function(s, d) { d.m = s - 1; };
753         },
754         w: emptyFunction, W: emptyFunction, D: emptyFunction,
755         d: function(n) { return function(s, d) { d.d = s }; },
756         F: emptyFunction, E: emptyFunction,
757         a: function(n) { return function(s, d) { d.pm = s == _("PM"); }; },
758         H: function(n) { return function(s, d) { d.h = s; }; },
759         k: function(n) { return function(s, d) { d.h = s == 24 ? 0 : s; }; },
760         K: function(n) { return function(s, d) { d.h2 = s; }; },
761         h: function(n) { return function(s, d) { d.h2 = s == 12 ? 0 : s; }; },
762         m: function(n) { return function(s, d) { d.min = s; }; },
763         s: function(n) { return function(s, d) { d.s = s; }; },
764         S: function(n) { return function(s, d) { d.ms = s; }; }
765         // TODO: z and Z
766     };
767     
768     var threshold = new Date();
769     var century = Math.floor((threshold.getUTCFullYear() + 20) / 100) * 100;
770     
771     parseDateTime = function(formatMatch, string) {
772         var handlers = [];
773         var rex = formatMatch.replace(pregex,
774             function(match, pfmt, mfmt, text, quote, escape) {
775                 if (pfmt) {
776                     handlers.push(pfuns[pfmt.charAt(0)](pfmt.length));
777                     return prexs[pfmt.charAt(0)](pfmt.length);
778                 } else if (mfmt) {
779                     handlers.push(pfuns[mfmt.charAt(0)](mfmt.length));
780                     return mrexs[mfmt.charAt(0)](mfmt.length);
781                 } else if (text) {
782                     return text;
783                 } else if (quote) {
784                     return "'";
785                 } else if (escape) {
786                     return "\\" + escape;
787                 }
788             });
789         var match = string.match(new RegExp("^\\s*" + rex + "\\s*$", "i"));
790         if (!match) return null;
791         var d = { bc: false, century: false, pm: false,
792             y: 1970, m: 0, d: 1, h: 0, h2: 0, min: 0, s: 0, ms: 0 };
793         for (var i = 0; i < handlers.length; i++)
794             handlers[i](match[i + 1], d);
795         if (d.century) {
796             d.y = Number(d.y) + century;
797             var date = new Date(0);
798             date.setUTCFullYear(d.y - 20, d.m, d.d);
799             date.setUTCHours(d.h, d.min, d.s, d.ms);
800             if (date.getTime() > threshold.getTime()) d.y -= 100;
801         }
802         if (d.bc) d.y = 1 - d.y;
803         if (!d.h) d.h = Number(d.h2) + (d.pm ? 12 : 0);
804         var date = new Date(0);
805         date.setUTCFullYear(d.y, d.m, d.d);
806         date.setUTCHours(d.h, d.min, d.s, d.ms);
807         return date;
808     };
809 
810 })();
811 
812 /**
813  * Format UTC into human readable date and time formats
814  * @function
815  * @param {Date} date The date and time as a Date object.
816  * @param {String} format A string which selects one of the following predefined
817  * formats: <dl>
818  * <dt>date</dt><dd>only the date</dd>
819  * <dt>time</dt><dd>only the time</dd>
820  * <dt>datetime</dt><dd>date and time</dd>
821  * <dt>dateday</dt><dd>date with the day of week</dd>
822  * <dt>hour</dt><dd>hour (big font) for timescales in calendar views</dd>
823  * <dt>suffix</dt><dd>suffix (small font) for timescales in calendar views</dd>
824  * <dt>onlyhour</dt><dd>2-digit hour for timescales in team views</dd></dl>
825  * @type String
826  * @return The formatted string
827  * @ignore
828  */
829 var formatDate;
830 
831 /**
832  * Parse human readable date and time formats
833  * @function
834  * @param {String} string The string to parse
835  * @param {String} format A string which selects one of the following predefined
836  * formats:<dl>
837  * <dt>date</dt><dd>only the date</dd>
838  * <dt>time</dt><dd>only the time</dd></dl>
839  * @type Date
840  * @return The parsed Date object or null in case of errors.
841  * @ignore
842  */
843 var parseDateString;
844 
845 (function() {
846     var formats;
847     function updateFormats() {
848         var date_def = configGetKey("gui.global.region.date.predefined") != 0;
849         var time_def = configGetKey("gui.global.region.time.predefined") != 0;
850         var date = date_def ? _("yyyy-MM-dd")
851                             : configGetKey("gui.global.region.date.format");
852         var time = time_def ? _("HH:mm")
853                             : configGetKey("gui.global.region.time.format");
854         var hour = configGetKey("gui.global.region.time.format_hour");
855         var suffix = configGetKey("gui.global.region.time.format_suffix");
856         formats = {
857             date: date,
858             time: time,
859             //#. Short date format (month and day only)
860             //#. MM is month, dd is day of the month
861             shortdate: _("MM/dd"),
862             //#. The relative position of date and time.
863             //#. %1$s is the date
864             //#. %2$s is the time
865             datetime: format(pgettext("datetime", "%1$s %2$s"), date, time),
866             //#. The date with the day of the week.
867             //#. EEEE is the full day of the week,
868             //#. EEE is the short day of the week,
869             //#. %s is the date.
870             dateday: format(_("EEEE, %s"), date),
871             //#. The date with the day of the week.
872             //#. EEEE is the full day of the week,
873             //#. EEE is the short day of the week,
874             //#. %s is the date.
875             dateshortday: format(_("EEE, %s"), date),
876             //#. The format for calendar timescales
877             //#. when the interval is at least one hour.
878             //#. H is 1-24, HH is 01-24, h is 1-12, hh is 01-12, a is AM/PM,
879             //#. mm is minutes.
880             hour: time_def ? pgettext("dayview", "HH:mm") : hour,
881             //#. The format for hours on calendar timescales
882             //#. when the interval is less than one hour.
883             prefix: time_def ? pgettext("dayview", "HH") : suffix ? "hh" : "HH",
884             //#. The format for minutes on calendar timescales
885             //#. when the interval is less than one hour.
886             //#. 12h formats should use AM/PM ("a").
887             //#. 24h formats should use minutes ("mm").
888             suffix: time_def ? pgettext("dayview", "mm") : suffix ? "a" : "mm",
889             //#. The format for team view timescales
890             //#. HH is 01-24, hh is 01-12, H is 1-24, h 1-12, a is AM/PM
891             onlyhour: time_def ? pgettext("teamview", "H") : suffix ? "ha" : "H"
892         };
893     }
894     register("LanguageChangedInternal", updateFormats);
895     register("OX_Configuration_Changed", updateFormats);
896     register("OX_Configuration_Loaded", updateFormats);
897     
898     formatDate = function(date, format) {
899         return formatDateTime(formats[format], new Date(date));
900     };    
901 
902     parseDateString = function(string, format) {
903         return parseDateTime(formats[format || "date"].replace("yyyy","yy"), string);
904     };
905 
906 })();
907 
908 function formatNumbers(value,format_language) {
909 	var val;
910 	if(!format_language) {
911 		format_language=configGetKey("language");
912 	}
913 	switch(format_language) {
914 		case "en_US":
915 			return value;
916 			break;
917 		default:
918 			val = String(value).replace(/\./,"\,");
919 			return val;
920 			break;
921 	}
922 }
923 
924 function round(val) {
925 	val = formatNumbers(Math.round(parseFloat(String(val).replace(/\,/,"\.")) * 100) / 100);
926 	return val;
927 }
928 
929 /**
930  * Formats an interval as a string
931  * @param {Number} t The interval in milliseconds
932  * @param {Boolean} until Specifies whether the returned text should be in
933  * objective case (if true) or in nominative case (if false).
934  * @type String
935  * @return The formatted interval.
936  */
937 function getInterval(t, until) {
938     function minutes(m) {
939         return format(until
940             //#. Reminder (objective case): in X minutes
941             //#. %d is the number of minutes
942             //#, c-format
943             ? npgettext("in", "%d minute", "%d minutes", m)
944             //#. General duration (nominative case): X minutes
945             //#. %d is the number of minutes
946             //#, c-format
947             :  ngettext("%d minute", "%d minutes", m),
948             m);
949     }
950     function get_h(h) {
951         return format(until
952             //#. Reminder (objective case): in X hours
953             //#. %d is the number of hours
954             //#, c-format
955             ? npgettext("in", "%d hour", "%d hours", h)
956             //#. General duration (nominative case): X hours
957             //#. %d is the number of hours
958             //#, c-format
959             :  ngettext(      "%d hour", "%d hours", h),
960             h);
961     }
962     function get_hm(h, m) {
963         return format(until
964             //#. Reminder (objective case): in X hours and Y minutes
965             //#. %1$d is the number of hours
966             //#. %2$s is the text for the remainder of the last hour
967             //#, c-format
968             ? npgettext("in", "%1$d hour and %2$s", "%1$d hours and %2$s", h)
969             //#. General duration (nominative case): X hours and Y minutes
970             //#. %1$d is the number of hours
971             //#. %2$s is the text for the remainder of the last hour
972             //#, c-format
973             :  ngettext("%1$d hour and %2$s", "%1$d hours and %2$s", h),
974             h, minutes(m));
975     }
976     function hours(t) {
977         if (t < 60) return minutes(t); // min/h
978         var h = Math.floor(t / 60);
979         var m = t % 60;
980         return m ? get_hm(h, m) : get_h(h);
981     }
982     function get_d(d) {
983         return format(until
984             //#. Reminder (objective case): in X days
985             //#. %d is the number of days
986             //#, c-format
987             ? npgettext("in", "%d day", "%d days", d)
988             //#. General duration (nominative case): X days
989             //#. %d is the number of days
990             //#, c-format
991             : ngettext("%d day", "%d days", d),
992             d);
993     }
994     function get_dhm(d, t) {
995         return format(until
996             //#. Reminder (objective case): in X days, Y hours and Z minutes
997             //#. %1$d is the number of days
998             //#. %2$s is the text for the remainder of the last day
999             //#, c-format
1000             ? npgettext("in", "%1$d day, %2$s", "%1$d days, %2$s", d)
1001             //#. General duration (nominative case): X days, Y hours and Z minutes
1002             //#. %1$d is the number of days
1003             //#. %2$s is the text for the remainder of the last day
1004             //#, c-format
1005             : ngettext("%1$d day, %2$s", "%1$d days, %2$s", d),
1006             d, hours(t));
1007     }
1008     function days(t) {
1009         if (t < 1440) return hours(t); // min/day
1010         var d = Math.floor(t / 1440);
1011         t = t % 1440;
1012         return t ? get_dhm(d, t) : get_d(d); 
1013     }
1014     function get_w(w) {
1015         return format(until
1016             //#. Reminder (objective case): in X weeks
1017             //#. %d is the number of weeks
1018             //#, c-format
1019             ? npgettext("in", "%d week", "%d weeks", w)
1020             //#. General duration (nominative case): X weeks
1021             //#. %d is the number of weeks
1022             //#, c-format
1023             : ngettext("%d week", "%d weeks", w),
1024             w);
1025     }
1026 
1027     t = Math.round(t / 60000); // ms/min
1028 	if (t >= 10080 && t % 10080 == 0) { // min/week
1029         return get_w(Math.round(t / 10080));
1030 	} else {
1031         return days(t);
1032     }
1033 }
1034 
1035 var currencies = [
1036             { iso: "CAD", name: "Canadian dollar", isoLangCodes: [ "CA" ] },
1037             { iso: "CHF", name: "Swiss franc", isoLangCodes: [ "CH" ] },
1038             { iso: "DKK", name: "Danish krone", isoLangCodes: [ "DK" ] },
1039             { iso: "EUR", name: "Euro", isoLangCodes: [ "AT", "BE", "CY", "FI", "FR", "DE", "GR", "IE", "IT", "LU", "MT", "NL", "PT", "SI", "ES" ] },
1040             { iso: "GBP", name: "Pound sterling", isoLangCodes: [ "GB" ] },
1041             { iso: "PLN", name: "Zloty", isoLangCodes: [ "PL" ] },
1042             { iso: "RUB", name: "Russian rouble", isoLangCodes: [ "RU" ] },
1043             { iso: "SEK", name: "Swedish krona", isoLangCodes: [ "SE" ] },
1044             { iso: "USD", name: "US dollar", isoLangCodes: [ "US" ] }
1045         ];    
1046