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