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