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