You are currently offline. You came here through the manifest.appcache file.
18 |You are currently offline. You came here through the manifest.appcache file.
18 |Press the + button in the top right corner to install this app.
48 | 49 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | Cross-domain XHR failed
"; 478 | crossDomainXHRDisplay.style.display = "block"; 479 | }; 480 | xhr.send(); 481 | }; 482 | } 483 | 484 | // deviceStorage, pictures 485 | var deviceStoragePictures = document.querySelector("#device-storage-pictures"), 486 | deviceStoragePicturesDisplay = document.querySelector("#device-storage-pictures-display"); 487 | if (deviceStoragePictures && deviceStoragePicturesDisplay) { 488 | deviceStoragePictures.onclick = function () { 489 | var deviceStorage = navigator.getDeviceStorage("pictures"), 490 | cursor = deviceStorage.enumerate(); 491 | deviceStoragePicturesDisplay.innerHTML = "deviceStorage failed
" + 510 | (this.error.message || this.error.name || this.error.toString()) + "
"; 511 | deviceStoragePicturesDisplay.style.display = "block"; 512 | }; 513 | }; 514 | } 515 | 516 | // List contacts 517 | var getAllContacts = document.querySelector("#get-all-contacts"), 518 | getAllContactsDisplay = document.querySelector("#get-all-contacts-display"); 519 | if (getAllContacts && getAllContactsDisplay) { 520 | getAllContacts.onclick = function () { 521 | var getContacts = window.navigator.mozContacts.getAll({}); 522 | getAllContactsDisplay.style.display = "block"; 523 | 524 | getContacts.onsuccess = function () { 525 | var result = getContacts.result; 526 | if (result) { 527 | getAllContactsDisplay.innerHTML += result.givenName + " " + result.familyName + "Id: " + alarm.id + 578 | ", date: " + alarm.date + 579 | ", respectTimezone: " + alarm.respectTimezone + 580 | ", data: " + JSON.stringify(alarm.data) + "
"; 581 | }); 582 | }; 583 | 584 | getAllAlarms.onerror = function () { 585 | alarmDisplay.innerHTML = "Failed to get all alarms
" + this.error.name; 586 | }; 587 | }; 588 | } 589 | 590 | var removeAllAlarms = document.querySelector("#remove-all-alarms"), 591 | removeAlarmsDisplay = document.querySelector("#remove-alarms-display"); 592 | if(removeAllAlarms) { 593 | removeAllAlarms.onclick = function () { 594 | var getAddedAlarms = navigator.mozAlarms.getAll(); 595 | getAddedAlarms.onsuccess = function () { 596 | this.result.forEach(function (alarm) { 597 | navigator.mozAlarms.remove(alarm.id); 598 | }); 599 | removeAlarmsDisplay.innerHTML = "All alarms removed"; 600 | if (alarmDisplay) { 601 | alarmDisplay.innerHTML = ""; 602 | } 603 | }; 604 | 605 | getAddedAlarms.onerror = function () { 606 | removeAlarmsDisplay.innerHTML = "Failed to remove all alarms
" + this.error.name; 607 | }; 608 | }; 609 | } 610 | })(); 611 | -------------------------------------------------------------------------------- /js/l10n.js: -------------------------------------------------------------------------------- 1 | (function(window, undefined) { 2 | 'use strict'; 3 | 4 | /* jshint validthis:true */ 5 | function L10nError(message, id, loc) { 6 | this.name = 'L10nError'; 7 | this.message = message; 8 | this.id = id; 9 | this.loc = loc; 10 | } 11 | L10nError.prototype = Object.create(Error.prototype); 12 | L10nError.prototype.constructor = L10nError; 13 | 14 | 15 | /* jshint browser:true */ 16 | 17 | var io = { 18 | load: function load(url, callback, sync) { 19 | var xhr = new XMLHttpRequest(); 20 | 21 | if (xhr.overrideMimeType) { 22 | xhr.overrideMimeType('text/plain'); 23 | } 24 | 25 | xhr.open('GET', url, !sync); 26 | 27 | xhr.addEventListener('load', function io_load(e) { 28 | if (e.target.status === 200 || e.target.status === 0) { 29 | callback(null, e.target.responseText); 30 | } else { 31 | callback(new L10nError('Not found: ' + url)); 32 | } 33 | }); 34 | xhr.addEventListener('error', callback); 35 | xhr.addEventListener('timeout', callback); 36 | 37 | // the app: protocol throws on 404, see https://bugzil.la/827243 38 | try { 39 | xhr.send(null); 40 | } catch (e) { 41 | callback(new L10nError('Not found: ' + url)); 42 | } 43 | }, 44 | 45 | loadJSON: function loadJSON(url, callback) { 46 | var xhr = new XMLHttpRequest(); 47 | 48 | if (xhr.overrideMimeType) { 49 | xhr.overrideMimeType('application/json'); 50 | } 51 | 52 | xhr.open('GET', url); 53 | 54 | xhr.responseType = 'json'; 55 | xhr.addEventListener('load', function io_loadjson(e) { 56 | if (e.target.status === 200 || e.target.status === 0) { 57 | callback(null, e.target.response); 58 | } else { 59 | callback(new L10nError('Not found: ' + url)); 60 | } 61 | }); 62 | xhr.addEventListener('error', callback); 63 | xhr.addEventListener('timeout', callback); 64 | 65 | // the app: protocol throws on 404, see https://bugzil.la/827243 66 | try { 67 | xhr.send(null); 68 | } catch (e) { 69 | callback(new L10nError('Not found: ' + url)); 70 | } 71 | } 72 | }; 73 | 74 | function EventEmitter() {} 75 | 76 | EventEmitter.prototype.emit = function ee_emit() { 77 | if (!this._listeners) { 78 | return; 79 | } 80 | 81 | var args = Array.prototype.slice.call(arguments); 82 | var type = args.shift(); 83 | if (!this._listeners[type]) { 84 | return; 85 | } 86 | 87 | var typeListeners = this._listeners[type].slice(); 88 | for (var i = 0; i < typeListeners.length; i++) { 89 | typeListeners[i].apply(this, args); 90 | } 91 | }; 92 | 93 | EventEmitter.prototype.addEventListener = function ee_add(type, listener) { 94 | if (!this._listeners) { 95 | this._listeners = {}; 96 | } 97 | if (!(type in this._listeners)) { 98 | this._listeners[type] = []; 99 | } 100 | this._listeners[type].push(listener); 101 | }; 102 | 103 | EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) { 104 | if (!this._listeners) { 105 | return; 106 | } 107 | 108 | var typeListeners = this._listeners[type]; 109 | var pos = typeListeners.indexOf(listener); 110 | if (pos === -1) { 111 | return; 112 | } 113 | 114 | typeListeners.splice(pos, 1); 115 | }; 116 | 117 | 118 | function getPluralRule(lang) { 119 | var locales2rules = { 120 | 'af': 3, 121 | 'ak': 4, 122 | 'am': 4, 123 | 'ar': 1, 124 | 'asa': 3, 125 | 'az': 0, 126 | 'be': 11, 127 | 'bem': 3, 128 | 'bez': 3, 129 | 'bg': 3, 130 | 'bh': 4, 131 | 'bm': 0, 132 | 'bn': 3, 133 | 'bo': 0, 134 | 'br': 20, 135 | 'brx': 3, 136 | 'bs': 11, 137 | 'ca': 3, 138 | 'cgg': 3, 139 | 'chr': 3, 140 | 'cs': 12, 141 | 'cy': 17, 142 | 'da': 3, 143 | 'de': 3, 144 | 'dv': 3, 145 | 'dz': 0, 146 | 'ee': 3, 147 | 'el': 3, 148 | 'en': 3, 149 | 'eo': 3, 150 | 'es': 3, 151 | 'et': 3, 152 | 'eu': 3, 153 | 'fa': 0, 154 | 'ff': 5, 155 | 'fi': 3, 156 | 'fil': 4, 157 | 'fo': 3, 158 | 'fr': 5, 159 | 'fur': 3, 160 | 'fy': 3, 161 | 'ga': 8, 162 | 'gd': 24, 163 | 'gl': 3, 164 | 'gsw': 3, 165 | 'gu': 3, 166 | 'guw': 4, 167 | 'gv': 23, 168 | 'ha': 3, 169 | 'haw': 3, 170 | 'he': 2, 171 | 'hi': 4, 172 | 'hr': 11, 173 | 'hu': 0, 174 | 'id': 0, 175 | 'ig': 0, 176 | 'ii': 0, 177 | 'is': 3, 178 | 'it': 3, 179 | 'iu': 7, 180 | 'ja': 0, 181 | 'jmc': 3, 182 | 'jv': 0, 183 | 'ka': 0, 184 | 'kab': 5, 185 | 'kaj': 3, 186 | 'kcg': 3, 187 | 'kde': 0, 188 | 'kea': 0, 189 | 'kk': 3, 190 | 'kl': 3, 191 | 'km': 0, 192 | 'kn': 0, 193 | 'ko': 0, 194 | 'ksb': 3, 195 | 'ksh': 21, 196 | 'ku': 3, 197 | 'kw': 7, 198 | 'lag': 18, 199 | 'lb': 3, 200 | 'lg': 3, 201 | 'ln': 4, 202 | 'lo': 0, 203 | 'lt': 10, 204 | 'lv': 6, 205 | 'mas': 3, 206 | 'mg': 4, 207 | 'mk': 16, 208 | 'ml': 3, 209 | 'mn': 3, 210 | 'mo': 9, 211 | 'mr': 3, 212 | 'ms': 0, 213 | 'mt': 15, 214 | 'my': 0, 215 | 'nah': 3, 216 | 'naq': 7, 217 | 'nb': 3, 218 | 'nd': 3, 219 | 'ne': 3, 220 | 'nl': 3, 221 | 'nn': 3, 222 | 'no': 3, 223 | 'nr': 3, 224 | 'nso': 4, 225 | 'ny': 3, 226 | 'nyn': 3, 227 | 'om': 3, 228 | 'or': 3, 229 | 'pa': 3, 230 | 'pap': 3, 231 | 'pl': 13, 232 | 'ps': 3, 233 | 'pt': 3, 234 | 'rm': 3, 235 | 'ro': 9, 236 | 'rof': 3, 237 | 'ru': 11, 238 | 'rwk': 3, 239 | 'sah': 0, 240 | 'saq': 3, 241 | 'se': 7, 242 | 'seh': 3, 243 | 'ses': 0, 244 | 'sg': 0, 245 | 'sh': 11, 246 | 'shi': 19, 247 | 'sk': 12, 248 | 'sl': 14, 249 | 'sma': 7, 250 | 'smi': 7, 251 | 'smj': 7, 252 | 'smn': 7, 253 | 'sms': 7, 254 | 'sn': 3, 255 | 'so': 3, 256 | 'sq': 3, 257 | 'sr': 11, 258 | 'ss': 3, 259 | 'ssy': 3, 260 | 'st': 3, 261 | 'sv': 3, 262 | 'sw': 3, 263 | 'syr': 3, 264 | 'ta': 3, 265 | 'te': 3, 266 | 'teo': 3, 267 | 'th': 0, 268 | 'ti': 4, 269 | 'tig': 3, 270 | 'tk': 3, 271 | 'tl': 4, 272 | 'tn': 3, 273 | 'to': 0, 274 | 'tr': 0, 275 | 'ts': 3, 276 | 'tzm': 22, 277 | 'uk': 11, 278 | 'ur': 3, 279 | 've': 3, 280 | 'vi': 0, 281 | 'vun': 3, 282 | 'wa': 4, 283 | 'wae': 3, 284 | 'wo': 0, 285 | 'xh': 3, 286 | 'xog': 3, 287 | 'yo': 0, 288 | 'zh': 0, 289 | 'zu': 3 290 | }; 291 | 292 | // utility functions for plural rules methods 293 | function isIn(n, list) { 294 | return list.indexOf(n) !== -1; 295 | } 296 | function isBetween(n, start, end) { 297 | return typeof n === typeof start && start <= n && n <= end; 298 | } 299 | 300 | // list of all plural rules methods: 301 | // map an integer to the plural form name to use 302 | var pluralRules = { 303 | '0': function() { 304 | return 'other'; 305 | }, 306 | '1': function(n) { 307 | if ((isBetween((n % 100), 3, 10))) { 308 | return 'few'; 309 | } 310 | if (n === 0) { 311 | return 'zero'; 312 | } 313 | if ((isBetween((n % 100), 11, 99))) { 314 | return 'many'; 315 | } 316 | if (n === 2) { 317 | return 'two'; 318 | } 319 | if (n === 1) { 320 | return 'one'; 321 | } 322 | return 'other'; 323 | }, 324 | '2': function(n) { 325 | if (n !== 0 && (n % 10) === 0) { 326 | return 'many'; 327 | } 328 | if (n === 2) { 329 | return 'two'; 330 | } 331 | if (n === 1) { 332 | return 'one'; 333 | } 334 | return 'other'; 335 | }, 336 | '3': function(n) { 337 | if (n === 1) { 338 | return 'one'; 339 | } 340 | return 'other'; 341 | }, 342 | '4': function(n) { 343 | if ((isBetween(n, 0, 1))) { 344 | return 'one'; 345 | } 346 | return 'other'; 347 | }, 348 | '5': function(n) { 349 | if ((isBetween(n, 0, 2)) && n !== 2) { 350 | return 'one'; 351 | } 352 | return 'other'; 353 | }, 354 | '6': function(n) { 355 | if (n === 0) { 356 | return 'zero'; 357 | } 358 | if ((n % 10) === 1 && (n % 100) !== 11) { 359 | return 'one'; 360 | } 361 | return 'other'; 362 | }, 363 | '7': function(n) { 364 | if (n === 2) { 365 | return 'two'; 366 | } 367 | if (n === 1) { 368 | return 'one'; 369 | } 370 | return 'other'; 371 | }, 372 | '8': function(n) { 373 | if ((isBetween(n, 3, 6))) { 374 | return 'few'; 375 | } 376 | if ((isBetween(n, 7, 10))) { 377 | return 'many'; 378 | } 379 | if (n === 2) { 380 | return 'two'; 381 | } 382 | if (n === 1) { 383 | return 'one'; 384 | } 385 | return 'other'; 386 | }, 387 | '9': function(n) { 388 | if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) { 389 | return 'few'; 390 | } 391 | if (n === 1) { 392 | return 'one'; 393 | } 394 | return 'other'; 395 | }, 396 | '10': function(n) { 397 | if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) { 398 | return 'few'; 399 | } 400 | if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) { 401 | return 'one'; 402 | } 403 | return 'other'; 404 | }, 405 | '11': function(n) { 406 | if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) { 407 | return 'few'; 408 | } 409 | if ((n % 10) === 0 || 410 | (isBetween((n % 10), 5, 9)) || 411 | (isBetween((n % 100), 11, 14))) { 412 | return 'many'; 413 | } 414 | if ((n % 10) === 1 && (n % 100) !== 11) { 415 | return 'one'; 416 | } 417 | return 'other'; 418 | }, 419 | '12': function(n) { 420 | if ((isBetween(n, 2, 4))) { 421 | return 'few'; 422 | } 423 | if (n === 1) { 424 | return 'one'; 425 | } 426 | return 'other'; 427 | }, 428 | '13': function(n) { 429 | if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) { 430 | return 'few'; 431 | } 432 | if (n !== 1 && (isBetween((n % 10), 0, 1)) || 433 | (isBetween((n % 10), 5, 9)) || 434 | (isBetween((n % 100), 12, 14))) { 435 | return 'many'; 436 | } 437 | if (n === 1) { 438 | return 'one'; 439 | } 440 | return 'other'; 441 | }, 442 | '14': function(n) { 443 | if ((isBetween((n % 100), 3, 4))) { 444 | return 'few'; 445 | } 446 | if ((n % 100) === 2) { 447 | return 'two'; 448 | } 449 | if ((n % 100) === 1) { 450 | return 'one'; 451 | } 452 | return 'other'; 453 | }, 454 | '15': function(n) { 455 | if (n === 0 || (isBetween((n % 100), 2, 10))) { 456 | return 'few'; 457 | } 458 | if ((isBetween((n % 100), 11, 19))) { 459 | return 'many'; 460 | } 461 | if (n === 1) { 462 | return 'one'; 463 | } 464 | return 'other'; 465 | }, 466 | '16': function(n) { 467 | if ((n % 10) === 1 && n !== 11) { 468 | return 'one'; 469 | } 470 | return 'other'; 471 | }, 472 | '17': function(n) { 473 | if (n === 3) { 474 | return 'few'; 475 | } 476 | if (n === 0) { 477 | return 'zero'; 478 | } 479 | if (n === 6) { 480 | return 'many'; 481 | } 482 | if (n === 2) { 483 | return 'two'; 484 | } 485 | if (n === 1) { 486 | return 'one'; 487 | } 488 | return 'other'; 489 | }, 490 | '18': function(n) { 491 | if (n === 0) { 492 | return 'zero'; 493 | } 494 | if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) { 495 | return 'one'; 496 | } 497 | return 'other'; 498 | }, 499 | '19': function(n) { 500 | if ((isBetween(n, 2, 10))) { 501 | return 'few'; 502 | } 503 | if ((isBetween(n, 0, 1))) { 504 | return 'one'; 505 | } 506 | return 'other'; 507 | }, 508 | '20': function(n) { 509 | if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !( 510 | isBetween((n % 100), 10, 19) || 511 | isBetween((n % 100), 70, 79) || 512 | isBetween((n % 100), 90, 99) 513 | )) { 514 | return 'few'; 515 | } 516 | if ((n % 1000000) === 0 && n !== 0) { 517 | return 'many'; 518 | } 519 | if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) { 520 | return 'two'; 521 | } 522 | if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) { 523 | return 'one'; 524 | } 525 | return 'other'; 526 | }, 527 | '21': function(n) { 528 | if (n === 0) { 529 | return 'zero'; 530 | } 531 | if (n === 1) { 532 | return 'one'; 533 | } 534 | return 'other'; 535 | }, 536 | '22': function(n) { 537 | if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) { 538 | return 'one'; 539 | } 540 | return 'other'; 541 | }, 542 | '23': function(n) { 543 | if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) { 544 | return 'one'; 545 | } 546 | return 'other'; 547 | }, 548 | '24': function(n) { 549 | if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) { 550 | return 'few'; 551 | } 552 | if (isIn(n, [2, 12])) { 553 | return 'two'; 554 | } 555 | if (isIn(n, [1, 11])) { 556 | return 'one'; 557 | } 558 | return 'other'; 559 | } 560 | }; 561 | 562 | // return a function that gives the plural form name for a given integer 563 | var index = locales2rules[lang.replace(/-.*$/, '')]; 564 | if (!(index in pluralRules)) { 565 | return function() { return 'other'; }; 566 | } 567 | return pluralRules[index]; 568 | } 569 | 570 | 571 | 572 | 573 | function PropertiesParser() { 574 | var parsePatterns = { 575 | comment: /^\s*#|^\s*$/, 576 | entity: /^([^=\s]+)\s*=\s*(.+)$/, 577 | multiline: /[^\\]\\$/, 578 | macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i, 579 | unicode: /\\u([0-9a-fA-F]{1,4})/g, 580 | entries: /[\r\n]+/, 581 | controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g 582 | }; 583 | 584 | this.parse = function (ctx, source) { 585 | var ast = Object.create(null); 586 | 587 | var entries = source.split(parsePatterns.entries); 588 | for (var i = 0; i < entries.length; i++) { 589 | var line = entries[i]; 590 | 591 | if (parsePatterns.comment.test(line)) { 592 | continue; 593 | } 594 | 595 | while (parsePatterns.multiline.test(line) && i < entries.length) { 596 | line = line.slice(0, -1) + entries[++i].trim(); 597 | } 598 | 599 | var entityMatch = line.match(parsePatterns.entity); 600 | if (entityMatch) { 601 | try { 602 | parseEntity(entityMatch[1], entityMatch[2], ast); 603 | } catch (e) { 604 | if (ctx) { 605 | ctx._emitter.emit('error', e); 606 | } else { 607 | throw e; 608 | } 609 | } 610 | } 611 | } 612 | return ast; 613 | }; 614 | 615 | function setEntityValue(id, attr, key, value, ast) { 616 | var obj = ast; 617 | var prop = id; 618 | 619 | if (attr) { 620 | if (!(id in obj)) { 621 | obj[id] = {}; 622 | } 623 | if (typeof(obj[id]) === 'string') { 624 | obj[id] = {'_': obj[id]}; 625 | } 626 | obj = obj[id]; 627 | prop = attr; 628 | } 629 | 630 | if (!key) { 631 | obj[prop] = value; 632 | return; 633 | } 634 | 635 | if (!(prop in obj)) { 636 | obj[prop] = {'_': {}}; 637 | } else if (typeof(obj[prop]) === 'string') { 638 | obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}}; 639 | } 640 | obj[prop]._[key] = value; 641 | } 642 | 643 | function parseEntity(id, value, ast) { 644 | var name, key; 645 | 646 | var pos = id.indexOf('['); 647 | if (pos !== -1) { 648 | name = id.substr(0, pos); 649 | key = id.substring(pos + 1, id.length - 1); 650 | } else { 651 | name = id; 652 | key = null; 653 | } 654 | 655 | var nameElements = name.split('.'); 656 | 657 | if (nameElements.length > 2) { 658 | throw new Error('Error in ID: "' + name + '".' + 659 | ' Nested attributes are not supported.'); 660 | } 661 | 662 | var attr; 663 | if (nameElements.length > 1) { 664 | name = nameElements[0]; 665 | attr = nameElements[1]; 666 | } else { 667 | attr = null; 668 | } 669 | 670 | setEntityValue(name, attr, key, unescapeString(value), ast); 671 | } 672 | 673 | function unescapeControlCharacters(str) { 674 | return str.replace(parsePatterns.controlChars, '$1'); 675 | } 676 | 677 | function unescapeUnicode(str) { 678 | return str.replace(parsePatterns.unicode, function(match, token) { 679 | return unescape('%u' + '0000'.slice(token.length) + token); 680 | }); 681 | } 682 | 683 | function unescapeString(str) { 684 | if (str.lastIndexOf('\\') !== -1) { 685 | str = unescapeControlCharacters(str); 686 | } 687 | return unescapeUnicode(str); 688 | } 689 | 690 | function parseMacro(str) { 691 | var match = str.match(parsePatterns.macro); 692 | if (!match) { 693 | throw new L10nError('Malformed macro'); 694 | } 695 | return [match[1], match[2]]; 696 | } 697 | } 698 | 699 | 700 | 701 | var MAX_PLACEABLE_LENGTH = 2500; 702 | var MAX_PLACEABLES = 100; 703 | var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g; 704 | 705 | function Entity(id, node, env) { 706 | this.id = id; 707 | this.env = env; 708 | // the dirty guard prevents cyclic or recursive references from other 709 | // Entities; see Entity.prototype.resolve 710 | this.dirty = false; 711 | if (typeof node === 'string') { 712 | this.value = node; 713 | } else { 714 | // it's either a hash or it has attrs, or both 715 | for (var key in node) { 716 | if (key[0] !== '_') { 717 | if (!this.attributes) { 718 | this.attributes = Object.create(null); 719 | } 720 | this.attributes[key] = new Entity(this.id + '.' + key, node[key], 721 | env); 722 | } 723 | } 724 | this.value = node._ || null; 725 | this.index = node._index; 726 | } 727 | } 728 | 729 | Entity.prototype.resolve = function E_resolve(ctxdata) { 730 | if (this.dirty) { 731 | return undefined; 732 | } 733 | 734 | this.dirty = true; 735 | var val; 736 | // if resolve fails, we want the exception to bubble up and stop the whole 737 | // resolving process; however, we still need to clean up the dirty flag 738 | try { 739 | val = resolve(ctxdata, this.env, this.value, this.index); 740 | } finally { 741 | this.dirty = false; 742 | } 743 | return val; 744 | }; 745 | 746 | Entity.prototype.toString = function E_toString(ctxdata) { 747 | try { 748 | return this.resolve(ctxdata); 749 | } catch (e) { 750 | return undefined; 751 | } 752 | }; 753 | 754 | Entity.prototype.valueOf = function E_valueOf(ctxdata) { 755 | if (!this.attributes) { 756 | return this.toString(ctxdata); 757 | } 758 | 759 | var entity = { 760 | value: this.toString(ctxdata), 761 | attributes: Object.create(null) 762 | }; 763 | 764 | for (var key in this.attributes) { 765 | /* jshint -W089 */ 766 | entity.attributes[key] = this.attributes[key].toString(ctxdata); 767 | } 768 | 769 | return entity; 770 | }; 771 | 772 | function subPlaceable(ctxdata, env, match, id) { 773 | if (ctxdata && ctxdata.hasOwnProperty(id) && 774 | (typeof ctxdata[id] === 'string' || 775 | (typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) { 776 | return ctxdata[id]; 777 | } 778 | 779 | // XXX: special case for Node.js where still: 780 | // '__proto__' in Object.create(null) => true 781 | if (id in env && id !== '__proto__') { 782 | if (!(env[id] instanceof Entity)) { 783 | env[id] = new Entity(id, env[id], env); 784 | } 785 | var value = env[id].resolve(ctxdata); 786 | if (typeof value === 'string') { 787 | // prevent Billion Laughs attacks 788 | if (value.length >= MAX_PLACEABLE_LENGTH) { 789 | throw new L10nError('Too many characters in placeable (' + 790 | value.length + ', max allowed is ' + 791 | MAX_PLACEABLE_LENGTH + ')'); 792 | } 793 | return value; 794 | } 795 | } 796 | return match; 797 | } 798 | 799 | function interpolate(ctxdata, env, str) { 800 | var placeablesCount = 0; 801 | var value = str.replace(rePlaceables, function(match, id) { 802 | // prevent Quadratic Blowup attacks 803 | if (placeablesCount++ >= MAX_PLACEABLES) { 804 | throw new L10nError('Too many placeables (' + placeablesCount + 805 | ', max allowed is ' + MAX_PLACEABLES + ')'); 806 | } 807 | return subPlaceable(ctxdata, env, match, id); 808 | }); 809 | placeablesCount = 0; 810 | return value; 811 | } 812 | 813 | function resolve(ctxdata, env, expr, index) { 814 | if (typeof expr === 'string') { 815 | return interpolate(ctxdata, env, expr); 816 | } 817 | 818 | if (typeof expr === 'boolean' || 819 | typeof expr === 'number' || 820 | !expr) { 821 | return expr; 822 | } 823 | 824 | // otherwise, it's a dict 825 | 826 | if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) { 827 | var argValue = ctxdata[index[1]]; 828 | 829 | // special cases for zero, one, two if they are defined on the hash 830 | if (argValue === 0 && 'zero' in expr) { 831 | return resolve(ctxdata, env, expr.zero); 832 | } 833 | if (argValue === 1 && 'one' in expr) { 834 | return resolve(ctxdata, env, expr.one); 835 | } 836 | if (argValue === 2 && 'two' in expr) { 837 | return resolve(ctxdata, env, expr.two); 838 | } 839 | 840 | var selector = env.__plural(argValue); 841 | if (expr.hasOwnProperty(selector)) { 842 | return resolve(ctxdata, env, expr[selector]); 843 | } 844 | } 845 | 846 | // if there was no index or no selector was found, try 'other' 847 | if ('other' in expr) { 848 | return resolve(ctxdata, env, expr.other); 849 | } 850 | 851 | return undefined; 852 | } 853 | 854 | function compile(env, ast) { 855 | /* jshint -W089 */ 856 | env = env || Object.create(null); 857 | for (var id in ast) { 858 | env[id] = new Entity(id, ast[id], env); 859 | } 860 | return env; 861 | } 862 | 863 | 864 | 865 | var propertiesParser = null; 866 | 867 | function Locale(id, ctx) { 868 | this.id = id; 869 | this.ctx = ctx; 870 | this.isReady = false; 871 | this.entries = Object.create(null); 872 | this.entries.__plural = getPluralRule(id); 873 | } 874 | 875 | Locale.prototype.getEntry = function L_getEntry(id) { 876 | /* jshint -W093 */ 877 | 878 | var entries = this.entries; 879 | 880 | if (!(id in entries)) { 881 | return undefined; 882 | } 883 | 884 | if (entries[id] instanceof Entity) { 885 | return entries[id]; 886 | } 887 | 888 | return entries[id] = new Entity(id, entries[id], entries); 889 | }; 890 | 891 | Locale.prototype.build = function L_build(callback) { 892 | var sync = !callback; 893 | var ctx = this.ctx; 894 | var self = this; 895 | 896 | var l10nLoads = ctx.resLinks.length; 897 | 898 | function onL10nLoaded(err) { 899 | if (err) { 900 | ctx._emitter.emit('error', err); 901 | } 902 | if (--l10nLoads <= 0) { 903 | self.isReady = true; 904 | if (callback) { 905 | callback(); 906 | } 907 | } 908 | } 909 | 910 | if (l10nLoads === 0) { 911 | onL10nLoaded(); 912 | return; 913 | } 914 | 915 | function onJSONLoaded(err, json) { 916 | if (!err && json) { 917 | self.addAST(json); 918 | } 919 | onL10nLoaded(err); 920 | } 921 | 922 | function onPropLoaded(err, source) { 923 | if (!err && source) { 924 | if (!propertiesParser) { 925 | propertiesParser = new PropertiesParser(); 926 | } 927 | var ast = propertiesParser.parse(ctx, source); 928 | self.addAST(ast); 929 | } 930 | onL10nLoaded(err); 931 | } 932 | 933 | 934 | for (var i = 0; i < ctx.resLinks.length; i++) { 935 | var path = ctx.resLinks[i].replace('{{locale}}', this.id); 936 | var type = path.substr(path.lastIndexOf('.') + 1); 937 | 938 | switch (type) { 939 | case 'json': 940 | io.loadJSON(path, onJSONLoaded, sync); 941 | break; 942 | case 'properties': 943 | io.load(path, onPropLoaded, sync); 944 | break; 945 | } 946 | } 947 | }; 948 | 949 | Locale.prototype.addAST = function(ast) { 950 | /* jshint -W089 */ 951 | for (var id in ast) { 952 | this.entries[id] = ast[id]; 953 | } 954 | }; 955 | 956 | Locale.prototype.getEntity = function(id, ctxdata) { 957 | var entry = this.getEntry(id); 958 | 959 | if (!entry) { 960 | return null; 961 | } 962 | return entry.valueOf(ctxdata); 963 | }; 964 | 965 | 966 | 967 | function Context(id) { 968 | 969 | this.id = id; 970 | this.isReady = false; 971 | this.isLoading = false; 972 | 973 | this.supportedLocales = []; 974 | this.resLinks = []; 975 | this.locales = {}; 976 | 977 | this._emitter = new EventEmitter(); 978 | 979 | 980 | // Getting translations 981 | 982 | function getWithFallback(id) { 983 | /* jshint -W084 */ 984 | 985 | if (!this.isReady) { 986 | throw new L10nError('Context not ready'); 987 | } 988 | 989 | var cur = 0; 990 | var loc; 991 | var locale; 992 | while (loc = this.supportedLocales[cur]) { 993 | locale = this.getLocale(loc); 994 | if (!locale.isReady) { 995 | // build without callback, synchronously 996 | locale.build(null); 997 | } 998 | var entry = locale.getEntry(id); 999 | if (entry === undefined) { 1000 | cur++; 1001 | warning.call(this, new L10nError(id + ' not found in ' + loc, id, 1002 | loc)); 1003 | continue; 1004 | } 1005 | return entry; 1006 | } 1007 | 1008 | error.call(this, new L10nError(id + ' not found', id)); 1009 | return null; 1010 | } 1011 | 1012 | this.get = function get(id, ctxdata) { 1013 | var entry = getWithFallback.call(this, id); 1014 | if (entry === null) { 1015 | return ''; 1016 | } 1017 | 1018 | return entry.toString(ctxdata) || ''; 1019 | }; 1020 | 1021 | this.getEntity = function getEntity(id, ctxdata) { 1022 | var entry = getWithFallback.call(this, id); 1023 | if (entry === null) { 1024 | return null; 1025 | } 1026 | 1027 | return entry.valueOf(ctxdata); 1028 | }; 1029 | 1030 | 1031 | // Helpers 1032 | 1033 | this.getLocale = function getLocale(code) { 1034 | /* jshint -W093 */ 1035 | 1036 | var locales = this.locales; 1037 | if (locales[code]) { 1038 | return locales[code]; 1039 | } 1040 | 1041 | return locales[code] = new Locale(code, this); 1042 | }; 1043 | 1044 | 1045 | // Getting ready 1046 | 1047 | function negotiate(available, requested, defaultLocale) { 1048 | if (available.indexOf(requested[0]) === -1 || 1049 | requested[0] === defaultLocale) { 1050 | return [defaultLocale]; 1051 | } else { 1052 | return [requested[0], defaultLocale]; 1053 | } 1054 | } 1055 | 1056 | function freeze(supported) { 1057 | var locale = this.getLocale(supported[0]); 1058 | if (locale.isReady) { 1059 | setReady.call(this, supported); 1060 | } else { 1061 | locale.build(setReady.bind(this, supported)); 1062 | } 1063 | } 1064 | 1065 | function setReady(supported) { 1066 | this.supportedLocales = supported; 1067 | this.isReady = true; 1068 | this._emitter.emit('ready'); 1069 | } 1070 | 1071 | this.requestLocales = function requestLocales() { 1072 | if (this.isLoading && !this.isReady) { 1073 | throw new L10nError('Context not ready'); 1074 | } 1075 | 1076 | this.isLoading = true; 1077 | var requested = Array.prototype.slice.call(arguments); 1078 | 1079 | var supported = negotiate(requested.concat('en-US'), requested, 'en-US'); 1080 | freeze.call(this, supported); 1081 | }; 1082 | 1083 | 1084 | // Events 1085 | 1086 | this.addEventListener = function addEventListener(type, listener) { 1087 | this._emitter.addEventListener(type, listener); 1088 | }; 1089 | 1090 | this.removeEventListener = function removeEventListener(type, listener) { 1091 | this._emitter.removeEventListener(type, listener); 1092 | }; 1093 | 1094 | this.ready = function ready(callback) { 1095 | if (this.isReady) { 1096 | setTimeout(callback); 1097 | } 1098 | this.addEventListener('ready', callback); 1099 | }; 1100 | 1101 | this.once = function once(callback) { 1102 | /* jshint -W068 */ 1103 | if (this.isReady) { 1104 | setTimeout(callback); 1105 | return; 1106 | } 1107 | 1108 | var callAndRemove = (function() { 1109 | this.removeEventListener('ready', callAndRemove); 1110 | callback(); 1111 | }).bind(this); 1112 | this.addEventListener('ready', callAndRemove); 1113 | }; 1114 | 1115 | 1116 | // Errors 1117 | 1118 | function warning(e) { 1119 | this._emitter.emit('warning', e); 1120 | return e; 1121 | } 1122 | 1123 | function error(e) { 1124 | this._emitter.emit('error', e); 1125 | return e; 1126 | } 1127 | } 1128 | 1129 | 1130 | 1131 | var DEBUG = false; 1132 | var isPretranslated = false; 1133 | var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur']; 1134 | var nodeObserver = false; 1135 | 1136 | var moConfig = { 1137 | attributes: true, 1138 | characterData: false, 1139 | childList: true, 1140 | subtree: true, 1141 | attributeFilter: ['data-l10n-id', 'data-l10n-args'] 1142 | }; 1143 | 1144 | // Public API 1145 | 1146 | navigator.mozL10n = { 1147 | ctx: new Context(), 1148 | get: function get(id, ctxdata) { 1149 | return navigator.mozL10n.ctx.get(id, ctxdata); 1150 | }, 1151 | localize: function localize(element, id, args) { 1152 | return localizeElement.call(navigator.mozL10n, element, id, args); 1153 | }, 1154 | translate: function () { 1155 | // XXX: Remove after removing obsolete calls. Bugs 992473 and 1020136 1156 | }, 1157 | translateFragment: function (fragment) { 1158 | return translateFragment.call(navigator.mozL10n, fragment); 1159 | }, 1160 | setAttributes: setL10nAttributes, 1161 | getAttributes: getL10nAttributes, 1162 | ready: function ready(callback) { 1163 | return navigator.mozL10n.ctx.ready(callback); 1164 | }, 1165 | once: function once(callback) { 1166 | return navigator.mozL10n.ctx.once(callback); 1167 | }, 1168 | get readyState() { 1169 | return navigator.mozL10n.ctx.isReady ? 'complete' : 'loading'; 1170 | }, 1171 | language: { 1172 | set code(lang) { 1173 | navigator.mozL10n.ctx.requestLocales(lang); 1174 | }, 1175 | get code() { 1176 | return navigator.mozL10n.ctx.supportedLocales[0]; 1177 | }, 1178 | get direction() { 1179 | return getDirection(navigator.mozL10n.ctx.supportedLocales[0]); 1180 | } 1181 | }, 1182 | _getInternalAPI: function() { 1183 | return { 1184 | Error: L10nError, 1185 | Context: Context, 1186 | Locale: Locale, 1187 | Entity: Entity, 1188 | getPluralRule: getPluralRule, 1189 | rePlaceables: rePlaceables, 1190 | getTranslatableChildren: getTranslatableChildren, 1191 | translateDocument: translateDocument, 1192 | loadINI: loadINI, 1193 | fireLocalizedEvent: fireLocalizedEvent, 1194 | PropertiesParser: PropertiesParser, 1195 | compile: compile 1196 | }; 1197 | } 1198 | }; 1199 | 1200 | navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n)); 1201 | 1202 | if (DEBUG) { 1203 | navigator.mozL10n.ctx.addEventListener('error', console.error); 1204 | navigator.mozL10n.ctx.addEventListener('warning', console.warn); 1205 | } 1206 | 1207 | function getDirection(lang) { 1208 | return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr'; 1209 | } 1210 | 1211 | var readyStates = { 1212 | 'loading': 0, 1213 | 'interactive': 1, 1214 | 'complete': 2 1215 | }; 1216 | 1217 | function waitFor(state, callback) { 1218 | state = readyStates[state]; 1219 | if (readyStates[document.readyState] >= state) { 1220 | callback(); 1221 | return; 1222 | } 1223 | 1224 | document.addEventListener('readystatechange', function l10n_onrsc() { 1225 | if (readyStates[document.readyState] >= state) { 1226 | document.removeEventListener('readystatechange', l10n_onrsc); 1227 | callback(); 1228 | } 1229 | }); 1230 | } 1231 | 1232 | if (window.document) { 1233 | isPretranslated = (document.documentElement.lang === navigator.language); 1234 | 1235 | // this is a special case for netError bug; see https://bugzil.la/444165 1236 | if (document.documentElement.dataset.noCompleteBug) { 1237 | pretranslate.call(navigator.mozL10n); 1238 | return; 1239 | } 1240 | 1241 | 1242 | if (isPretranslated) { 1243 | waitFor('interactive', function() { 1244 | window.setTimeout(initResources.bind(navigator.mozL10n)); 1245 | }); 1246 | } else { 1247 | if (document.readyState === 'complete') { 1248 | window.setTimeout(initResources.bind(navigator.mozL10n)); 1249 | } else { 1250 | waitFor('interactive', pretranslate.bind(navigator.mozL10n)); 1251 | } 1252 | } 1253 | 1254 | } 1255 | 1256 | function pretranslate() { 1257 | /* jshint -W068 */ 1258 | if (inlineLocalization.call(this)) { 1259 | waitFor('interactive', (function() { 1260 | window.setTimeout(initResources.bind(this)); 1261 | }).bind(this)); 1262 | } else { 1263 | initResources.call(this); 1264 | } 1265 | } 1266 | 1267 | function inlineLocalization() { 1268 | var script = document.documentElement 1269 | .querySelector('script[type="application/l10n"]' + 1270 | '[lang="' + navigator.language + '"]'); 1271 | if (!script) { 1272 | return false; 1273 | } 1274 | 1275 | var locale = this.ctx.getLocale(navigator.language); 1276 | // the inline localization is happenning very early, when the ctx is not 1277 | // yet ready and when the resources haven't been downloaded yet; add the 1278 | // inlined JSON directly to the current locale 1279 | locale.addAST(JSON.parse(script.innerHTML)); 1280 | // localize the visible DOM 1281 | var l10n = { 1282 | ctx: locale, 1283 | language: { 1284 | code: locale.id, 1285 | direction: getDirection(locale.id) 1286 | } 1287 | }; 1288 | translateDocument.call(l10n); 1289 | 1290 | // the visible DOM is now pretranslated 1291 | isPretranslated = true; 1292 | return true; 1293 | } 1294 | 1295 | function initResources() { 1296 | var resLinks = document.head 1297 | .querySelectorAll('link[type="application/l10n"]'); 1298 | var iniLinks = []; 1299 | 1300 | for (var i = 0; i < resLinks.length; i++) { 1301 | var link = resLinks[i]; 1302 | var url = link.getAttribute('href'); 1303 | var type = url.substr(url.lastIndexOf('.') + 1); 1304 | if (type === 'ini') { 1305 | iniLinks.push(url); 1306 | } 1307 | this.ctx.resLinks.push(url); 1308 | } 1309 | 1310 | var iniLoads = iniLinks.length; 1311 | if (iniLoads === 0) { 1312 | initLocale.call(this); 1313 | return; 1314 | } 1315 | 1316 | function onIniLoaded(err) { 1317 | if (err) { 1318 | this.ctx._emitter.emit('error', err); 1319 | } 1320 | if (--iniLoads === 0) { 1321 | initLocale.call(this); 1322 | } 1323 | } 1324 | 1325 | for (i = 0; i < iniLinks.length; i++) { 1326 | loadINI.call(this, iniLinks[i], onIniLoaded.bind(this)); 1327 | } 1328 | } 1329 | 1330 | function initLocale() { 1331 | this.ctx.requestLocales(navigator.language); 1332 | window.addEventListener('languagechange', function l10n_langchange() { 1333 | navigator.mozL10n.language.code = navigator.language; 1334 | }); 1335 | } 1336 | 1337 | function localizeMutations(mutations) { 1338 | var mutation; 1339 | 1340 | for (var i = 0; i < mutations.length; i++) { 1341 | mutation = mutations[i]; 1342 | if (mutation.type === 'childList') { 1343 | var addedNode; 1344 | 1345 | for (var j = 0; j < mutation.addedNodes.length; j++) { 1346 | addedNode = mutation.addedNodes[j]; 1347 | 1348 | if (addedNode.nodeType !== Node.ELEMENT_NODE) { 1349 | continue; 1350 | } 1351 | 1352 | if (addedNode.childElementCount) { 1353 | translateFragment.call(this, addedNode); 1354 | } else if (addedNode.hasAttribute('data-l10n-id')) { 1355 | translateElement.call(this, addedNode); 1356 | } 1357 | } 1358 | } 1359 | 1360 | if (mutation.type === 'attributes') { 1361 | translateElement.call(this, mutation.target); 1362 | } 1363 | } 1364 | } 1365 | 1366 | function onMutations(mutations, self) { 1367 | self.disconnect(); 1368 | localizeMutations.call(this, mutations); 1369 | self.observe(document, moConfig); 1370 | } 1371 | 1372 | function onReady() { 1373 | if (!isPretranslated) { 1374 | translateDocument.call(this); 1375 | } 1376 | isPretranslated = false; 1377 | 1378 | if (!nodeObserver) { 1379 | nodeObserver = new MutationObserver(onMutations.bind(this)); 1380 | nodeObserver.observe(document, moConfig); 1381 | } 1382 | 1383 | fireLocalizedEvent.call(this); 1384 | } 1385 | 1386 | function fireLocalizedEvent() { 1387 | var event = new CustomEvent('localized', { 1388 | 'bubbles': false, 1389 | 'cancelable': false, 1390 | 'detail': { 1391 | 'language': this.ctx.supportedLocales[0] 1392 | } 1393 | }); 1394 | window.dispatchEvent(event); 1395 | } 1396 | 1397 | /* jshint -W104 */ 1398 | 1399 | function loadINI(url, callback) { 1400 | var ctx = this.ctx; 1401 | io.load(url, function(err, source) { 1402 | var pos = ctx.resLinks.indexOf(url); 1403 | 1404 | if (err) { 1405 | // remove the ini link from resLinks 1406 | ctx.resLinks.splice(pos, 1); 1407 | return callback(err); 1408 | } 1409 | 1410 | if (!source) { 1411 | ctx.resLinks.splice(pos, 1); 1412 | return callback(new Error('Empty file: ' + url)); 1413 | } 1414 | 1415 | var patterns = parseINI(source, url).resources.map(function(x) { 1416 | return x.replace('en-US', '{{locale}}'); 1417 | }); 1418 | ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns)); 1419 | callback(); 1420 | }); 1421 | } 1422 | 1423 | function relativePath(baseUrl, url) { 1424 | if (url[0] === '/') { 1425 | return url; 1426 | } 1427 | 1428 | var dirs = baseUrl.split('/') 1429 | .slice(0, -1) 1430 | .concat(url.split('/')) 1431 | .filter(function(path) { 1432 | return path !== '.'; 1433 | }); 1434 | 1435 | return dirs.join('/'); 1436 | } 1437 | 1438 | var iniPatterns = { 1439 | 'section': /^\s*\[(.*)\]\s*$/, 1440 | 'import': /^\s*@import\s+url\((.*)\)\s*$/i, 1441 | 'entry': /[\r\n]+/ 1442 | }; 1443 | 1444 | function parseINI(source, iniPath) { 1445 | var entries = source.split(iniPatterns.entry); 1446 | var locales = ['en-US']; 1447 | var genericSection = true; 1448 | var uris = []; 1449 | var match; 1450 | 1451 | for (var i = 0; i < entries.length; i++) { 1452 | var line = entries[i]; 1453 | // we only care about en-US resources 1454 | if (genericSection && iniPatterns['import'].test(line)) { 1455 | match = iniPatterns['import'].exec(line); 1456 | var uri = relativePath(iniPath, match[1]); 1457 | uris.push(uri); 1458 | continue; 1459 | } 1460 | 1461 | // but we need the list of all locales in the ini, too 1462 | if (iniPatterns.section.test(line)) { 1463 | genericSection = false; 1464 | match = iniPatterns.section.exec(line); 1465 | locales.push(match[1]); 1466 | } 1467 | } 1468 | return { 1469 | locales: locales, 1470 | resources: uris 1471 | }; 1472 | } 1473 | 1474 | /* jshint -W104 */ 1475 | 1476 | function translateDocument() { 1477 | document.documentElement.lang = this.language.code; 1478 | document.documentElement.dir = this.language.direction; 1479 | translateFragment.call(this, document.documentElement); 1480 | } 1481 | 1482 | function translateFragment(element) { 1483 | if (element.hasAttribute('data-l10n-id')) { 1484 | translateElement.call(this, element); 1485 | } 1486 | 1487 | var nodes = getTranslatableChildren(element); 1488 | for (var i = 0; i < nodes.length; i++ ) { 1489 | translateElement.call(this, nodes[i]); 1490 | } 1491 | } 1492 | 1493 | function setL10nAttributes(element, id, args) { 1494 | element.setAttribute('data-l10n-id', id); 1495 | if (args) { 1496 | element.setAttribute('data-l10n-args', JSON.stringify(args)); 1497 | } 1498 | } 1499 | 1500 | function getL10nAttributes(element) { 1501 | return { 1502 | id: element.getAttribute('data-l10n-id'), 1503 | args: JSON.parse(element.getAttribute('data-l10n-args')) 1504 | }; 1505 | } 1506 | 1507 | function getTranslatableChildren(element) { 1508 | return element ? element.querySelectorAll('*[data-l10n-id]') : []; 1509 | } 1510 | 1511 | function localizeElement(element, id, args) { 1512 | if (!id) { 1513 | element.removeAttribute('data-l10n-id'); 1514 | element.removeAttribute('data-l10n-args'); 1515 | setTextContent(element, ''); 1516 | return; 1517 | } 1518 | 1519 | element.setAttribute('data-l10n-id', id); 1520 | if (args && typeof args === 'object') { 1521 | element.setAttribute('data-l10n-args', JSON.stringify(args)); 1522 | } else { 1523 | element.removeAttribute('data-l10n-args'); 1524 | } 1525 | } 1526 | 1527 | function translateElement(element) { 1528 | var l10n = getL10nAttributes(element); 1529 | 1530 | if (!l10n.id) { 1531 | return false; 1532 | } 1533 | 1534 | var entity = this.ctx.getEntity(l10n.id, l10n.args); 1535 | 1536 | if (!entity) { 1537 | return false; 1538 | } 1539 | 1540 | if (typeof entity === 'string') { 1541 | setTextContent(element, entity); 1542 | return true; 1543 | } 1544 | 1545 | if (entity.value) { 1546 | setTextContent(element, entity.value); 1547 | } 1548 | 1549 | for (var key in entity.attributes) { 1550 | var attr = entity.attributes[key]; 1551 | if (key === 'ariaLabel') { 1552 | element.setAttribute('aria-label', attr); 1553 | } else if (key === 'innerHTML') { 1554 | // XXX: to be removed once bug 994357 lands 1555 | element.innerHTML = attr; 1556 | } else { 1557 | element.setAttribute(key, attr); 1558 | } 1559 | } 1560 | 1561 | return true; 1562 | } 1563 | 1564 | function setTextContent(element, text) { 1565 | // standard case: no element children 1566 | if (!element.firstElementChild) { 1567 | element.textContent = text; 1568 | return; 1569 | } 1570 | 1571 | // this element has element children: replace the content of the first 1572 | // (non-blank) child textNode and clear other child textNodes 1573 | var found = false; 1574 | var reNotBlank = /\S/; 1575 | for (var child = element.firstChild; child; child = child.nextSibling) { 1576 | if (child.nodeType === Node.TEXT_NODE && 1577 | reNotBlank.test(child.nodeValue)) { 1578 | if (found) { 1579 | child.nodeValue = ''; 1580 | } else { 1581 | child.nodeValue = text; 1582 | found = true; 1583 | } 1584 | } 1585 | } 1586 | // if no (non-empty) textNode is found, insert a textNode before the 1587 | // element's first child. 1588 | if (!found) { 1589 | element.insertBefore(document.createTextNode(text), element.firstChild); 1590 | } 1591 | } 1592 | 1593 | })(this); --------------------------------------------------------------------------------