i18n.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  4. Copyright (C) 2019-2020 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://libregit.spks.xyz/heckyel/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. switch (text) {
  69. case 'b':
  70. return document.createElement('b');
  71. case 'blockquote':
  72. return document.createElement('blockquote');
  73. case 'code':
  74. return document.createElement('code');
  75. case 'em':
  76. return document.createElement('em');
  77. case 'i':
  78. return document.createElement('i');
  79. case 'kbd':
  80. return document.createElement('kbd');
  81. case 'span':
  82. return document.createElement('span');
  83. case 'sup':
  84. return document.createElement('sup');
  85. default:
  86. break;
  87. }
  88. };
  89. let safeTextToTextNode = function (text) {
  90. if (text.indexOf('&') !== -1) {
  91. text = text
  92. .replace(/&ldquo;/g, '“')
  93. .replace(/&rdquo;/g, '”')
  94. .replace(/&lsquo;/g, '‘')
  95. .replace(/&rsquo;/g, '’')
  96. .replace(/&lt;/g, '<')
  97. .replace(/&gt;/g, '>')
  98. .replace(/&apos;/g, '\'');
  99. }
  100. return document.createTextNode(text);
  101. };
  102. let safeTextToDOM = function (text, parent) {
  103. if (text === '') {
  104. return;
  105. }
  106. if (text.indexOf('<') === -1) {
  107. return parent.appendChild(safeTextToTextNode(text));
  108. }
  109. // Using the raw <p> element is not allowed for security reason,
  110. // but it's good for formatting content, so here it's substituted
  111. // for a safer equivalent (for the extension.)
  112. text = text
  113. .replace(/^<p>|<\/p>/g, '')
  114. .replace(/<p>/g, '\n\n');
  115. let matches;
  116. let matches1 = reSafeTags.exec(text);
  117. let matches2 = reSafeLink.exec(text);
  118. if (matches1 !== null && matches2 !== null) {
  119. matches = matches1.index < matches2.index ? matches1 : matches2;
  120. } else if (matches1 !== null) {
  121. matches = matches1;
  122. } else if (matches2 !== null) {
  123. matches = matches2;
  124. } else {
  125. matches = reSafeInput.exec(text);
  126. }
  127. if (matches === null) {
  128. parent.appendChild(safeTextToTextNode(text));
  129. return;
  130. }
  131. safeTextToDOM(matches[1], parent);
  132. let node = safeTextToTagNode(matches[2]) || parent;
  133. safeTextToDOM(matches[3], node);
  134. parent.appendChild(node);
  135. safeTextToDOM(matches[4], parent);
  136. };
  137. // Helper to deal with the i18n'ing of HTML files.
  138. vAPI.i18n.render = function (context) {
  139. let docu = document;
  140. let root = context || docu;
  141. let i, elem, text;
  142. let elems = root.querySelectorAll('[data-i18n]');
  143. let n = elems.length;
  144. for (i=0; i<n; ++i) {
  145. elem = elems[i];
  146. text = vAPI.i18n(elem.getAttribute('data-i18n'));
  147. if (!text) {
  148. continue;
  149. }
  150. // TODO: remove once it's all replaced with <input type="...">
  151. if (text.indexOf('{') !== -1) {
  152. text =
  153. text.replace(/\{\{input:([^}]+)\}\}/g, '<input type="$1">');
  154. }
  155. safeTextToDOM(text, elem);
  156. }
  157. uDom('[title]', context).forEach(function (elem) {
  158. let title = vAPI.i18n(elem.attr('title'));
  159. if (title) {
  160. elem.attr('title', title);
  161. }
  162. });
  163. uDom('[placeholder]', context).forEach(function (elem) {
  164. elem.attr('placeholder', vAPI.i18n(elem.attr('placeholder')));
  165. });
  166. uDom('[data-i18n-tip]', context).forEach(function (elem) {
  167. elem.attr('data-tip',
  168. vAPI.i18n(elem.attr('data-i18n-tip'))
  169. .replace(/<br>/g, '\n')
  170. .replace(/\n{3,}/g, '\n\n'));
  171. });
  172. };
  173. vAPI.i18n.render();
  174. vAPI.i18n.renderElapsedTimeToString = function (tstamp) {
  175. let value = (Date.now() - tstamp) / 60000;
  176. if (value < 2) {
  177. return vAPI.i18n('elapsedOneMinuteAgo');
  178. }
  179. if (value < 60) {
  180. return vAPI
  181. .i18n('elapsedManyMinutesAgo')
  182. .replace('{{value}}', Math.floor(value).toLocaleString());
  183. }
  184. value /= 60;
  185. if (value < 2) {
  186. return vAPI.i18n('elapsedOneHourAgo');
  187. }
  188. if (value < 24) {
  189. return vAPI
  190. .i18n('elapsedManyHoursAgo')
  191. .replace('{{value}}', Math.floor(value).toLocaleString());
  192. }
  193. value /= 24;
  194. if (value < 2) {
  195. return vAPI.i18n('elapsedOneDayAgo');
  196. }
  197. return vAPI
  198. .i18n('elapsedManyDaysAgo')
  199. .replace('{{value}}', Math.floor(value).toLocaleString());
  200. };
  201. })();