1 /* 2 * Utils.js - Core utility routines 3 * 4 * Copyright © 2012-2015, 2018-2019, 2021 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 var ilib = require("./ilib.js"); 21 var Locale = require("./Locale.js"); 22 var JSUtils = require("./JSUtils.js"); 23 var Path = require("./Path.js"); 24 var ISet = require("./ISet.js"); 25 26 var Utils = {}; 27 28 /** 29 * Return the property name inside of ilib.data that corresponds to the given locale data file. 30 * 31 * @private 32 * @param {String} basename the basename of the file 33 * @param {String} pathname the path from the root to the base file which usually encodes the 34 * locale of the file 35 * @param {String=} root the root directory of the file or undefined for the standard locale dir 36 */ 37 function getPropertyName(basename, pathname, root) { 38 var bits = [ basename ]; 39 if (root) { 40 bits = bits.concat(root.split("\/")); 41 } 42 if (pathname) { 43 bits = bits.concat(pathname.split("\/")); 44 } 45 return bits.join('_'); 46 } 47 48 /** 49 * Return an array of locales that represent the sublocales of 50 * the given locale. These sublocales are intended to be used 51 * to load locale data. Each sublocale might be represented 52 * separately by files on disk in order to share them with other 53 * locales that have the same sublocales. The sublocales are 54 * given in the order that they should be loaded, which is 55 * least specific to most specific.<p> 56 * 57 * For example, the locale "en-US" would have the sublocales 58 * "root", "en", "und-US", and "en-US".<p> 59 * 60 * <h4>Variations</h4> 61 * 62 * With only language and region specified, the following 63 * sequence of sublocales will be generated:<p> 64 * 65 * <pre> 66 * root 67 * language 68 * und-region 69 * language-region 70 * </pre> 71 * 72 * With only language and script specified:<p> 73 * 74 * <pre> 75 * root 76 * language 77 * language-script 78 * </pre> 79 * 80 * With only script and region specified:<p> 81 * 82 * <pre> 83 * root 84 * und-region 85 * </pre> 86 * 87 * With only region and variant specified:<p> 88 * 89 * <pre> 90 * root 91 * und-region 92 * region-variant 93 * </pre> 94 * 95 * With only language, script, and region specified:<p> 96 * 97 * <pre> 98 * root 99 * language 100 * und-region 101 * language-script 102 * language-region 103 * language-script-region 104 * </pre> 105 * 106 * With only language, region, and variant specified:<p> 107 * 108 * <pre> 109 * root 110 * language 111 * und-region 112 * language-region 113 * und-region-variant 114 * language-region-variant 115 * </pre> 116 * 117 * With all parts specified:<p> 118 * 119 * <pre> 120 * root 121 * language 122 * und-region 123 * language-script 124 * language-region 125 * und-region-variant 126 * language-script-region 127 * language-region-variant 128 * language-script-region-variant 129 * </pre> 130 * 131 * @static 132 * @param {Locale|String} locale the locale to find the sublocales for 133 * @return {Array.<string>} An array of locale specifiers that 134 * are the sublocales of the given on 135 */ 136 Utils.getSublocales = function(locale) { 137 var ret = ["root"]; 138 var loc = typeof(locale) === "string" ? new Locale(locale) : locale; 139 var lang = loc.getLanguage(); 140 var region = loc.getRegion(); 141 var script = loc.getScript(); 142 var variant = loc.getVariant(); 143 144 if (lang) { 145 ret.push(lang); 146 } 147 if (region) { 148 ret.push('und-' + region); 149 } 150 151 if (lang) { 152 if (script) { 153 ret.push(lang + '-' + script); 154 } 155 if (region) { 156 ret.push(lang + '-' + region); 157 } 158 if (variant) { 159 ret.push(lang + '-' + variant); 160 } 161 } 162 163 if (region && variant) { 164 ret.push("und-" + region + '-' + variant); 165 } 166 167 if (lang) { 168 if (script && region) { 169 ret.push(lang + '-' + script + '-' + region); 170 } 171 if (script && variant) { 172 ret.push(lang + '-' + script + '-' + variant); 173 } 174 if (region && variant) { 175 ret.push(lang + '-' + region + '-' + variant); 176 } 177 if (script && region && variant) { 178 ret.push(lang + '-' + script + '-' + region + '-' + variant); 179 } 180 } 181 return ret; 182 }; 183 184 /** 185 * Find and merge all the locale data for a particular prefix in the given locale 186 * and return it as a single javascript object. This merges the data in the 187 * correct order: 188 * 189 * <ol> 190 * <li>shared data (usually English) 191 * <li>data for language 192 * <li>data for language + region 193 * <li>data for language + region + script 194 * <li>data for language + region + script + variant 195 * </ol> 196 * 197 * It is okay for any of the above to be missing. This function will just skip the 198 * missing data. 199 * 200 * @static 201 * @param {string} prefix prefix under ilib.data of the data to merge 202 * @param {Locale} locale locale of the data being sought 203 * @param {boolean=} replaceArrays if true, replace the array elements in object1 with those in object2. 204 * If false, concatenate array elements in object1 with items in object2. 205 * @param {boolean=} returnOne if true, only return the most locale-specific data. If false, 206 * merge all the relevant locale data together. 207 * @param {string=} root root path if there is one 208 * @return {Object?} the merged locale data 209 */ 210 Utils.mergeLocData = function (prefix, locale, replaceArrays, returnOne, root) { 211 var data = undefined; 212 var loc = locale || new Locale(); 213 var mostSpecific; 214 215 data = {}; 216 217 mostSpecific = data; 218 219 Utils.getSublocales(loc).forEach(function(l) { 220 var property = getPropertyName(prefix, (l === "root") ? undefined : l.replace(/-/g, "/"), root); 221 222 if (ilib.data[property]) { 223 if (returnOne) { 224 mostSpecific = ilib.data[property]; 225 } else { 226 data = JSUtils.merge(data, ilib.data[property], replaceArrays); 227 } 228 } 229 }); 230 231 return returnOne ? mostSpecific : data; 232 }; 233 234 235 /** 236 * Return an array of relative path names for the 237 * files that represent the data for the given locale.<p> 238 * 239 * Note that to prevent the situation where a directory for 240 * a language exists next to the directory for a region where 241 * the language code and region code differ only by case, the 242 * plain region directories are located under the special 243 * "undefined" language directory which has the ISO code "und". 244 * The reason is that some platforms have case-insensitive 245 * file systems, and you cannot have 2 directories with the 246 * same name which only differ by case. For example, "es" is 247 * the ISO 639 code for the language "Spanish" and "ES" is 248 * the ISO 3166 code for the region "Spain", so both the 249 * directories cannot exist underneath "locale". The region 250 * therefore will be loaded from "und/ES" instead.<p> 251 * 252 * <h4>Variations</h4> 253 * 254 * With only language and region specified, the following 255 * sequence of paths will be generated:<p> 256 * 257 * <pre> 258 * language 259 * und/region 260 * language/region 261 * </pre> 262 * 263 * With only language and script specified:<p> 264 * 265 * <pre> 266 * language 267 * language/script 268 * </pre> 269 * 270 * With only script and region specified:<p> 271 * 272 * <pre> 273 * und/region 274 * </pre> 275 * 276 * With only region and variant specified:<p> 277 * 278 * <pre> 279 * und/region 280 * region/variant 281 * </pre> 282 * 283 * With only language, script, and region specified:<p> 284 * 285 * <pre> 286 * language 287 * und/region 288 * language/script 289 * language/region 290 * language/script/region 291 * </pre> 292 * 293 * With only language, region, and variant specified:<p> 294 * 295 * <pre> 296 * language 297 * und/region 298 * language/region 299 * region/variant 300 * language/region/variant 301 * </pre> 302 * 303 * With all parts specified:<p> 304 * 305 * <pre> 306 * language 307 * und/region 308 * language/script 309 * language/region 310 * region/variant 311 * language/script/region 312 * language/region/variant 313 * language/script/region/variant 314 * </pre> 315 * 316 * @static 317 * @param {Locale} locale load the files for this locale 318 * @param {string?} name the file name of each file to load without 319 * any path 320 * @return {Array.<string>} An array of relative path names 321 * for the files that contain the locale data 322 */ 323 Utils.getLocFiles = function(locale, name) { 324 var filename = name || "resources.json"; 325 var loc = locale || new Locale(); 326 327 return Utils.getSublocales(loc).map(function(l) { 328 return (l === "root") ? filename : Path.join(l.replace(/-/g, "/"), filename); 329 }); 330 }; 331 332 /** 333 * Load data using the new loader object or via the old function callback. 334 * @static 335 * @private 336 */ 337 Utils._callLoadData = function (files, sync, params, root, callback) { 338 // console.log("Utils._callLoadData called"); 339 if (typeof(ilib._load) === 'function') { 340 // console.log("Utils._callLoadData: calling as a regular function"); 341 return ilib._load(files, sync, params, callback); 342 } else if (typeof(ilib._load) === 'object' && typeof(ilib._load.loadFiles) === 'function') { 343 // console.log("Utils._callLoadData: calling as an object"); 344 return ilib._load.loadFiles(files, sync, params, callback, root); 345 } 346 347 // console.log("Utils._callLoadData: not calling. Type is " + typeof(ilib._load) + " and instanceof says " + (ilib._load instanceof Loader)); 348 return undefined; 349 }; 350 351 function getPropertyNameFromFile(basename, filepath, root) { 352 var dir = Path.dirname(filepath); 353 return getPropertyName(basename, (dir === "." || dir === "/" || dir === "..") ? undefined : dir, root); 354 } 355 356 /** 357 * Return true if the locale data corresponding to the given pathname is not already loaded 358 * or assembled. 359 * 360 * @private 361 * @param basename 362 * @param locale 363 * @returns 364 */ 365 function dataNotExists(basename, pathname, root) { 366 return !ilib.data[getPropertyNameFromFile(basename, pathname, root)]; 367 } 368 369 /** 370 * Find locale data or load it in. If the data with the given name is preassembled, it will 371 * find the data in ilib.data. If the data is not preassembled but there is a loader function, 372 * this function will call it to load the data. Otherwise, the callback will be called with 373 * undefined as the data. This function will create a cache under the given class object. 374 * If data was successfully loaded, it will be set into the cache so that future access to 375 * the same data for the same locale is much quicker.<p> 376 * 377 * The parameters can specify any of the following properties:<p> 378 * 379 * <ul> 380 * <li><i>name</i> - String. The name of the file being loaded. Default: ResBundle.json 381 * <li><i>object</i> - String. The name of the class attempting to load data. This is used to differentiate parts of the cache. 382 * <li><i>locale</i> - Locale. The locale for which data is loaded. Default is the current locale. 383 * <li><i>nonlocale</i> - boolean. If true, the data being loaded is not locale-specific. 384 * <li><i>type</i> - String. Type of file to load. This can be "json" or "other" type. Default: "json" 385 * <li><i>replace</i> - boolean. When merging json objects, this parameter controls whether to merge arrays 386 * or have arrays replace each other. If true, arrays in child objects replace the arrays in parent 387 * objects. When false, the arrays in child objects are concatenated with the arrays in parent objects. 388 * <li><i>root</i> - String. If provided, look in this root directory first for files, and then fall back 389 * to the standard include paths if they are not found in this root. If not provided, just search the 390 * standard include paths. 391 * <li><i>loadParams</i> - Object. An object with parameters to pass to the loader function 392 * <li><i>sync</i> - boolean. Whether or not to load the data synchronously 393 * <li><i>callback</i> - function(?)=. callback Call back function to call when the data is available. 394 * Data is not returned from this method, so a callback function is mandatory. 395 * </ul> 396 * 397 * @static 398 * @param {Object} params Parameters configuring how to load the files (see above) 399 */ 400 Utils.loadData = function(params) { 401 var name = "resources.json", 402 locale = new Locale(ilib.getLocale()), 403 sync = false, 404 type = undefined, 405 loadParams = {}, 406 callback = undefined, 407 nonlocale = false, 408 replace = false, 409 root, 410 basename; 411 412 if (!params || typeof(params.callback) !== 'function') { 413 throw "Utils.loadData called without a callback. It must have a callback to work."; 414 } 415 416 if (params.name) { 417 name = params.name; 418 } 419 if (params.locale) { 420 locale = (typeof(params.locale) === 'string') ? new Locale(params.locale) : params.locale; 421 } 422 if (params.type) { 423 type = params.type; 424 } 425 if (params.loadParams) { 426 loadParams = params.loadParams; 427 } 428 if (params.sync) { 429 sync = params.sync; 430 } 431 if (params.nonlocale) { 432 nonlocale = !!params.nonlocale; 433 } 434 if (typeof(params.replace) === 'boolean') { 435 replace = params.replace; 436 } 437 438 root = params.root; 439 callback = params.callback; 440 441 if (!type) { 442 var dot = name.lastIndexOf("."); 443 type = (dot !== -1) ? name.substring(dot+1) : "text"; 444 } 445 446 if (typeof(ilib.data.cache) === "undefined") { 447 ilib.data.cache = {}; 448 } 449 if (typeof(ilib.data.cache.fileSet) === "undefined") { 450 ilib.data.cache.fileSet = new ISet(); 451 } 452 453 var data, returnOne = ((loadParams && loadParams.returnOne) || type !== "json"); 454 455 basename = name.substring(0, name.lastIndexOf(".")).replace(/[\.:\(\)\/\\\+\-]/g, "_"); 456 457 if (ilib._cacheMerged) { 458 if (typeof(ilib.data.cache.merged) === "undefined") { 459 ilib.data.cache.merged = {}; 460 } 461 var spec = ((!nonlocale && locale.getSpec().replace(/-/g, '_')) || "root") + "," + basename + "," + String(JSUtils.hashCode(loadParams)); 462 if (typeof(ilib.data.cache.merged[spec]) !== 'undefined') { 463 // cache hit! 464 callback(ilib.data.cache.merged[spec]); 465 return; 466 } 467 } 468 469 if (typeof(ilib._load) !== 'undefined') { 470 // We have a loader, so we can figure out which json files are loaded already and 471 // which are not so that we can load the missing ones. 472 // the data is not preassembled, so attempt to load it dynamically 473 var files = nonlocale ? [ name || "resources.json" ] : Utils.getLocFiles(locale, name); 474 475 var isPath = ilib._load.isMultiPaths; 476 477 if (typeof(isPath) === "undefined" || isPath === false){ 478 // find the ones we haven't loaded before 479 files = files.filter(ilib.bind(this, function(file) { 480 return !ilib.data.cache.fileSet.has(Path.join(root, file)) && dataNotExists(basename, file, root); 481 })); 482 } 483 484 if (files.length) { 485 Utils._callLoadData(files, sync, loadParams, root, ilib.bind(this, function(arr) { 486 for (var i = 0; i < files.length; i++) { 487 if (arr[i]) { 488 var property = nonlocale ? basename : getPropertyNameFromFile(basename, files[i], root); 489 490 if (isPath || !ilib.data[property]) { 491 ilib.data[property] = arr[i]; 492 } 493 } 494 495 ilib.data.cache.fileSet.add(Path.join(root, files[i])); 496 } 497 498 if (!nonlocale) { 499 data = Utils.mergeLocData(basename, locale, replace, returnOne, root); 500 if (ilib._cacheMerged) ilib.data.cache.merged[spec] = data; 501 } else { 502 data = ilib.data[basename]; 503 } 504 505 callback(data); 506 })); 507 508 return; 509 } 510 // otherwise the code below will return the already-loaded data 511 } 512 513 // No loader, or data already loaded? Then use whatever data we have already in ilib.data 514 if (!nonlocale) { 515 data = Utils.mergeLocData(basename, locale, replace, returnOne, root); 516 if (ilib._cacheMerged) ilib.data.cache.merged[spec] = data; 517 } else { 518 data = ilib.data[basename]; 519 } 520 521 callback(data); 522 }; 523 524 module.exports = Utils; 525