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