localization.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. 'use strict';
  2. //#region Exports
  3. function t(key, params, strict = true) {
  4. const s = chrome.i18n.getMessage(key, params);
  5. if (!s && strict) throw `Missing string "${key}"`;
  6. return s;
  7. }
  8. Object.assign(t, {
  9. template: {},
  10. parser: new DOMParser(),
  11. ALLOWED_TAGS: ['a', 'b', 'code', 'i', 'sub', 'sup', 'wbr'],
  12. RX_WORD_BREAK: new RegExp([
  13. '(',
  14. /[\d\w\u007B-\uFFFF]{10}/,
  15. '|',
  16. /[\d\w\u007B-\uFFFF]{5,10}[!-/]/,
  17. '|',
  18. /((?!\s)\W){10}/,
  19. ')',
  20. /(?!\b|\s|$)/,
  21. ].map(rx => rx.source || rx).join(''), 'gu'),
  22. HTML(html) {
  23. return typeof html !== 'string'
  24. ? html
  25. : /<\w+/.test(html) // check for html tags
  26. ? t.createHtml(html.replace(/>\n\s*</g, '><').trim())
  27. : document.createTextNode(html);
  28. },
  29. NodeList(nodes) {
  30. const PREFIX = 'i18n-';
  31. for (let n = nodes.length; --n >= 0;) {
  32. const node = nodes[n];
  33. if (node.nodeType !== Node.ELEMENT_NODE) {
  34. continue;
  35. }
  36. if (node.localName === 'template') {
  37. t.createTemplate(node);
  38. continue;
  39. }
  40. for (let a = node.attributes.length; --a >= 0;) {
  41. const attr = node.attributes[a];
  42. const name = attr.nodeName;
  43. if (!name.startsWith(PREFIX)) {
  44. continue;
  45. }
  46. const type = name.substr(PREFIX.length);
  47. const value = t(attr.value);
  48. let toInsert, before;
  49. switch (type) {
  50. case 'word-break':
  51. // we already know that: hasWordBreak
  52. break;
  53. case 'text':
  54. before = node.firstChild;
  55. // fallthrough to text-append
  56. case 'text-append':
  57. toInsert = t.createText(value);
  58. break;
  59. case 'html': {
  60. toInsert = t.createHtml(value);
  61. break;
  62. }
  63. default:
  64. node.setAttribute(type, value);
  65. }
  66. t.stopObserver();
  67. if (toInsert) {
  68. node.insertBefore(toInsert, before || null);
  69. }
  70. node.removeAttribute(name);
  71. }
  72. }
  73. },
  74. /** Adds soft hyphens every 10 characters to ensure the long words break before breaking the layout */
  75. breakWord(text) {
  76. return text.length <= 10 ? text :
  77. text.replace(t.RX_WORD_BREAK, '$&\u00AD');
  78. },
  79. createTemplate(node) {
  80. const elements = node.content.querySelectorAll('*');
  81. t.NodeList(elements);
  82. t.template[node.dataset.id] = elements[0];
  83. // compress inter-tag whitespace to reduce number of DOM nodes by 25%
  84. const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
  85. const toRemove = [];
  86. while (walker.nextNode()) {
  87. const textNode = walker.currentNode;
  88. if (!/[\xA0\S]/.test(textNode.nodeValue)) { // allow \xA0 to keep &nbsp;
  89. toRemove.push(textNode);
  90. }
  91. }
  92. t.stopObserver();
  93. toRemove.forEach(el => el.remove());
  94. },
  95. createText(str) {
  96. return document.createTextNode(t.breakWord(str));
  97. },
  98. createHtml(str, trusted) {
  99. const root = t.parser.parseFromString(str, 'text/html').body;
  100. if (!trusted) {
  101. t.sanitizeHtml(root);
  102. } else if (str.includes('i18n-')) {
  103. t.NodeList(root.getElementsByTagName('*'));
  104. }
  105. const bin = document.createDocumentFragment();
  106. while (root.firstChild) {
  107. bin.appendChild(root.firstChild);
  108. }
  109. return bin;
  110. },
  111. sanitizeHtml(root) {
  112. const toRemove = [];
  113. const walker = document.createTreeWalker(root);
  114. for (let n; (n = walker.nextNode());) {
  115. if (n.nodeType === Node.TEXT_NODE) {
  116. n.nodeValue = t.breakWord(n.nodeValue);
  117. } else if (t.ALLOWED_TAGS.includes(n.localName)) {
  118. for (const attr of n.attributes) {
  119. if (n.localName !== 'a' || attr.localName !== 'href' || !/^https?:/.test(n.href)) {
  120. n.removeAttribute(attr.name);
  121. }
  122. }
  123. } else {
  124. toRemove.push(n);
  125. }
  126. }
  127. for (const n of toRemove) {
  128. const parent = n.parentNode;
  129. if (parent) parent.removeChild(n); // not using .remove() as there may be a non-element
  130. }
  131. },
  132. _intl: null,
  133. _intlY: null,
  134. _intlYHM: null,
  135. _intlWYHM: null,
  136. formatDate(date, needsTime) {
  137. if (!date) {
  138. return '';
  139. }
  140. try {
  141. const now = new Date();
  142. const newDate = new Date(Number(date) || date);
  143. const needsYear = newDate.getYear() !== now.getYear();
  144. const needsWeekDay = needsTime && (now - newDate <= 7 * 24 * 3600e3);
  145. const intlKey = `_intl${needsWeekDay ? 'W' : ''}${needsYear ? 'Y' : ''}${needsTime ? 'HM' : ''}`;
  146. const intl = t[intlKey] ||
  147. (t[intlKey] = new Intl.DateTimeFormat([chrome.i18n.getUILanguage(), 'en'], {
  148. day: 'numeric',
  149. month: 'short',
  150. year: needsYear ? '2-digit' : undefined,
  151. hour: needsTime ? 'numeric' : undefined,
  152. minute: needsTime ? '2-digit' : undefined,
  153. weekday: needsWeekDay ? 'long' : undefined,
  154. }));
  155. const string = intl.format(newDate);
  156. return string === 'Invalid Date' ? '' : string;
  157. } catch (e) {
  158. return '';
  159. }
  160. },
  161. });
  162. //#endregion
  163. //#region Internals
  164. (() => {
  165. const observer = new MutationObserver(process);
  166. let observing = false;
  167. Object.assign(t, {
  168. stopObserver() {
  169. if (observing) {
  170. observing = false;
  171. observer.disconnect();
  172. }
  173. },
  174. });
  175. document.addEventListener('DOMContentLoaded', () => {
  176. process(observer.takeRecords());
  177. t.stopObserver();
  178. }, {once: true});
  179. t.NodeList(document.getElementsByTagName('*'));
  180. start();
  181. function process(mutations) {
  182. mutations.forEach(m => t.NodeList(m.addedNodes));
  183. start();
  184. }
  185. function start() {
  186. if (!observing) {
  187. observing = true;
  188. observer.observe(document, {subtree: true, childList: true});
  189. }
  190. }
  191. })();
  192. //#endregion