i18n.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  4. Copyright (C) 2019-2022 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://gitlab.com/vannilla/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. 'use strict';
  19. // This file should always be included at the end of the `body` tag, so as
  20. // to ensure all i18n targets are already loaded.
  21. (function() {
  22. // https://github.com/gorhill/uBlock/issues/2084
  23. // Anything else than <a>, <b>, <code>, <em>, <i>, <input>, and
  24. // <span> will be rendered as plain text. For <input>, only the
  25. // type attribute is allowed. For <a>, only href attribute must
  26. // be present, and it MUST starts with `https://`, and includes no
  27. // single- or double-quotes. No HTML entities are allowed, there
  28. // is code to handle existing HTML entities already present in
  29. // translation files until they are all gone.
  30. //
  31. // ηMatrix:
  32. // We're not going to remove anything, but rather going to make
  33. // full use of HTML tags and HTML entities in translations. Of
  34. // course, this check for safe tags is going to stay and will be
  35. // used to check the source text. The above comment is kept just
  36. // in case.
  37. let reSafeTags =
  38. /^([\s\S]*?)<(b|blockquote|code|em|i|kbd|span|sup)>(.+?)<\/\2>([\s\S]*)$/;
  39. let reSafeInput = /^([\s\S]*?)<(input type="[^"]+")>(.*?)([\s\S]*)$/;
  40. let reInput = /^input type=(['"])([a-z]+)\1$/;
  41. let reSafeLink =
  42. /^([\s\S]*?)<(a href=['"]https?:\/\/[^'" <>]+['"])>(.+?)<\/a>([\s\S]*)$/;
  43. let reLink = /^a href=(['"])(https?:\/\/[^'"]+)\1$/;
  44. let safeTextToTagNode = function (text) {
  45. let matches;
  46. let node;
  47. if (text.lastIndexOf('a ', 0) === 0) {
  48. matches = reLink.exec(text);
  49. if (matches === null) {
  50. return null;
  51. }
  52. node = document.createElement('a');
  53. node.setAttribute('href', matches[2]);
  54. return node;
  55. }
  56. if (text.lastIndexOf('input ', 0) === 0) {
  57. matches = reInput.exec(text);
  58. if (matches === null) {
  59. return null;
  60. }
  61. node = document.createElement('input');
  62. node.setAttribute('type', matches[2]);
  63. return node;
  64. }
  65. // Firefox extension validator warns if using a variable as
  66. // argument for document.createElement().
  67. // ηMatrix: is it important for us?
  68. // ηMatrix (4.4.3 onwards): let's just use the variable and
  69. // hope for the best, no need to have a redundant switch.
  70. /*
  71. switch (text) {
  72. case 'b':
  73. return document.createElement('b');
  74. case 'blockquote':
  75. return document.createElement('blockquote');
  76. case 'code':
  77. return document.createElement('code');
  78. case 'em':
  79. return document.createElement('em');
  80. case 'i':
  81. return document.createElement('i');
  82. case 'kbd':
  83. return document.createElement('kbd');
  84. case 'span':
  85. return document.createElement('span');
  86. case 'sup':
  87. return document.createElement('sup');
  88. default:
  89. break;
  90. }
  91. */
  92. return document.createElement(text);
  93. };
  94. let safeTextToTextNode = function (text) {
  95. if (text.indexOf('&') !== -1) {
  96. text = text
  97. .replace(/&ldquo;/g, '“')
  98. .replace(/&rdquo;/g, '”')
  99. .replace(/&lsquo;/g, '‘')
  100. .replace(/&rsquo;/g, '’')
  101. .replace(/&lt;/g, '<')
  102. .replace(/&gt;/g, '>')
  103. .replace(/&apos;/g, '\'');
  104. }
  105. return document.createTextNode(text);
  106. };
  107. let safeTextToDOM = function (text, parent) {
  108. if (text === '') {
  109. return;
  110. }
  111. if (text.indexOf('<') === -1) {
  112. return parent.appendChild(safeTextToTextNode(text));
  113. }
  114. // Using the raw <p> element is not allowed for security reason,
  115. // but it's good for formatting content, so here it's substituted
  116. // for a safer equivalent (for the extension.)
  117. text = text
  118. .replace(/^<p>|<\/p>/g, '')
  119. .replace(/<p>/g, '\n\n');
  120. let matches;
  121. let matches1 = reSafeTags.exec(text);
  122. let matches2 = reSafeLink.exec(text);
  123. if (matches1 !== null && matches2 !== null) {
  124. matches = matches1.index < matches2.index ? matches1 : matches2;
  125. } else if (matches1 !== null) {
  126. matches = matches1;
  127. } else if (matches2 !== null) {
  128. matches = matches2;
  129. } else {
  130. matches = reSafeInput.exec(text);
  131. }
  132. if (matches === null) {
  133. parent.appendChild(safeTextToTextNode(text));
  134. return;
  135. }
  136. safeTextToDOM(matches[1], parent);
  137. let node = safeTextToTagNode(matches[2]) || parent;
  138. safeTextToDOM(matches[3], node);
  139. parent.appendChild(node);
  140. safeTextToDOM(matches[4], parent);
  141. };
  142. // Helper to deal with the i18n'ing of HTML files.
  143. vAPI.i18n.render = function (context) {
  144. let docu = document;
  145. let root = context || docu;
  146. let i, elem, text;
  147. let elems = root.querySelectorAll('[data-i18n]');
  148. let n = elems.length;
  149. for (i=0; i<n; ++i) {
  150. elem = elems[i];
  151. text = vAPI.i18n(elem.getAttribute('data-i18n'));
  152. if (!text) {
  153. continue;
  154. }
  155. // TODO: remove once it's all replaced with <input type="...">
  156. if (text.indexOf('{') !== -1) {
  157. text =
  158. text.replace(/\{\{input:([^}]+)\}\}/g, '<input type="$1">');
  159. }
  160. safeTextToDOM(text, elem);
  161. }
  162. uDom('[title]', context).forEach(function (elem) {
  163. let title = vAPI.i18n(elem.attr('title'));
  164. if (title) {
  165. elem.attr('title', title);
  166. }
  167. });
  168. uDom('[placeholder]', context).forEach(function (elem) {
  169. elem.attr('placeholder', vAPI.i18n(elem.attr('placeholder')));
  170. });
  171. uDom('[data-i18n-tip]', context).forEach(function (elem) {
  172. elem.attr('data-tip',
  173. vAPI.i18n(elem.attr('data-i18n-tip'))
  174. .replace(/<br>/g, '\n')
  175. .replace(/\n{3,}/g, '\n\n'));
  176. });
  177. };
  178. vAPI.i18n.render();
  179. vAPI.i18n.renderElapsedTimeToString = function (tstamp) {
  180. let value = (Date.now() - tstamp) / 60000;
  181. if (value < 2) {
  182. return vAPI.i18n('elapsedOneMinuteAgo');
  183. }
  184. if (value < 60) {
  185. return vAPI
  186. .i18n('elapsedManyMinutesAgo')
  187. .replace('{{value}}', Math.floor(value).toLocaleString());
  188. }
  189. value /= 60;
  190. if (value < 2) {
  191. return vAPI.i18n('elapsedOneHourAgo');
  192. }
  193. if (value < 24) {
  194. return vAPI
  195. .i18n('elapsedManyHoursAgo')
  196. .replace('{{value}}', Math.floor(value).toLocaleString());
  197. }
  198. value /= 24;
  199. if (value < 2) {
  200. return vAPI.i18n('elapsedOneDayAgo');
  201. }
  202. return vAPI
  203. .i18n('elapsedManyDaysAgo')
  204. .replace('{{value}}', Math.floor(value).toLocaleString());
  205. };
  206. })();