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