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