1 /* 2 * ResBundle.js - Resource bundle definition 3 * 4 * Copyright © 2012-2016, 2018-2019, 2022 JEDLSoft 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 // !data pseudomap 21 22 var ilib = require("../index.js"); 23 var Utils = require("./Utils.js"); 24 var JSUtils = require("./JSUtils.js"); 25 26 var Locale = require("./Locale.js"); 27 var LocaleInfo = require("./LocaleInfo.js"); 28 29 var IString = require("./IString.js"); 30 31 /** 32 * @class 33 * Create a new resource bundle instance. The resource bundle loads strings 34 * appropriate for a particular locale and provides them via the getString 35 * method.<p> 36 * 37 * The options object may contain any (or none) of the following properties: 38 * 39 * <ul> 40 * <li><i>locale</i> - The locale of the strings to load. If not specified, the default 41 * locale is the the default for the web page or app in which the bundle is 42 * being loaded. 43 * 44 * <li><i>name</i> - Base name of the resource bundle to load. If not specified the default 45 * base name is "resources". 46 * 47 * <li><i>type</i> - Name the type of strings this bundle contains. Valid values are 48 * "xml", "html", "text", "c", "raw", "ruby", or "template". The default is "text". 49 * If the type is "xml" or "html", 50 * then XML/HTML entities and tags are not pseudo-translated. During a real translation, 51 * HTML character entities are translated to their corresponding characters in a source 52 * string before looking that string up in the translations. Also, the characters "<", ">", 53 * and "&" are converted to entities again in the output, but characters are left as they 54 * are. If the type is "xml", "html", "ruby", or "text" types, then the replacement parameter names 55 * are not pseudo-translated as well so that the output can be used for formatting with 56 * the IString class. If the type is "c" then all C language style printf replacement 57 * parameters (eg. "%s" and "%d") are skipped automatically. This includes iOS/Objective-C/Swift 58 * substitution parameters like "%@" or "%1$@". If the type is raw, all characters 59 * are pseudo-translated, including replacement parameters as well as XML/HTML tags and entities. 60 * 61 * <li><i>lengthen</i> - when pseudo-translating the string, tell whether or not to 62 * automatically lengthen the string to simulate "long" languages such as German 63 * or French. This is a boolean value. Default is false. 64 * 65 * <li><i>missing</i> - what to do when a resource is missing. The choices are: 66 * <ul> 67 * <li><i>source</i> - return the source string unchanged 68 * <li><i>pseudo</i> - return the pseudo-translated source string, translated to the 69 * script of the locale if the mapping is available, or just the default Latin 70 * pseudo-translation if not 71 * <li><i>empty</i> - return the empty string 72 * </ul> 73 * The default behaviour is the same as before, which is to return the source string 74 * unchanged. 75 * 76 * <li><i>basePath</i> - look in the given path for the resource bundle files. This can be 77 * an absolute path or a relative path that is relative to the application's root. 78 * Default if this is not specified is to look in the standard path (ie. in the root 79 * of the app). 80 * 81 * <li><i>onLoad</i> - a callback function to call when the resources are fully 82 * loaded. When the onLoad option is given, this class will attempt to 83 * load any missing locale data using the ilib loader callback. 84 * When the constructor is done (even if the data is already preassembled), the 85 * onLoad function is called with the current instance as a parameter, so this 86 * callback can be used with preassembled or dynamic loading or a mix of the two. 87 * 88 * <li>sync - tell whether to load any missing locale data synchronously or 89 * asynchronously. If this option is given as "false", then the "onLoad" 90 * callback must be given, as the instance returned from this constructor will 91 * not be usable for a while. 92 * 93 * <li><i>loadParams</i> - an object containing parameters to pass to the 94 * loader callback function when locale data is missing. The parameters are not 95 * interpretted or modified in any way. They are simply passed along. The object 96 * may contain any property/value pairs as long as the calling code is in 97 * agreement with the loader callback function as to what those parameters mean. 98 * </ul> 99 * 100 * The locale option may be given as a locale spec string or as an 101 * Locale object. If the locale option is not specified, then strings for 102 * the default locale will be loaded.<p> 103 * 104 * The name option can be used to put groups of strings together in a 105 * single bundle. The strings will then appear together in a JS object in 106 * a JS file that can be included before the ilib.<p> 107 * 108 * A resource bundle with a particular name is actually a set of bundles 109 * that are each specific to a language, a language plus a region, etc. 110 * All bundles with the same base name should 111 * contain the same set of source strings, but with different translations for 112 * the given locale. The user of the bundle does not need to be aware of 113 * the locale of the bundle, as long as it contains values for the strings 114 * it needs.<p> 115 * 116 * Strings in bundles for a particular locale are inherited from parent bundles 117 * that are more generic. In general, the hierarchy is as follows (from 118 * least locale-specific to most locale-specific): 119 * 120 * <ol> 121 * <li> language 122 * <li> region 123 * <li> language_script 124 * <li> language_region 125 * <li> region_variant 126 * <li> language_script_region 127 * <li> language_region_variant 128 * <li> language_script_region_variant 129 * </ol> 130 * 131 * That is, if the translation for a string does not exist in the current 132 * locale, the more-generic parent locale is searched for the string. In the 133 * worst case scenario, the string is not found in the base locale's strings. 134 * In this case, the missing option guides this class on what to do. If 135 * the missing option is "source", then the original source is returned as 136 * the translation. If it is "empty", the empty string is returned. If it 137 * is "pseudo", then the pseudo-translated string that is appropriate for 138 * the default script of the locale is returned.<p> 139 * 140 * This allows developers to create code with new or changed strings in it and check in that 141 * code without waiting for the translations to be done first. The translated 142 * version of the app or web site will still function properly, but will show 143 * a spurious untranslated string here and there until the translations are 144 * done and also checked in.<p> 145 * 146 * The base is whatever language your developers use to code in. For 147 * a German web site, strings in the source code may be written in German 148 * for example. Often this base is English, as many web sites are coded in 149 * English, but that is not required.<p> 150 * 151 * The strings can be extracted with the ilib localization tool (which will be 152 * shipped at some future time.) Once the strings 153 * have been translated, the set of translated files can be generated with the 154 * same tool. The output from the tool can be used as input to the ResBundle 155 * object. It is up to the web page or app to make sure the JS file that defines 156 * the bundle is included before creating the ResBundle instance.<p> 157 * 158 * A special locale "zxx-XX" is used as the pseudo-translation locale because 159 * zxx means "no linguistic information" in the ISO 639 standard, and the region 160 * code XX is defined to be user-defined in the ISO 3166 standard. 161 * Pseudo-translation is a locale where the translations are generated on 162 * the fly based on the contents of the source string. Characters in the source 163 * string are replaced with other characters and returned. 164 * 165 * Example. If the source string is: 166 * 167 * <pre> 168 * "This is a string" 169 * </pre> 170 * 171 * then the pseudo-translated version might look something like this: 172 * 173 * <pre> 174 * "Ţħïş ïş á şţřïñĝ" 175 * </pre> 176 * <p> 177 * 178 * Pseudo-translation can be used to test that your app or web site is translatable 179 * before an actual translation has happened. These bugs can then be fixed 180 * before the translation starts, avoiding an explosion of bugs later when 181 * each language's tester registers the same bug complaining that the same 182 * string is not translated. When pseudo-localizing with 183 * the Latin script, this allows the strings to be readable in the UI in the 184 * source language (if somewhat funky-looking), 185 * so that a tester can easily verify that the string is properly externalized 186 * and loaded from a resource bundle without the need to be able to read a 187 * foreign language.<p> 188 * 189 * If one of a list of script tags is given in the pseudo-locale specifier, then the 190 * pseudo-localization can map characters to very rough transliterations of 191 * characters in the given script. For example, zxx-Hebr-XX maps strings to 192 * Hebrew characters, which can be used to test your UI in a right-to-left 193 * language to catch bidi bugs before a translation is done. Currently, the 194 * list of target scripts includes Hebrew (Hebr), Chinese Simplified Han (Hans), 195 * and Cyrillic (Cyrl) with more to be added later. If no script is explicitly 196 * specified in the locale spec, or if the script is not supported, 197 * then the default mapping maps Latin base characters to accented versions of 198 * those Latin characters as in the example above. 199 * 200 * When the "lengthen" property is set to true in the options, the 201 * pseudotranslation code will add digits to the end of the string to simulate 202 * the lengthening that occurs when translating to other languages. The above 203 * example will come out like this: 204 * 205 * <pre> 206 * "Ţħïş ïş á şţřïñĝ76543210" 207 * </pre> 208 * 209 * The string is lengthened according to the length of the source string. If 210 * the source string is less than 20 characters long, the string is lengthened 211 * by 50%. If the source string is 20-40 212 * characters long, the string is lengthened by 33%. If te string is greater 213 * than 40 characters long, the string is lengthened by 20%.<p> 214 * 215 * The pseudotranslation always ends a string with the digit "0". If you do 216 * not see the digit "0" in the UI for your app, you know that truncation 217 * has occurred, and the number you see at the end of the string tells you 218 * how many characters were truncated.<p> 219 * 220 * 221 * @constructor 222 * @param {?Object} options Options controlling how the bundle is created 223 */ 224 var ResBundle = function (options) { 225 var lookupLocale, spec; 226 227 this.locale = new Locale(); // use the default locale 228 this.baseName = "strings"; 229 this.type = "text"; 230 this.loadParams = {}; 231 this.missing = "source"; 232 this.sync = true; 233 234 if (options) { 235 if (options.locale) { 236 this.locale = (typeof(options.locale) === 'string') ? 237 new Locale(options.locale) : 238 options.locale; 239 } 240 if (options.name) { 241 this.baseName = options.name; 242 } 243 if (options.type) { 244 this.type = options.type; 245 } 246 this.lengthen = options.lengthen || false; 247 this.path = options.basePath; 248 249 if (typeof(options.sync) !== 'undefined') { 250 this.sync = !!options.sync; 251 } 252 253 if (typeof(options.loadParams) !== 'undefined') { 254 this.loadParams = options.loadParams; 255 if (!this.path) { 256 if (typeof (options.loadParams.root) !== 'undefined') { 257 this.path = options.loadParams.root; 258 } else if (typeof (options.loadParams.base) !== 'undefined') { 259 this.path = options.loadParams.base; 260 } 261 } 262 } 263 if (typeof(options.missing) !== 'undefined') { 264 if (options.missing === "pseudo" || options.missing === "empty") { 265 this.missing = options.missing; 266 } 267 } 268 } else { 269 options = {sync: true}; 270 } 271 272 this.map = {}; 273 274 lookupLocale = this.locale.isPseudo() ? new Locale("en-US") : this.locale; 275 276 // ensure that the plural rules are loaded before we proceed 277 IString.loadPlurals(this.sync, lookupLocale, this.loadParams, ilib.bind(this, function() { 278 Utils.loadData({ 279 locale: lookupLocale, 280 name: this.baseName + ".json", 281 sync: this.sync, 282 loadParams: this.loadParams, 283 root: this.path, 284 callback: ilib.bind(this, function (map) { 285 if (!map) { 286 map = ilib.data[this.baseName] || {}; 287 } 288 this.map = map; 289 if (this.locale.isPseudo()) { 290 this._loadPseudo(this.locale, options.onLoad); 291 } else if (this.missing === "pseudo") { 292 new LocaleInfo(this.locale, { 293 sync: this.sync, 294 loadParams: this.loadParams, 295 onLoad: ilib.bind(this, function (li) { 296 var pseudoLocale = new Locale("zxx", "XX", undefined, li.getDefaultScript()); 297 this._loadPseudo(pseudoLocale, options.onLoad); 298 }) 299 }); 300 } else { 301 if (typeof(options.onLoad) === 'function') { 302 options.onLoad(this); 303 } 304 } 305 }) 306 }) 307 })); 308 309 // console.log("Merged resources " + this.locale.toString() + " are: " + JSON.stringify(this.map)); 310 //if (!this.locale.isPseudo() && JSUtils.isEmpty(this.map)) { 311 // console.log("Resources for bundle " + this.baseName + " locale " + this.locale.toString() + " are not available."); 312 //} 313 }; 314 315 ResBundle.defaultPseudo = ilib.data.pseudomap || { 316 "a": "à", 317 "e": "ë", 318 "i": "í", 319 "o": "õ", 320 "u": "ü", 321 "y": "ÿ", 322 "A": "Ã", 323 "E": "Ë", 324 "I": "Ï", 325 "O": "Ø", 326 "U": "Ú", 327 "Y": "Ŷ" 328 }; 329 330 ResBundle.prototype = { 331 /** 332 * @protected 333 */ 334 _loadPseudo: function (pseudoLocale, onLoad) { 335 Utils.loadData({ 336 object: "ResBundle", 337 locale: pseudoLocale, 338 name: "pseudomap.json", 339 sync: this.sync, 340 loadParams: this.loadParams, 341 callback: ilib.bind(this, function (map) { 342 this.pseudomap = (!map || JSUtils.isEmpty(map)) ? ResBundle.defaultPseudo : map; 343 if (typeof(onLoad) === 'function') { 344 onLoad(this); 345 } 346 }) 347 }); 348 }, 349 350 /** 351 * Return the locale of this resource bundle. 352 * @return {Locale} the locale of this resource bundle object 353 */ 354 getLocale: function () { 355 return this.locale; 356 }, 357 358 /** 359 * Return the name of this resource bundle. This corresponds to the name option 360 * given to the constructor. 361 * @return {string} name of the the current instance 362 */ 363 getName: function () { 364 return this.baseName; 365 }, 366 367 /** 368 * Return the type of this resource bundle. This corresponds to the type option 369 * given to the constructor. 370 * @return {string} type of the the current instance 371 */ 372 getType: function () { 373 return this.type; 374 }, 375 376 percentRE: new RegExp("%(\\d+\\$)?([\\-#\\+ 0,\\(])*(\\d+)?(\\.\\d+)?(h|hh|l|ll|j|z|t|L|q)?[diouxXfFeEgGaAcspnCS%@]"), 377 378 /** 379 * @private 380 * Pseudo-translate a string 381 */ 382 _pseudo: function (str) { 383 if (!str) { 384 return undefined; 385 } 386 var ret = "", i; 387 for (i = 0; i < str.length; i++) { 388 if (this.type !== "raw") { 389 if (this.type === "html" || this.type === "xml") { 390 if (str.charAt(i) === '<') { 391 ret += str.charAt(i++); 392 while (i < str.length && str.charAt(i) !== '>') { 393 ret += str.charAt(i++); 394 } 395 } else if (str.charAt(i) === '&') { 396 ret += str.charAt(i++); 397 while (i < str.length && str.charAt(i) !== ';' && str.charAt(i) !== ' ') { 398 ret += str.charAt(i++); 399 } 400 } else if (str.charAt(i) === '\\' && str.charAt(i+1) === "u") { 401 ret += str.substring(i, i+6); 402 i += 6; 403 } 404 } else if (this.type === "c") { 405 if (str.charAt(i) === "%") { 406 var m = this.percentRE.exec(str.substring(i)); 407 if (m && m.length) { 408 // console.log("Match found: " + JSON.stringify(m[0].replace("%", "%%"))); 409 ret += m[0]; 410 i += m[0].length; 411 } 412 } 413 } else if (this.type === "ruby") { 414 if (str.charAt(i) === "%" && i < str.length && str.charAt(i+1) !== "{") { 415 ret += str.charAt(i++); 416 while (i < str.length && str.charAt(i) !== '%') { 417 ret += str.charAt(i++); 418 } 419 } 420 } else if (this.type === "template") { 421 if (str.charAt(i) === '<' && str.charAt(i+1) === '%') { 422 ret += str.charAt(i++); 423 ret += str.charAt(i++); 424 while (i < str.length && (str.charAt(i) !== '>' || str.charAt(i-1) !== '%')) { 425 ret += str.charAt(i++); 426 } 427 } else if (str.charAt(i) === '&') { 428 ret += str.charAt(i++); 429 while (i < str.length && str.charAt(i) !== ';' && str.charAt(i) !== ' ') { 430 ret += str.charAt(i++); 431 } 432 } else if (str.charAt(i) === '\\' && str.charAt(i+1) === "u") { 433 ret += str.substring(i, i+6); 434 i += 6; 435 } 436 } 437 if (i < str.length) { 438 if (str.charAt(i) === '{') { 439 ret += str.charAt(i++); 440 while (i < str.length && str.charAt(i) !== '}') { 441 ret += str.charAt(i++); 442 } 443 if (i < str.length) { 444 ret += str.charAt(i); 445 } 446 } else { 447 ret += this.pseudomap[str.charAt(i)] || str.charAt(i); 448 } 449 } 450 } else { 451 ret += this.pseudomap[str.charAt(i)] || str.charAt(i); 452 } 453 } 454 if (this.lengthen) { 455 var add; 456 if (ret.length <= 20) { 457 add = Math.round(ret.length / 2); 458 } else if (ret.length > 20 && ret.length <= 40) { 459 add = Math.round(ret.length / 3); 460 } else { 461 add = Math.round(ret.length / 5); 462 } 463 for (i = add-1; i >= 0; i--) { 464 ret += (i % 10); 465 } 466 } 467 if (this.locale.getScript() === "Hans" || this.locale.getScript() === "Hant" || 468 this.locale.getScript() === "Hani" || 469 this.locale.getScript() === "Hrkt" || this.locale.getScript() === "Jpan" || 470 this.locale.getScript() === "Hira" || this.locale.getScript() === "Kana" ) { 471 // simulate Asian languages by getting rid of all the spaces 472 ret = ret.replace(/ /g, ""); 473 } 474 return ret; 475 }, 476 477 /** 478 * @private 479 * Escape html characters in the output. 480 */ 481 _escapeXml: function (str) { 482 str = str.replace(/&/g, '&'); 483 str = str.replace(/</g, '<'); 484 str = str.replace(/>/g, '>'); 485 return str; 486 }, 487 488 /** 489 * @private 490 * @param {string} str the string to unescape 491 */ 492 _unescapeXml: function (str) { 493 str = str.replace(/&/g, '&'); 494 str = str.replace(/</g, '<'); 495 str = str.replace(/>/g, '>'); 496 return str; 497 }, 498 499 /** 500 * @private 501 * Create a key name out of a source string. All this does so far is 502 * compress sequences of white space into a single space on the assumption 503 * that this doesn't really change the meaning of the string, and therefore 504 * all such strings that compress to the same thing should share the same 505 * translation. 506 * @param {null|string=} source the source string to make a key out of 507 */ 508 _makeKey: function (source) { 509 if (!source) return undefined; 510 var key = source.replace(/\s+/gm, ' '); 511 return (this.type === "xml" || this.type === "html") ? this._unescapeXml(key) : key; 512 }, 513 514 /** 515 * @private 516 */ 517 _getStringSingle: function(source, key, escapeMode) { 518 if (!source && !key) return new IString(""); 519 520 var trans; 521 if (this.locale.isPseudo()) { 522 var str = source ? source : this.map[key]; 523 trans = this._pseudo(str || key); 524 } else { 525 var keyName = key || this._makeKey(source); 526 if (typeof(this.map[keyName]) !== 'undefined') { 527 trans = this.map[keyName]; 528 } else if (this.missing === "pseudo") { 529 trans = this._pseudo(source || key); 530 } else if (this.missing === "empty") { 531 trans = ""; 532 } else { 533 trans = source; 534 } 535 } 536 537 if (escapeMode && escapeMode !== "none") { 538 if (escapeMode === "default") { 539 escapeMode = this.type; 540 } 541 if (escapeMode === "xml" || escapeMode === "html") { 542 trans = this._escapeXml(trans); 543 } else if (escapeMode === "js" || escapeMode === "attribute") { 544 trans = trans.replace(/'/g, "\\\'").replace(/"/g, "\\\""); 545 } 546 } 547 if (trans === undefined) { 548 return undefined; 549 } else { 550 var ret = new IString(trans); 551 ret.setLocale(this.locale.getSpec(), true, this.loadParams); // no callback 552 return ret; 553 } 554 }, 555 556 /** 557 * Return a localized string, array, or object. This method can localize individual 558 * strings or arrays of strings.<p> 559 * 560 * If the source parameter is a string, the translation of that string is looked 561 * up and returned. If the source parameter is an array of strings, then the translation 562 * of each of the elements of that array is looked up, and an array of translated strings 563 * is returned. <p> 564 * 565 * If any string is not found in the loaded set of 566 * resources, the original source string is returned. If the key is not given, 567 * then the source string itself is used as the key. In the case where the 568 * source string is used as the key, the whitespace is compressed down to 1 space 569 * each, and the whitespace at the beginning and end of the string is trimmed.<p> 570 * 571 * The escape mode specifies what type of output you are escaping the returned 572 * string for. Modes are similar to the types: 573 * 574 * <ul> 575 * <li>"html" -- prevents HTML injection by escaping the characters < > and & 576 * <li>"xml" -- currently same as "html" mode 577 * <li>"js" -- prevents breaking Javascript syntax by backslash escaping all quote and 578 * double-quote characters 579 * <li>"attribute" -- meant for HTML attribute values. Currently this is the same as 580 * "js" escape mode. 581 * <li>"default" -- use the type parameter from the constructor as the escape mode as well 582 * <li>"none" or undefined -- no escaping at all. 583 * </ul> 584 * 585 * The type parameter of the constructor specifies what type of strings this bundle 586 * is operating upon. This allows pseudo-translation and automatic key generation 587 * to happen properly by telling this class how to parse the string. The escape mode 588 * for this method is different in that it specifies how this string will be used in 589 * the calling code and therefore how to escape it properly.<p> 590 * 591 * For example, a section of Javascript code may be constructing an HTML snippet in a 592 * string to add to the web page. In this case, the type parameter in the constructor should 593 * be "html" so that the source string can be parsed properly, but the escape mode should 594 * be "js" so that the output string can be used in Javascript without causing syntax 595 * errors. 596 * 597 * @param {?string|Array.<string>=} source the source string or strings to translate 598 * @param {?string|Array.<string>=} key optional name of the key, if any 599 * @param {?string=} escapeMode escape mode, if any 600 * @return {IString|Array.<IString>|undefined} the translation of the given source/key or undefined 601 * if the translation is not found and the source is undefined 602 */ 603 getString: function (source, key, escapeMode) { 604 if (!source && !key) return new IString(""); 605 606 //if (typeof(source) === "object") { 607 // TODO localize objects 608 //} else 609 610 if (ilib.isArray(source)) { 611 return source.map(ilib.bind(this, function(str) { 612 return typeof(str) === "string" ? this._getStringSingle(str, key, escapeMode) : str; 613 })); 614 } else { 615 return this._getStringSingle(source, key, escapeMode); 616 } 617 }, 618 619 /** 620 * Return a localized string as an intrinsic Javascript String object. This does the same thing as 621 * the getString() method, but it returns a regular Javascript string instead of 622 * and IString instance. This means it cannot be formatted with the format() 623 * method without being wrapped in an IString instance first. 624 * 625 * @param {?string|Array.<string>=} source the source string to translate 626 * @param {?string|Array.<string>=} key optional name of the key, if any 627 * @param {?string=} escapeMode escape mode, if any 628 * @return {string|Array.<string>|undefined} the translation of the given source/key or undefined 629 * if the translation is not found and the source is undefined 630 */ 631 getStringJS: function(source, key, escapeMode) { 632 if (typeof(source) === 'undefined' && typeof(key) === 'undefined') { 633 return undefined; 634 } 635 //if (typeof(source) === "object") { 636 // TODO localize objects 637 //} else 638 639 if (ilib.isArray(source)) { 640 return this.getString(source, key, escapeMode).map(function(str) { 641 return (str && str instanceof IString) ? str.toString() : str; 642 }); 643 } else { 644 var s = this.getString(source, key, escapeMode); 645 return s ? s.toString() : undefined; 646 } 647 }, 648 649 /** 650 * Return true if the current bundle contains a translation for the given key and 651 * source. The 652 * getString method will always return a string for any given key and source 653 * combination, so it cannot be used to tell if a translation exists. Either one 654 * or both of the source and key must be specified. If both are not specified, 655 * this method will return false. 656 * 657 * @param {?string=} source source string to look up 658 * @param {?string=} key key to look up 659 * @return {boolean} true if this bundle contains a translation for the key, and 660 * false otherwise 661 */ 662 containsKey: function(source, key) { 663 if (typeof(source) === 'undefined' && typeof(key) === 'undefined') { 664 return false; 665 } 666 667 var keyName = key || this._makeKey(source); 668 return typeof(this.map[keyName]) !== 'undefined'; 669 }, 670 671 /** 672 * Return the merged resources as an entire object. When loading resources for a 673 * locale that are not just a set of translated strings, but instead an entire 674 * structured javascript object, you can gain access to that object via this call. This method 675 * will ensure that all the of the parts of the object are correct for the locale.<p> 676 * 677 * For pre-assembled data, it starts by loading <i>ilib.data[name]</i>, where 678 * <i>name</i> is the base name for this set of resources. Then, it successively 679 * merges objects in the base data using progressively more locale-specific data. 680 * It loads it in this order from <i>ilib.data</i>: 681 * 682 * <ol> 683 * <li> language 684 * <li> region 685 * <li> language_script 686 * <li> language_region 687 * <li> region_variant 688 * <li> language_script_region 689 * <li> language_region_variant 690 * <li> language_script_region_variant 691 * </ol> 692 * 693 * For dynamically loaded data, the code attempts to load the same sequence as 694 * above, but with slash path separators instead of underscores.<p> 695 * 696 * Loading the resources this way allows the program to share resources between all 697 * locales that share a common language, region, or script. As a 698 * general rule-of-thumb, resources should be as generic as possible in order to 699 * cover as many locales as possible. 700 * 701 * @return {Object} returns the object that is the basis for this resources instance 702 */ 703 getResObj: function () { 704 return this.map; 705 } 706 }; 707 708 module.exports = ResBundle; 709