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