1// I18n.js 2// ======= 3// 4// This small library provides the Rails I18n API on the Javascript. 5// You don't actually have to use Rails (or even Ruby) to use I18n.js. 6// Just make sure you export all translations in an object like this: 7// 8// I18n.translations.en = { 9// hello: "Hello World" 10// }; 11// 12// See tests for specific formatting like numbers and dates. 13// 14 15// Using UMD pattern from 16// https://github.com/umdjs/umd#regular-module 17// `returnExports.js` version 18;(function (root, factory) { 19 if (typeof define === 'function' && define.amd) { 20 // AMD. Register as an anonymous module. 21 define("i18n", function(){ return factory(root);}); 22 } else if (typeof module === 'object' && module.exports) { 23 // Node. Does not work with strict CommonJS, but 24 // only CommonJS-like environments that support module.exports, 25 // like Node. 26 module.exports = factory(root); 27 } else { 28 // Browser globals (root is window) 29 root.I18n = factory(root); 30 } 31}(this, function(global) { 32 "use strict"; 33 34 // Use previously defined object if exists in current scope 35 var I18n = global && global.I18n || {}; 36 37 // Just cache the Array#slice function. 38 var slice = Array.prototype.slice; 39 40 // Apply number padding. 41 var padding = function(number) { 42 return ("0" + number.toString()).substr(-2); 43 }; 44 45 // Improved toFixed number rounding function with support for unprecise floating points 46 // JavaScript's standard toFixed function does not round certain numbers correctly (for example 0.105 with precision 2). 47 var toFixed = function(number, precision) { 48 return decimalAdjust('round', number, -precision).toFixed(precision); 49 }; 50 51 // Is a given variable an object? 52 // Borrowed from Underscore.js 53 var isObject = function(obj) { 54 var type = typeof obj; 55 return type === 'function' || type === 'object' 56 }; 57 58 var isFunction = function(func) { 59 var type = typeof func; 60 return type === 'function' 61 }; 62 63 // Check if value is different than undefined and null; 64 var isSet = function(value) { 65 return typeof(value) !== 'undefined' && value !== null; 66 }; 67 68 // Is a given value an array? 69 // Borrowed from Underscore.js 70 var isArray = function(val) { 71 if (Array.isArray) { 72 return Array.isArray(val); 73 } 74 return Object.prototype.toString.call(val) === '[object Array]'; 75 }; 76 77 var isString = function(val) { 78 return typeof val === 'string' || Object.prototype.toString.call(val) === '[object String]'; 79 }; 80 81 var isNumber = function(val) { 82 return typeof val === 'number' || Object.prototype.toString.call(val) === '[object Number]'; 83 }; 84 85 var isBoolean = function(val) { 86 return val === true || val === false; 87 }; 88 89 var isNull = function(val) { 90 return val === null; 91 }; 92 93 var decimalAdjust = function(type, value, exp) { 94 // If the exp is undefined or zero... 95 if (typeof exp === 'undefined' || +exp === 0) { 96 return Math[type](value); 97 } 98 value = +value; 99 exp = +exp; 100 // If the value is not a number or the exp is not an integer... 101 if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { 102 return NaN; 103 } 104 // Shift 105 value = value.toString().split('e'); 106 value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); 107 // Shift back 108 value = value.toString().split('e'); 109 return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); 110 } 111 112 var lazyEvaluate = function(message, scope) { 113 if (isFunction(message)) { 114 return message(scope); 115 } else { 116 return message; 117 } 118 } 119 120 var merge = function (dest, obj) { 121 var key, value; 122 for (key in obj) if (obj.hasOwnProperty(key)) { 123 value = obj[key]; 124 if (isString(value) || isNumber(value) || isBoolean(value) || isArray(value) || isNull(value)) { 125 dest[key] = value; 126 } else { 127 if (dest[key] == null) dest[key] = {}; 128 merge(dest[key], value); 129 } 130 } 131 return dest; 132 }; 133 134 // Set default days/months translations. 135 var DATE = { 136 day_names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 137 , abbr_day_names: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] 138 , month_names: [null, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
138November", "December"] 139 , abbr_month_names: [null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 140 , meridian: ["AM", "PM"] 141 }; 142 143 // Set default number format. 144 var NUMBER_FORMAT = { 145 precision: 3 146 , separator: "." 147 , delimiter: "," 148 , strip_insignificant_zeros: false 149 }; 150 151 // Set default currency format. 152 var CURRENCY_FORMAT = { 153 unit: "$" 154 , precision: 2 155 , format: "%u%n" 156 , sign_first: true 157 , delimiter: "," 158 , separator: "." 159 }; 160 161 // Set default percentage format. 162 var PERCENTAGE_FORMAT = { 163 unit: "%" 164 , precision: 3 165 , format: "%n%u" 166 , separator: "." 167 , delimiter: "" 168 }; 169 170 // Set default size units. 171 var SIZE_UNITS = [null, "kb", "mb", "gb", "tb"]; 172 173 // Other default options 174 var DEFAULT_OPTIONS = { 175 // Set default locale. This locale will be used when fallback is enabled and 176 // the translation doesn't exist in a particular locale. 177 defaultLocale: "en" 178 // Set the current locale to `en`. 179 , locale: "en" 180 // Set the translation key separator. 181 , defaultSeparator: "." 182 // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. 183 , placeholder: /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm 184 // Set if engine should fallback to the default locale when a translation 185 // is missing. 186 , fallbacks: false 187 // Set the default translation object. 188 , translations: {} 189 // Set missing translation behavior. 'message' will display a message 190 // that the translation is missing, 'guess' will try to guess the string 191 , missingBehaviour: 'message' 192 // if you use missingBehaviour with 'message', but want to know that the 193 // string is actually missing for testing purposes, you can prefix the 194 // guessed string by setting the value here. By default, no prefix! 195 , missingTranslationPrefix: '' 196 }; 197 198 // Set default locale. This locale will be used when fallback is enabled and 199 // the translation doesn't exist in a particular locale. 200 I18n.reset = function() { 201 var key; 202 for (key in DEFAULT_OPTIONS) { 203 this[key] = DEFAULT_OPTIONS[key]; 204 } 205 }; 206 207 // Much like `reset`, but only assign options if not already assigned 208 I18n.initializeOptions = function() { 209 var key; 210 for (key in DEFAULT_OPTIONS) if (!isSet(this[key])) { 211 this[key] = DEFAULT_OPTIONS[key]; 212 } 213 }; 214 I18n.initializeOptions(); 215 216 // Return a list of all locales that must be tried before returning the 217 // missing translation message. By default, this will consider the inline option, 218 // current locale and fallback locale. 219 // 220 // I18n.locales.get("de-DE"); 221 // // ["de-DE", "de", "en"] 222 // 223 // You can define custom rules for any locale. Just make sure you return a array 224 // containing all locales. 225 // 226 // // Default the Wookie locale to English. 227 // I18n.locales["wk"] = function(locale) { 228 // return ["en"]; 229 // }; 230 // 231 I18n.locales = {}; 232 233 // Retrieve locales based on inline locale, current locale or default to 234 // I18n's detection. 235 I18n.locales.get = function(locale) { 236 var result = this[locale] || this[I18n.locale] || this["default"]; 237 238 if (isFunction(result)) { 239 result = result(locale); 240 } 241 242 if (isArray(result) === false) { 243 result = [result]; 244 } 245 246 return result; 247 }; 248 249 // The default locale list. 250 I18n.locales["default"] = function(locale) { 251 var locales = [] 252 , list = [] 253 ; 254 255 // Handle the inline locale option that can be provided to 256 // the `I18n.t` options. 257 if (locale) { 258 locales.push(locale); 259 } 260 261 // Add the current locale to the list. 262 if (!locale && I18n.locale) { 263 locales.push(I18n.locale); 264 } 265 266 // Add the default locale if fallback strategy is enabled. 267 if (I18n.fallbacks && I18n.defaultLocale) { 268 locales.push(I18n.defaultLocale); 269 } 270 271 // Locale code format 1: 272 // According to RFC4646 (http://www.ietf.org/rfc/rfc4646.txt) 273 // language codes for Traditional Chinese should be `zh-Hant` 274 // 275 // But due to backward compatibility 276 // We use older version of IETF language tag 277 // @see http://www.w3.org/TR/html401/struct/dirlang.html 278 // @see http://en.wikipedia.org/wiki/IETF_language_tag 279 // 280 // Format: `language-code = primary-code ( "-" subcode )*` 281 // 282 // primary-code uses ISO639-1 283 // @see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 284 // @see http://www.iso.org/iso/home/standards/language_codes.htm 285 // 286 // subcode uses ISO 3166-1 alpha-2 287 // @see http://en.wikipedia.org/wiki/ISO_3166 288 // @see http://www.iso.org/iso/country_codes.htm 289 // 290 // @note 291 // subcode can be in upper case or lower case 292 // defining it in upper case is a convention only 293 294 295 // Locale code format 2: 296 // Format: `code = primary-code ( "-" region-code )*` 297 // primary-code uses ISO 639-1 298 // script-code uses ISO 15924 299 // region-code uses ISO 3166-1 alpha-2 300 // Example: zh-Hant-TW, en-HK, zh-Hant-CN 301 // 302 // It is similar to RFC4646 (or actually the same), 303 // but seems to be limited to language, script, region 304 305 // Compute each locale with its country code. 306 // So this will return an array containing 307 // `de-DE` and `de` 308 // or 309 // `zh-hans-tw`, `zh-hans`, `zh` 310 // locales.
311 locales.forEach(function(locale) { 312 var localeParts = locale.split("-"); 313 var firstFallback = null; 314 var secondFallback = null; 315 if (localeParts.length === 3) { 316 firstFallback = [ 317 localeParts[0], 318 localeParts[1] 319 ].join("-"); 320 secondFallback = localeParts[0]; 321 } 322 else if (localeParts.length === 2) { 323 firstFallback = localeParts[0]; 324 } 325 326 if (list.indexOf(locale) === -1) { 327 list.push(locale); 328 } 329 330 if (! I18n.fallbacks) { 331 return; 332 } 333 334 [ 335 firstFallback, 336 secondFallback 337 ].forEach(function(nullableFallbackLocale) { 338 // We don't want null values 339 if (typeof nullableFallbackLocale === "undefined") { return; } 340 if (nullableFallbackLocale === null) { return; } 341 // We don't want duplicate values 342 // 343 // Comparing with `locale` first is faster than 344 // checking whether value's presence in the list 345 if (nullableFallbackLocale === locale) { return; } 346 if (list.indexOf(nullableFallbackLocale) !== -1) { return; } 347 348 list.push(nullableFallbackLocale); 349 }); 350 }); 351 352 // No locales set? English it is. 353 if (!locales.length) { 354 locales.push("en"); 355 } 356 357 return list; 358 }; 359 360 // Hold pluralization rules. 361 I18n.pluralization = {}; 362 363 // Return the pluralizer for a specific locale. 364 // If no specify locale is found, then I18n's default will be used. 365 I18n.pluralization.get = function(locale) { 366 return this[locale] || this[I18n.locale] || this["default"]; 367 }; 368 369 // The default pluralizer rule. 370 // It detects the `zero`, `one`, and `other` scopes. 371 I18n.pluralization["default"] = function(count) { 372 switch (count) { 373 case 0: return ["zero", "other"]; 374 case 1: return ["one"]; 375 default: return ["other"]; 376 } 377 }; 378 379 // Return current locale. If no locale has been set, then 380 // the current locale will be the default locale. 381 I18n.currentLocale = function() { 382 return this.locale || this.defaultLocale; 383 }; 384 385 // Check if value is different than undefined and null; 386 I18n.isSet = isSet; 387 388 // Find and process the translation using the provided scope and options. 389 // This is used internally by some functions and should not be used as an 390 // public API. 391 I18n.lookup = function(scope, options) { 392 options = options || {} 393 394 var locales = this.locales.get(options.locale).slice() 395 , locale 396 , scopes 397 , fullScope 398 , translations 399 ; 400 401 fullScope = this.getFullScope(scope, options); 402 403 while (locales.length) { 404 locale = locales.shift(); 405 scopes = fullScope.split(this.defaultSeparator); 406 translations = this.translations[locale]; 407 408 if (!translations) { 409 continue; 410 } 411 while (scopes.length) { 412 translations = translations[scopes.shift()]; 413 414 if (translations === undefined || translations === null) { 415 break; 416 } 417 } 418 419 if (translations !== undefined && translations !== null) { 420 return translations; 421 } 422 } 423 424 if (isSet(options.defaultValue)) { 425 return lazyEvaluate(options.defaultValue, scope); 426 } 427 }; 428 429 // lookup pluralization rule key into translations 430 I18n.pluralizationLookupWithoutFallback = function(count, locale, translations) { 431 var pluralizer = this.pluralization.get(locale) 432 , pluralizerKeys = pluralizer(count) 433 , pluralizerKey 434 , message; 435 436 if (isObject(translations)) { 437 while (pluralizerKeys.length) { 438 pluralizerKey = pluralizerKeys.shift(); 439 if (isSet(translations[pluralizerKey])) { 440 message = translations[pluralizerKey]; 441 break; 442 } 443 } 444 } 445 446 return message; 447 }; 448 449 // Lookup dedicated to pluralization 450 I18n.pluralizationLookup = function(count, scope, options) { 451 options = options || {} 452 var locales = this.locales.get(options.locale).slice() 453 , locale 454 , scopes 455 , translations 456 , message 457 ; 458 scope = this.getFullScope(scope, options); 459 460 while (locales.length) { 461 locale = locales.shift(); 462 scopes = scope.split(this.defaultSeparator); 463 translations = this.translations[locale]; 464 465 if (!translations) { 466 continue; 467 } 468 469 while (scopes.length) { 470 translations = translations[scopes.shift()]; 471 if (!isObject(translations)) { 472 break; 473 } 474 if (scopes.length == 0) { 475 message = this.pluralizationLookupWithoutFallback(count, locale, translations); 476 } 477 } 478 if (message != null && message != undefined) { 479 break; 480 } 481 } 482 483 if (message == null || message == undefined) { 484 if (isSet(options.defaultValue)) { 485 if (isObject(options.defaultValue)) { 486 message = this.pluralizationLookupWithoutFallback(count, options.locale, options.defaultValue); 487 } else { 488 message = options.defaultValue; 489 } 490 translations = options.defaultValue; 491 } 492 } 493 494 return { message: message, translations: translations }; 495 }; 496 497 // Rails changed the way the meridian is stored. 498 // It started with `date.meridian` returning an array, 499 // then it switched to `time.am` and `time.pm`. 500 // This function abstracts this difference and returns 501 // the correct meridian or the default value when none is provided. 502 I18n.meridian = function() { 503 var time = this.lookup("time"); 504 var date = this.lookup("date"); 505 506 if (time && time.am && time.pm) { 507 return [time.am, time.pm]; 508 } else if (date && date.meridian) { 509 return date.meridian; 510 } else { 511 return DATE.meridian; 512 } 513 }; 514 515 // Merge serveral hash options, checking if value is set before 516 // overwriting any value. The precedence is from left to right. 517 //
518 // I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"}); 519 // #=> {name: "John Doe", role: "user"} 520 // 521 I18n.prepareOptions = function() { 522 var args = slice.call(arguments) 523 , options = {} 524 , subject 525 ; 526 527 while (args.length) { 528 subject = args.shift(); 529 530 if (typeof(subject) != "object") { 531 continue; 532 } 533 534 for (var attr in subject) { 535 if (!subject.hasOwnProperty(attr)) { 536 continue; 537 } 538 539 if (isSet(options[attr])) { 540 continue; 541 } 542 543 options[attr] = subject[attr]; 544 } 545 } 546 547 return options; 548 }; 549 550 // Generate a list of translation options for default fallbacks. 551 // `defaultValue` is also deleted from options as it is returned as part of 552 // the translationOptions array. 553 I18n.createTranslationOptions = function(scope, options) { 554 var translationOptions = [{scope: scope}]; 555 556 // Defaults should be an array of hashes containing either 557 // fallback scopes or messages 558 if (isSet(options.defaults)) { 559 translationOptions = translationOptions.concat(options.defaults); 560 } 561 562 // Maintain support for defaultValue. Since it is always a message 563 // insert it in to the translation options as such. 564 if (isSet(options.defaultValue)) { 565 translationOptions.push({ message: options.defaultValue }); 566 } 567 568 return translationOptions; 569 }; 570 571 // Translate the given scope with the provided options. 572 I18n.translate = function(scope, options) { 573 options = options || {} 574 575 var translationOptions = this.createTranslationOptions(scope, options); 576 577 var translation; 578 579 var optionsWithoutDefault = this.prepareOptions(options) 580 delete optionsWithoutDefault.defaultValue 581 582 // Iterate through the translation options until a translation 583 // or message is found. 584 var translationFound = 585 translationOptions.some(function(translationOption) { 586 if (isSet(translationOption.scope)) { 587 translation = this.lookup(translationOption.scope, optionsWithoutDefault); 588 } else if (isSet(translationOption.message)) { 589 translation = lazyEvaluate(translationOption.message, scope); 590 } 591 592 if (translation !== undefined && translation !== null) { 593 return true; 594 } 595 }, this); 596 597 if (!translationFound) { 598 return this.missingTranslation(scope, options); 599 } 600 601 if (typeof(translation) === "string") { 602 translation = this.interpolate(translation, options); 603 } else if (isArray(translation)) { 604 translation = translation.map(function(t) { 605 return (typeof(t) === "string" ? this.interpolate(t, options) : t); 606 }, this); 607 } else if (isObject(translation) && isSet(options.count)) { 608 translation = this.pluralize(options.count, scope, options); 609 } 610 611 return translation; 612 }; 613 614 // This function interpolates the all variables in the given message. 615 I18n.interpolate = function(message, options) { 616 if (message === null) { 617 return message; 618 } 619 620 options = options || {} 621 var matches = message.match(this.placeholder) 622 , placeholder 623 , value 624 , name 625 , regex 626 ; 627 628 if (!matches) { 629 return message; 630 } 631 632 var value; 633 634 while (matches.length) { 635 placeholder = matches.shift(); 636 name = placeholder.replace(this.placeholder, "$1"); 637 638 if (isSet(options[name])) { 639 value = options[name].toString().replace(/\$/gm, "_#$#_"); 640 } else if (name in options) { 641 value = this.nullPlaceholder(placeholder, message, options); 642 } else { 643 value = this.missingPlaceholder(placeholder, message, options); 644 } 645 646 regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}")); 647 message = message.replace(regex, value); 648 } 649 650 return message.replace(/_#\$#_/g, "$"); 651 }; 652 653 // Pluralize the given scope using the `count` value. 654 // The pluralized translation may have other placeholders, 655 // which will be retrieved from `options`. 656 I18n.pluralize = function(count, scope, options) { 657 options = this.prepareOptions({count: String(count)}, options) 658 var pluralizer, message, result; 659 660 result = this.pluralizationLookup(count, scope, options); 661 if (result.translations == undefined || result.translations == null) { 662 return this.missingTranslation(scope, options); 663 } 664 665 if (result.message != undefined && result.message != null) { 666 return this.interpolate(result.message, options); 667 } 668 else { 669 pluralizer = this.pluralization.get(options.locale); 670 return this.missingTranslation(scope + '.' + pluralizer(count)[0], options); 671 } 672 }; 673 674 // Return a missing translation message for the given parameters. 675 I18n.missingTranslation = function(scope, options) { 676 //guess intended string 677 if(this.missingBehaviour == 'guess'){ 678 //get only the last portion of the scope 679 var s = scope.split('.').slice(-1)[0]; 680 //replace underscore with space && camelcase with space and lowercase letter 681 return (this.missingTranslationPrefix.length > 0 ? this.missingTranslationPrefix : '') + 682 s.replace('_',' ').replace(/([a-z])([A-Z])/g, 683 function(match, p1, p2) {return p1 + ' ' + p2.toLowerCase()} ); 684 } 685 686 var localeForTranslation = (options != null && options.locale != null) ? options.locale : this.currentLocale(); 687 var fullScope = this.getFullScope(scope, options); 688 var fullScopeWithLocale = [localeForTranslation, fullScope].join(this.defaultSeparator); 689 690 return '[missing "' + fullScopeWithLocale + '" translation]'; 691 }; 692 693 // Return a missing placeholder message for given parameters 694 I18n.missingPlaceholder = function(placeholder, message, options) { 695 return "[missing " + placeholder + " value]"; 696 }; 697 698 I18n.nullPlaceholder = function() { 699 return I18n.missingPlaceholder.apply(I18n, arguments); 700 }; 701 702 // Format number using localization rules. 703 // The options will be retrieved from the `number.format` scope. 704 // If this isn't present, then the following options will be used: 705 // 706 // - `precision`: `3` 707 // - `separator`: `"."` 708 // - `delimiter`: `","` 709 // - `strip_insignificant_zeros`: `false` 710 // 711 // You can also override these options by providing the `options` argument. 712 // 713 I18n.toNumber = function(number, options) { 714 options = this.prepareOptions( 715 options 716 , this.lookup("number.format") 717 , NUMBER_FORMAT 718 ); 719 720 var negative = number < 0 721 , string = toFixed(Math.abs(number), options.precision).toString() 722 , parts = string.split(".") 723 , precision 724 , buffer = [] 725 , formattedNumber 726 , format = options.format || "%n" 727 , sign = negative ? "-" : "" 728 ; 729 730 number = parts[0]; 731 precision = parts[1]; 732 733 while (number.length > 0) { 734 buffer.unshift(number.substr(Math.max(0, number.length - 3), 3)); 735 number = number.substr(0, number.length -3); 736 } 737 738 formattedNumber = buffer.join(options.delimiter); 739 740 if (options.strip_insignificant_zeros && precision) { 741 precision = precision.replace(/0+$/, ""); 742 } 743 744 if (options.precision > 0 && precision) { 745 formattedNumber += options.separator + precision; 746 } 747 748 if (options.sign_first) { 749 format = "%s" + format; 750 } 751 else { 752 format = format.replace("%n", "%s%n"); 753 } 754 755 formattedNumber = format 756 .replace("%u", options.unit) 757 .replace("%n", formattedNumber) 758 .replace("%s", sign) 759 ; 760 761 return formattedNumber; 762 }; 763 764 // Format currency with localization rules. 765 // The options will be retrieved from the `number.currency.format` and 766 // `number.format` scopes, in that order. 767 // 768 // Any missing option will be retrieved from the `I18n.toNumber` defaults and 769 // the following options: 770 // 771 // - `unit`: `"$"` 772 // - `precision`: `2` 773 // - `format`: `"%u%n"` 774 // - `delimiter`: `","` 775 // - `separator`: `"."` 776 // 777 // You can also override these options by providing the `options` argument. 778 // 779 I18n.toCurrency = function(number, options) { 780 options = this.prepareOptions( 781 options 782 , this.lookup("number.currency.format") 783 , this.lookup("number.format") 784 , CURRENCY_FORMAT 785 ); 786 787 return this.toNumber(number, options); 788 }; 789 790 // Localize several values. 791 // You can provide the following scopes: `currency`, `number`, or `percentage`.
792 // If you provide a scope that matches the `/^(date|time)/` regular expression 793 // then the `value` will be converted by using the `I18n.toTime` function. 794 // 795 // It will default to the value's `toString` function. 796 // 797 I18n.localize = function(scope, value, options) { 798 options || (options = {}); 799 800 switch (scope) { 801 case "currency": 802 return this.toCurrency(value); 803 case "number": 804 scope = this.lookup("number.format"); 805 return this.toNumber(value, scope); 806 case "percentage": 807 return this.toPercentage(value); 808 default: 809 var localizedValue; 810 811 if (scope.match(/^(date|time)/)) { 812 localizedValue = this.toTime(scope, value); 813 } else { 814 localizedValue = value.toString(); 815 } 816 817 return this.interpolate(localizedValue, options); 818 } 819 }; 820 821 // Parse a given `date` string into a JavaScript Date object. 822 // This function is time zone aware. 823 // 824 // The following string formats are recognized: 825 // 826 // yyyy-mm-dd 827 // yyyy-mm-dd[ T]hh:mm::ss 828 // yyyy-mm-dd[ T]hh:mm::ss 829 // yyyy-mm-dd[ T]hh:mm::ssZ 830 // yyyy-mm-dd[ T]hh:mm::ss+0000 831 // yyyy-mm-dd[ T]hh:mm::ss+00:00 832 // yyyy-mm-dd[ T]hh:mm::ss.123Z 833 // 834 I18n.parseDate = function(date) { 835 var matches, convertedDate, fraction; 836 // we have a date, so just return it. 837 if (typeof(date) == "object") { 838 return date; 839 }; 840 841 matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})([\.,]\d{1,3})?)?(Z|\+00:?00)?/); 842 843 if (matches) { 844 for (var i = 1; i <= 6; i++) { 845 matches[i] = parseInt(matches[i], 10) || 0; 846 } 847 848 // month starts on 0 849 matches[2] -= 1; 850 851 fraction = matches[7] ? 1000 * ("0" + matches[7]) : null; 852 853 if (matches[8]) { 854 convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction)); 855 } else { 856 convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction); 857 } 858 } else if (typeof(date) == "number") { 859 // UNIX timestamp 860 convertedDate = new Date(); 861 convertedDate.setTime(date); 862 } else if (date.match(/([A-Z][a-z]{2}) ([A-Z][a-z]{2}) (\d+) (\d+:\d+:\d+) ([+-]\d+) (\d+)/)) { 863 // This format `Wed Jul 20 13:03:39 +0000 2011` is parsed by 864 // webkit/firefox, but not by IE, so we must parse it manually. 865 convertedDate = new Date(); 866 convertedDate.setTime(Date.parse([ 867 RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$6, RegExp.$4, RegExp.$5 868 ].join(" "))); 869 } else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) { 870 // a valid javascript format with timezone info 871 convertedDate = new Date(); 872 convertedDate.setTime(Date.parse(date)); 873 } else { 874 // an arbitrary javascript string 875 convertedDate = new Date(); 876 convertedDate.setTime(Date.parse(date)); 877 } 878 879 return convertedDate; 880 }; 881 882 // Formats time according to the directives in the given format string. 883 // The directives begins with a percent (%) character. Any text not listed as a 884 // directive will be passed through to the output string. 885 // 886 // The accepted formats are: 887 // 888 // %a - The abbreviated weekday name (Sun) 889 // %A - The full weekday name (Sunday) 890 // %b - The abbreviated month name (Jan) 891 // %B - The full month name (January) 892 // %c - The preferred local date and time representation 893 // %d - Day of the month (01..31) 894 // %-d - Day of the month (1..31) 895 // %H - Hour of the day, 24-hour clock (00..23) 896 // %-H - Hour of the day, 24-hour clock (0..23) 897 // %I - Hour of the day, 12-hour clock (01..12) 898 // %-I - Hour of the day, 12-hour clock (1..12) 899 // %m - Month of the year (01..12) 900 // %-m - Month of the year (1..12) 901 // %M - Minute of the hour (00..59) 902 // %-M - Minute of the hour (0..59) 903 // %p - Meridian indicator (AM or PM) 904 // %S - Second of the minute (00..60) 905 // %-S - Second of the minute (0..60) 906 // %w - Day of the week (Sunday is 0, 0..6) 907 // %y - Year without a century (00..99) 908 // %-y - Year without a century (0..99) 909 // %Y - Year with century 910 // %z - Timezone offset (+0545) 911 // 912 I18n.strftime = function(date, format) { 913 var options = this.lookup("date") 914 , meridianOptions = I18n.meridian() 915 ; 916 917 if (!options) { 918 options = {}; 919 } 920 921 options = this.prepareOptions(options, DATE); 922 923 if (isNaN(date.getTime())) { 924 throw new Error('I18n.strftime() requires a valid date object, but received an invali
924d date.'); 925 } 926 927 var weekDay = date.getDay() 928 , day = date.getDate() 929 , year = date.getFullYear() 930 , month = date.getMonth() + 1 931 , hour = date.getHours() 932 , hour12 = hour 933 , meridian = hour > 11 ? 1 : 0 934 , secs = date.getSeconds() 935 , mins = date.getMinutes() 936 , offset = date.getTimezoneOffset() 937 , absOffsetHours = Math.floor(Math.abs(offset / 60)) 938 , absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60) 939 , timezoneoffset = (offset > 0 ? "-" : "+") + 940 (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + 941 (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes) 942 ; 943 944 if (hour12 > 12) { 945 hour12 = hour12 - 12; 946 } else if (hour12 === 0) { 947 hour12 = 12; 948 } 949 950 format = format.replace("%a", options.abbr_day_names[weekDay]); 951 format = format.replace("%A", options.day_names[weekDay]); 952 format = format.replace("%b", options.abbr_month_names[month]); 953 format = format.replace("%B", options.month_names[month]); 954 format = format.replace("%d", padding(day)); 955 format = format.replace("%e", day); 956 format = format.replace("%-d", day); 957 format = format.replace("%H", padding(hour)); 958 format = format.replace("%-H", hour); 959 format = format.replace("%I", padding(hour12)); 960 format = format.replace("%-I", hour12); 961 format = format.replace("%m", padding(month)); 962 format = format.replace("%-m", month); 963 format = format.replace("%M", padding(mins)); 964 format = format.replace("%-M", mins); 965 format = format.replace("%p", meridianOptions[meridian]); 966 format = format.replace("%S", padding(secs)); 967 format = format.replace("%-S", secs); 968 format = format.replace("%w", weekDay); 969 format = format.replace("%y", padding(year)); 970 format = format.replace("%-y", padding(year).replace(/^0+/, "")); 971 format = format.replace("%Y", year); 972 format = format.replace("%z", timezoneoffset); 973 974 return format; 975 }; 976 977 // Convert the given dateString into a formatted date. 978 I18n.toTime = function(scope, dateString) { 979 var date = this.parseDate(dateString) 980 , format = this.lookup(scope) 981 ; 982 983 if (date.toString().match(/invalid/i)) { 984 return date.toString(); 985 } 986 987 if (!format) { 988 return date.toString(); 989 } 990 991 return this.strftime(date, format); 992 }; 993 994 // Convert a number into a formatted percentage value. 995 I18n.toPercentage = function(number, options) { 996 options = this.prepareOptions( 997 options 998 , this.lookup("number.percentage.format") 999 , this.lookup("number.format") 1000 , PERCENTAGE_FORMAT 1001 ); 1002 1003 return this.toNumber(number, options); 1004 }; 1005 1006 // Convert a number into a readable size representation. 1007 I18n.toHumanSize = function(number, options) { 1008 var kb = 1024 1009 , size = number 1010 , iterations = 0 1011 , unit 1012 , precision 1013 ; 1014 1015 while (size >= kb && iterations < 4) { 1016 size = size / kb; 1017 iterations += 1; 1018 } 1019 1020 if (iterations === 0) { 1021 unit = this.t("number.human.storage_units.units.byte", {count: size}); 1022 precision = 0; 1023 } else { 1024 unit = this.t("number.human.storage_units.units." + SIZE_UNITS[iterations]); 1025 precision = (size - Math.floor(size) === 0) ? 0 : 1; 1026 } 1027 1028 options = this.prepareOptions( 1029 options 1030 , {unit: unit, precision: precision, format: "%n%u", delimiter: ""} 1031 ); 1032 1033 return this.toNumber(size, options); 1034 }; 1035 1036 I18n.getFullScope = function(scope, options) { 1037 options = options || {} 1038 1039 // Deal with the scope as an array. 1040 if (isArray(scope)) { 1041 scope = scope.join(this.defaultSeparator); 1042 } 1043 1044 // Deal with the scope option provided through the second argument. 1045 // 1046 // I18n.t('hello', {scope: 'greetings'}); 1047 // 1048 if (options.scope) { 1049 scope = [options.scope, scope].join(this.defaultSeparator); 1050 } 1051 1052 return scope; 1053 }; 1054 /** 1055 * Merge obj1 with obj2 (shallow merge), without modifying inputs 1056 * @param {Object} obj1 1057 * @param {Object} obj2 1058 * @returns {Object} Merged values of obj1 and obj2 1059 *
1060 * In order to support ES3, `Object.prototype.hasOwnProperty.call` is used 1061 * Idea is from: 1062 * https://stackoverflow.com/questions/8157700/object-has-no-hasownproperty-method-i-e-its-undefined-ie8 1063 */ 1064 I18n.extend = function ( obj1, obj2 ) { 1065 if (typeof(obj1) === "undefined" && typeof(obj2) === "undefined") { 1066 return {}; 1067 } 1068 return merge(obj1, obj2); 1069 }; 1070 1071 // Set aliases, so we can save some typing. 1072 I18n.t = I18n.translate; 1073 I18n.l = I18n.localize; 1074 I18n.p = I18n.pluralize; 1075 1076 return I18n; 1077}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.