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