localization.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. 'use strict';
  2. const template = {};
  3. tDocLoader();
  4. function t(key, params) {
  5. const cache = !params && t.cache[key];
  6. const s = cache || chrome.i18n.getMessage(key, params);
  7. if (s === '') {
  8. throw `Missing string "${key}"`;
  9. }
  10. if (!params && !cache) {
  11. t.cache[key] = s;
  12. }
  13. return s;
  14. }
  15. function tHTML(html, tag) {
  16. // body is a text node without HTML tags
  17. if (typeof html === 'string' && !tag && /<\w+/.test(html) === false) {
  18. return document.createTextNode(html);
  19. }
  20. if (typeof html === 'string') {
  21. // spaces are removed; use &nbsp; for an explicit space
  22. html = html.replace(/>\s+</g, '><').trim();
  23. if (tag) {
  24. html = `<${tag}>${html}</${tag}>`;
  25. }
  26. const body = t.DOMParser.parseFromString(html, 'text/html').body;
  27. if (html.includes('i18n-')) {
  28. tNodeList(body.getElementsByTagName('*'));
  29. }
  30. // the html string may contain more than one top-level node
  31. if (!body.childNodes[1]) {
  32. return body.firstChild;
  33. }
  34. const fragment = document.createDocumentFragment();
  35. while (body.firstChild) {
  36. fragment.appendChild(body.firstChild);
  37. }
  38. return fragment;
  39. }
  40. return html;
  41. }
  42. function tNodeList(nodes) {
  43. const PREFIX = 'i18n-';
  44. for (let n = nodes.length; --n >= 0;) {
  45. const node = nodes[n];
  46. if (node.nodeType !== Node.ELEMENT_NODE) {
  47. continue;
  48. }
  49. if (node.localName === 'template') {
  50. createTemplate(node);
  51. continue;
  52. }
  53. for (let a = node.attributes.length; --a >= 0;) {
  54. const attr = node.attributes[a];
  55. const name = attr.nodeName;
  56. if (!name.startsWith(PREFIX)) {
  57. continue;
  58. }
  59. const type = name.substr(PREFIX.length);
  60. const value = t(attr.value);
  61. let toInsert, before;
  62. switch (type) {
  63. case 'word-break':
  64. // we already know that: hasWordBreak
  65. break;
  66. case 'text':
  67. before = node.firstChild;
  68. // fallthrough to text-append
  69. case 'text-append':
  70. toInsert = createText(value);
  71. break;
  72. case 'html': {
  73. toInsert = createHtml(value);
  74. break;
  75. }
  76. default:
  77. node.setAttribute(type, value);
  78. }
  79. tDocLoader.pause();
  80. if (toInsert) {
  81. node.insertBefore(toInsert, before || null);
  82. }
  83. node.removeAttribute(name);
  84. }
  85. }
  86. function createTemplate(node) {
  87. const elements = node.content.querySelectorAll('*');
  88. tNodeList(elements);
  89. template[node.dataset.id] = elements[0];
  90. // compress inter-tag whitespace to reduce number of DOM nodes by 25%
  91. const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
  92. const toRemove = [];
  93. while (walker.nextNode()) {
  94. const textNode = walker.currentNode;
  95. if (!textNode.nodeValue.trim()) {
  96. toRemove.push(textNode);
  97. }
  98. }
  99. tDocLoader.pause();
  100. toRemove.forEach(el => el.remove());
  101. }
  102. function createText(str) {
  103. return document.createTextNode(tWordBreak(str));
  104. }
  105. function createHtml(value) {
  106. // <a href=foo>bar</a> are the only recognizable HTML elements
  107. const rx = /(?:<a\s([^>]*)>([^<]*)<\/a>)?([^<]*)/gi;
  108. const bin = document.createDocumentFragment();
  109. for (let m; (m = rx.exec(value)) && m[0];) {
  110. const [, linkParams, linkText, nextText] = m;
  111. if (linkText) {
  112. const href = /\bhref\s*=\s*(\S+)/.exec(linkParams);
  113. const a = bin.appendChild(document.createElement('a'));
  114. a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || '';
  115. a.appendChild(createText(linkText));
  116. }
  117. if (nextText) {
  118. bin.appendChild(createText(nextText));
  119. }
  120. }
  121. return bin;
  122. }
  123. }
  124. function tDocLoader() {
  125. t.DOMParser = new DOMParser();
  126. t.cache = (() => {
  127. try {
  128. return JSON.parse(localStorage.L10N);
  129. } catch (e) {}
  130. })() || {};
  131. // reset L10N cache on UI language change
  132. const UIlang = chrome.i18n.getUILanguage();
  133. if (t.cache.browserUIlanguage !== UIlang) {
  134. t.cache = {browserUIlanguage: UIlang};
  135. localStorage.L10N = JSON.stringify(t.cache);
  136. }
  137. const cacheLength = Object.keys(t.cache).length;
  138. Object.assign(tDocLoader, {
  139. observer: new MutationObserver(process),
  140. start() {
  141. if (!tDocLoader.observing) {
  142. tDocLoader.observing = true;
  143. tDocLoader.observer.observe(document, {subtree: true, childList: true});
  144. }
  145. },
  146. stop() {
  147. tDocLoader.pause();
  148. document.removeEventListener('DOMContentLoaded', onLoad);
  149. },
  150. pause() {
  151. if (tDocLoader.observing) {
  152. tDocLoader.observing = false;
  153. tDocLoader.observer.disconnect();
  154. }
  155. },
  156. });
  157. tNodeList(document.getElementsByTagName('*'));
  158. tDocLoader.start();
  159. document.addEventListener('DOMContentLoaded', onLoad);
  160. function process(mutations) {
  161. for (const mutation of mutations) {
  162. tNodeList(mutation.addedNodes);
  163. }
  164. tDocLoader.start();
  165. }
  166. function onLoad() {
  167. tDocLoader.stop();
  168. process(tDocLoader.observer.takeRecords());
  169. if (cacheLength !== Object.keys(t.cache).length) {
  170. localStorage.L10N = JSON.stringify(t.cache);
  171. }
  172. }
  173. }
  174. function tWordBreak(text) {
  175. // adds soft hyphens every 10 characters to ensure the long words break before breaking the layout
  176. return text.length <= 10 ? text : text.replace(/[\d\w\u0080-\uFFFF]{10}|((?!\s)\W){10}/g, '$&\u00AD');
  177. }