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