1/****************************************************************************** 2 * jquery.i18n.properties 3 * 4 * Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 5 * MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 6 * 7 * @version 1.2.7 8 * @url https://github.com/jquery-i18n-properties/jquery-i18n-properties 9 * @inspiration Localisation assistance for jQuery (http://keith-wood.name/localisation.html) 10 * by Keith Wood (kbwood{at}iinet.com.au) June 2007 11 * 12 *****************************************************************************/ 13 14(function ($) { 15 16 $.i18n = {}; 17 18 /** 19 * Map holding bundle keys if mode is 'map' or 'both'. Values of this can also be an 20 * Object, in which case the key is a namespace. 21 */ 22 $.i18n.map = {}; 23 24 var debug = function (message) { 25 window.console && console.log('i18n::' + message); 26 }; 27 28 /** 29 * Load and parse message bundle files (.properties), 30 * making bundles keys available as javascript variables. 31 * 32 * i18n files are named <name>.js, or <name>_<language>.js or <name>_<language>_<country>.js 33 * Where: 34 * The <language> argument is a valid ISO Language Code. These codes are the lower-case, 35 * two-letter codes as defined by ISO-639. You can find a full list of these codes at a 36 * number of sites, such as: http://www.loc.gov/standards/iso639-2/englangn.html 37 * The <country> argument is a valid ISO Country Code. These codes are the upper-case, 38 * two-letter codes as defined by ISO-3166. You can find a full list of these codes at a 39 * number of sites, such as: http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html 40 * 41 * Sample usage for a bundles/Messages.properties bundle: 42 * $.i18n.properties({ 43 * name: 'Messages', 44 * language: 'en_US', 45 * path: 'bundles' 46 * }); 47 * @param name (string/string[], optional) names of file to load (eg, 'Messages' or ['Msg1','Msg2']). Defaults to "Messages" 48 * @param language (string, optional) language/country code (eg, 'en', 'en_US', 'pt_BR'). if not specified, language reported by the browser will be used instead. 49 * @param path (string, optional) path of directory that contains file to load 50 * @param mode (string, optional) whether bundles keys are available as JavaScript variables/functions or as a map (eg, 'vars' or 'map') 51 * @param debug (boolean, optional) whether debug statements are logged at the console 52 * @param cache (boolean, optional) whether bundles should be cached by the browser, or forcibly reloaded on each page load. Defaults to false (i.e. forcibly reloaded) 53 * @param encoding (string, optional) the encoding to request for bundles. Property file resource bundles are specified to be in ISO-8859-1 format. Defaults to UTF-8 for backward compatibility. 54 * @param callback (function, optional) callback function to be called after script is terminated 55 */ 56 $.i18n.properties = function (settings) { 57 58 var defaults = { 59 name: 'Messages', 60 language: '', 61 path: '', 62 namespace: null, 63 mode: 'vars', 64 cache: false, 65 debug: false, 66 encoding: 'UTF-8', 67 async: false, 68 callback: null 69 }; 70 71 settings = $.extend(defaults, settings); 72 73 if (settings.namespace && typeof settings.namespace == 'string') { 74 // A namespace has been supplied, initialise it. 75 if (settings.namespace.match(/^[a-z]*$/)) { 76 $.i18n.map[settings.namespace] = {}; 77 } else { 78 debug('Namespaces can only be lower case letters, a - z'); 79 settings.namespace = null; 80 } 81 } 82 83 // Ensure a trailing slash on the path 84 if (!settings.path.match(/\/$/)) settings.path += '/'; 85 86 // Try to ensure that we have at a least a two letter language code 87 settings.language = this.normaliseLanguageCode(settings); 88 89 // Ensure an array 90 var files = (settings.name && settings.name.constructor === Array) ? settings.name : [settings.name]; 91 92 // A locale is at least a language code which means at least two files per name. If 93 // we also have a country code, thats an extra file per name. 94 settings.totalFiles = (files.length * 2) + ((settings.language.length >= 5) ? files.length : 0); 95 if (settings.debug) { 96 debug('totalFiles: ' + settings.totalFiles); 97 } 98 99 settings.filesLoaded = 0; 100
101 files.forEach(function (file) { 102 103 var defaultFileName, shortFileName, longFileName, fileNames; 104 // 1. load base (eg, Messages.properties) 105 defaultFileName = settings.path + file + '.properties'; 106 // 2. with language code (eg, Messages_pt.properties) 107 var shortCode = settings.language.substring(0, 2); 108 shortFileName = settings.path + file + '_' + shortCode + '.properties'; 109 // 3. with language code and country code (eg, Messages_pt_BR.properties) 110 if (settings.language.length >= 5) { 111 var longCode = settings.language.substring(0, 5); 112 longFileName = settings.path + file + '_' + longCode + '.properties'; 113 fileNames = [defaultFileName, shortFileName, longFileName]; 114 } else { 115 fileNames = [defaultFileName, shortFileName]; 116 } 117 loadAndParseFiles(fileNames, settings); 118 }); 119 120 // call callback 121 if (settings.callback && !settings.async) { 122 settings.callback(); 123 } 124 }; // properties 125 126 /** 127 * When configured with mode: 'map', allows access to bundle values by specifying its key. 128 * Eg, jQuery.i18n.prop('com.company.bundles.menu_add') 129 */ 130 $.i18n.prop = function (key /* Add parameters as function arguments as necessary */) { 131 132 var args = [].slice.call(arguments); 133 134 var phvList, namespace; 135 if (args.length == 2) { 136 if ($.isArray(args[1])) { 137 // An array was passed as the second parameter, so assume it is the list of place holder values. 138 phvList = args[1]; 139 } else if (typeof args[1] === 'object') { 140 // Second argument is an options object {namespace: 'mynamespace', replacements: ['egg', 'nog']} 141 namespace = args[1].namespace; 142 var replacements = args[1].replacements; 143 args.splice(-1, 1); 144 if (replacements) { 145 Array.prototype.push.apply(args, replacements); 146 } 147 } 148 } 149 150 var value = (namespace) ? $.i18n.map[namespace][key] : $.i18n.map[key]; 151 if (value === null) { 152 return '[' + ((namespace) ? namespace + '#' + key : key) + ']'; 153 } 154 155 // Place holder replacement 156 /** 157 * Tested with: 158 * test.t1=asdf ''{0}'' 159 * test.t2=asdf '{0}' '{1}'{1}'zxcv 160 * test.t3=This is \"a quote" 'a''{0}''s'd{fgh{ij' 161 * test.t4="'''{'0}''" {0}{a} 162 * test.t5="'''{0}'''" {1} 163 * test.t6=a {1} b {0} c 164 * test.t7=a 'quoted \\ s\ttringy' \t\t x 165 * 166 * Produces: 167 * test.t1, p1 ==> asdf 'p1' 168 * test.t2, p1 ==> asdf {0} {1}{1}zxcv 169 * test.t3, p1 ==> This is "a quote" a'{0}'sd{fgh{ij 170 * test.t4, p1 ==> "'{0}'" p1{a} 171 * test.t5, p1 ==> "'{0}'" {1} 172 * test.t6, p1 ==> a {1} b p1 c 173 * test.t6, p1, p2 ==> a p2 b p1 c 174 * test.t6, p1, p2, p3 ==> a p2 b p1 c 175 * test.t7 ==> a quoted \ s tringy x 176 */ 177 178 var i; 179 if (typeof(value) == 'string') { 180 // Handle escape characters. Done separately from the tokenizing loop below because escape characters are 181 // active in quoted strings. 182 i = 0; 183 while ((i = value.indexOf('\\', i)) != -1) { 184 if (value.charAt(i + 1) == 't') { 185 value = value.substring(0, i) + '\t' + value.substring((i++) + 2); // tab 186 } else if (value.charAt(i + 1) == 'r') { 187 value = value.substring(0, i) + '\r' + value.substring((i++) + 2); // return 188 } else if (value.charAt(i + 1) == 'n') { 189 value = value.substring(0, i) + '\n' + value.substring((i++) + 2); // line feed 190 } else if (value.charAt(i + 1) == 'f') { 191 value = value.substring(0, i) + '\f' + value.substring((i++) + 2); // form feed 192 } else if (value.charAt(i + 1) == '\\') { 193 value = value.substring(0, i) + '\\' + value.substring((i++) + 2); // \ 194 } else { 195 value = value.substring(0, i) + value.substring(i + 1); // Quietly drop the character 196 } 197 } 198 199 // Lazily convert the string to a list of tokens. 200 var arr = [], j, index; 201 i = 0; 202 while (i < value.length) { 203 if (value.charAt(i) == '\'') { 204 // Handle quotes 205 if (i == value.length - 1) { 206 value = value.substring(0, i); // Silently drop the trailing quote 207 } else if (value.charAt(i + 1) == '\'') { 208 value = value.substring(0, i) + value.substring(++i); // Escaped quote 209 } else { 210 // Quoted string 211 j = i + 2; 212 while ((j = value.indexOf('\'', j)) != -1) { 213 if (j == value.length - 1 || value.charAt(j + 1) != '\'') { 214 // Found start and end quotes. Remove them 215 value = value.substring(0, i) + value.substring(i + 1, j) + value.substring(j + 1); 216 i = j - 1; 217 break; 218 } else { 219 // Found a double quote, reduce to a single quote. 220 value = value.substring(0, j) + value.substring(++j); 221 } 222 } 223 224 if (j == -1) { 225 // There is no end quote. Drop the start quote 226 value = value.substring(0, i) + value.substring(i + 1); 227 } 228 } 229 } else if (value.charAt(i) == '{') { 230 // Beginning of an unquoted place holder. 231 j = value.indexOf('}', i + 1); 232 if (j == -1) { 233 i++; // No end. Process the rest of the line. Java would throw an exception 234 } else { 235 // Add 1 to the index so that it aligns with the function arguments. 236 index = parseInt(value.substring(i + 1, j)); 237 if (!isNaN(index) && index >= 0) { 238 // Put the line thus far (if it isn't empty) into the array 239 var s = value.substring(0, i); 240 if (s !== "") { 241 arr.push(s); 242 } 243 // Put the parameter reference into the array 244 arr.push(index); 245 // Start the processing over again starting from the rest of the line. 246 i = 0; 247 value = value.substring(j + 1); 248 } else { 249 i = j + 1; // Invalid parameter. Leave as is. 250 } 251 } 252 } else { 253 i++; 254 } 255 } // while 256 257 // Put the remainder of the no-empty line into the array. 258 if (value !== "") { 259 arr.push(value); 260 } 261 value = arr; 262 263 // Make the array the value for the entry. 264 if (namespace) { 265 $.i18n.map[settings.namespace][key] = arr; 266 } else { 267 $.i18n.map[key] = arr; 268 } 269 } 270 271 if (value.length === 0) { 272 return ""; 273 } 274 if (value.length == 1 && typeof(value[0]) == "string") { 275 return value[0]; 276 } 277 278 var str = ""; 279 for (i = 0, j = value.length; i < j; i++) { 280 if (typeof(value[i]) == "string") { 281 str += value[i]; 282 } else if (phvList && value[i] < phvList.length) { 283 // Must be a number 284 str += phvList[value[i]]; 285 } else if (!phvList && value[i] + 1 < args.length) { 286 str += args[value[i] + 1]; 287 } else { 288 str += "{" + value[i] + "}"; 289 } 290 } 291 292 return str; 293 }; 294 295 function callbackIfComplete(settings) { 296 297 if (settings.debug) { 298 debug('callbackIfComplete()'); 299 debug('totalFiles: ' + settings.totalFiles); 300 debug('filesLoaded: ' + settings.filesLoaded); 301 } 302 303 if (settings.async) { 304 if (settings.filesLoaded === settings.totalFiles) { 305 if (settings.callback) { 306 settings.callback(); 307 } 308 } 309 } 310 } 311 312 function loadAndParseFiles(fileNames, settings) { 313 314 if (settings.debug) debug('loadAndParseFiles'); 315 316 if (fileNames !== null && fileNames.length > 0) { 317 loadAndParseFile(fileNames[0], settings, function () { 318 fileNames.shift(); 319 loadAndParseFiles(fileNames,settings); 320 }); 321 } else { 322 callbackIfComplete(settings); 323 } 324 } 325 326 /** Load and parse .properties files */ 327 function loadAndParseFile(filename, settings, nextFile) { 328 329 if (settings.debug) { 330 debug('loadAndParseFile(\'' + filename +'\')'); 331 debug('totalFiles: ' + settings.totalFiles); 332 debug('filesLoaded: ' + settings.filesLoaded); 333 } 334 335 if (filename !== null && typeof filename !== 'undefined') { 336 $.ajax({ 337 url: filename, 338 async: settings.async, 339 cache: settings.cache, 340 dataType: 'text', 341 success: function (data, status) { 342 343 if (settings.debug) { 344 debug('Succeeded in downloading ' + filename + '.'); 345 debug(data); 346 } 347 348 parseData(data, settings); 349 nextFile(); 350 }, 351 error: function (jqXHR, textStatus, errorThrown) { 352 353 if (settings.debug) { 354 debug('Failed to download or parse ' + filename + '. errorThrown: ' + errorThrown); 355 } 356 if (jqXHR.status === 404) { 357 settings.totalFiles -= 1; 358 } 359 nextFile(); 360 } 361 }); 362 } 363 } 364 365 /** Parse .properties files */ 366 function parseData(data, settings) { 367 368 var parsed = ''; 369 var lines = data.split(/\n/); 370 var regPlaceHolder = /(\{\d+})/g; 371 var regRepPlaceHolder = /\{(\d+)}/g; 372 var unicodeRE = /(\\u.{4})/ig; 373 for (var i=0,j=lines.length;i<j;i++) { 374 var line = lines[i]; 375 376 line = line.trim(); 377 if (line.length > 0 && line.match("^#") != "#") { // skip comments 378 var pair = line.split('='); 379 if (pair.length > 0) { 380 /** Process key & value */ 381 var name = decodeURI(pair[0]).trim(); 382 var value = pair.length == 1 ? "" : pair[1]; 383 // process multi-line values 384 while (value.search(/\\$/) != -1) { 385 value = value.substring(0, value.length - 1); 386 value += lines[++i].trimRight(); 387 } 388 // Put values with embedded '='s back together 389 for (var s = 2; s < pair.length; s++) { 390 value += '=' + pair[s]; 391 } 392 value = value.trim(); 393 394 /** Mode: bundle keys in a map */ 395 if (settings.mode == 'map' || settings.mode == 'both') { 396 // handle unicode chars possibly left out 397 var unicodeMatches = value.match(unicodeRE); 398 if (unicodeMatches) {
399 unicodeMatches.forEach(function (match) { 400 value = value.replace(match, unescapeUnicode(match)); 401 }); 402 } 403 // add to map 404 if (settings.namespace) { 405 $.i18n.map[settings.namespace][name] = value; 406 } else { 407 $.i18n.map[name] = value; 408 } 409 } 410 411 /** Mode: bundle keys as vars/functions */ 412 if (settings.mode == 'vars' || settings.mode == 'both') { 413 value = value.replace(/"/g, '\\"'); // escape quotation mark (") 414 415 // make sure namespaced key exists (eg, 'some.key') 416 checkKeyNamespace(name); 417 418 // value with variable substitutions 419 if (regPlaceHolder.test(value)) { 420 var parts = value.split(regPlaceHolder); 421 // process function args 422 var first = true; 423 var fnArgs = ''; 424 var usedArgs = []; 425 parts.forEach(function (part) { 426 427 if (regPlaceHolder.test(part) && (usedArgs.length === 0 || usedArgs.indexOf(part) == -1)) { 428 if (!first) { 429 fnArgs += ','; 430 } 431 fnArgs += part.replace(regRepPlaceHolder, 'v$1'); 432 usedArgs.push(part); 433 first = false; 434 } 435 }); 436 parsed += name + '=function(' + fnArgs + '){'; 437 // process function body 438 var fnExpr = '"' + value.replace(regRepPlaceHolder, '"+v$1+"') + '"'; 439 parsed += 'return ' + fnExpr + ';' + '};'; 440 // simple value 441 } else { 442 parsed += name + '="' + value + '";'; 443 } 444 } // END: Mode: bundle keys as vars/functions 445 } // END: if(pair.length > 0) 446 } // END: skip comments 447 } 448 eval(parsed); 449 settings.filesLoaded += 1; 450 } 451 452 /** Make sure namespace exists (for keys with dots in name) */ 453 // TODO key parts that start with numbers quietly fail. i.e. month.short.1=Jan 454 function checkKeyNamespace(key) { 455 456 var regDot = /\./; 457 if (regDot.test(key)) { 458 var fullname = ''; 459 var names = key.split(/\./); 460 for (var i=0,j=names.length;i<j;i++) { 461 var name = names[i]; 462 463 if (i > 0) { 464 fullname += '.'; 465 } 466 467 fullname += name; 468 if (eval('typeof ' + fullname + ' == "undefined"')) { 469 eval(fullname + '={};'); 470 } 471 } 472 } 473 } 474 475 /** Ensure language code is in the format aa_AA. */ 476 $.i18n.normaliseLanguageCode = function (settings) { 477 478 var lang = settings.language; 479 if (!lang || lang.length < 2) { 480 if (settings.debug) debug('No language supplied. Pulling it from the browser ...'); 481 lang = (navigator.languages && navigator.languages.length > 0) ? navigator.languages[0] 482 : (navigator.language || navigator.userLanguage /* IE */ || 'en'); 483 if (settings.debug) debug('Language from browser: ' + lang); 484 } 485 486 lang = lang.toLowerCase(); 487 lang = lang.replace(/-/,"_"); // some browsers report language as en-US instead of en_US 488 if (lang.length > 3) { 489 lang = lang.substring(0, 3) + lang.substring(3).toUpperCase(); 490 } 491 return lang; 492 }; 493 494 /** Unescape unicode chars ('\u00e3') */ 495 function unescapeUnicode(str) { 496 497 // unescape unicode codes 498 var codes = []; 499 var code = parseInt(str.substr(2), 16); 500 if (code >= 0 && code < Math.pow(2, 16)) { 501 codes.push(code); 502 } 503 // convert codes to text 504 return codes.reduce(function (acc, val) { return acc + String.fromCharCode(val); }, ''); 505 } 506}) (jQuery);
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.