l10n.js 38 KB

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