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