I18n.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. 'use strict';
  2. const common = require('../common');
  3. const fc = require('fast-check');
  4. describe('I18n', function () {
  5. describe('translate', function () {
  6. this.timeout(30000);
  7. before(function () {
  8. PrivateBin.I18n.reset();
  9. });
  10. it('returns message ID unchanged if no translation found', () => {
  11. fc.assert(fc.property(
  12. fc.string(),
  13. function (messageId) {
  14. messageId = messageId.replace(/%(s|d)/g, '%%');
  15. var plurals = [messageId, messageId + 's'],
  16. fake = [messageId],
  17. result = PrivateBin.I18n.translate(messageId);
  18. PrivateBin.I18n.reset();
  19. var alias = PrivateBin.I18n._(messageId);
  20. PrivateBin.I18n.reset();
  21. var pluralResult = PrivateBin.I18n.translate(plurals);
  22. PrivateBin.I18n.reset();
  23. var pluralAlias = PrivateBin.I18n._(plurals);
  24. PrivateBin.I18n.reset();
  25. var fakeResult = PrivateBin.I18n.translate(fake);
  26. PrivateBin.I18n.reset();
  27. var fakeAlias = PrivateBin.I18n._(fake);
  28. PrivateBin.I18n.reset();
  29. if (messageId.indexOf('<a') === -1) {
  30. messageId = PrivateBin.Helper.htmlEntities(messageId);
  31. } else {
  32. messageId = DOMPurify.sanitize(
  33. messageId, {
  34. ALLOWED_TAGS: ['a', 'i', 'span'],
  35. ALLOWED_ATTR: ['href', 'id']
  36. }
  37. );
  38. }
  39. return messageId === result && messageId === alias &&
  40. messageId === pluralResult && messageId === pluralAlias &&
  41. messageId === fakeResult && messageId === fakeAlias;
  42. }
  43. ));
  44. });
  45. it('replaces %s in strings with first given parameter, encoding all, when no link is in the messageID', () => {
  46. fc.assert(fc.property(
  47. fc.string(),
  48. fc.array(fc.string(), {minLength: 1}),
  49. fc.string(),
  50. function (prefix, params, postfix) {
  51. prefix = prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
  52. params[0] = params[0].replace(/%(s|d)/g, '%%');
  53. postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
  54. const translation = PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
  55. params.unshift(prefix + '%s' + postfix);
  56. const result = PrivateBin.I18n.translate.apply(this, params);
  57. PrivateBin.I18n.reset();
  58. const alias = PrivateBin.I18n._.apply(this, params);
  59. PrivateBin.I18n.reset();
  60. return translation === result && translation === alias;
  61. }
  62. ));
  63. });
  64. it('replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID', () => {
  65. fc.assert(fc.property(
  66. fc.string(),
  67. fc.array(fc.string(), {minLength: 1}),
  68. fc.string(),
  69. function (prefix, params, postfix) {
  70. prefix = prefix.replace(/%(s|d)/g, '%%');
  71. params[0] = params[0].replace(/%(s|d)/g, '%%');
  72. postfix = postfix.replace(/%(s|d)/g, '%%');
  73. const translation = DOMPurify.sanitize(
  74. prefix + '<a href="' + params[0] + '"></a>' + postfix, {
  75. ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
  76. ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
  77. ALLOWED_ATTR: ['href', 'id']
  78. }
  79. );
  80. params.unshift(prefix + '<a href="%s"></a>' + postfix);
  81. const result = PrivateBin.I18n.translate.apply(this, params);
  82. PrivateBin.I18n.reset();
  83. const alias = PrivateBin.I18n._.apply(this, params);
  84. PrivateBin.I18n.reset();
  85. return translation === result && translation === alias;
  86. }
  87. ));
  88. });
  89. it('replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID', () => {
  90. fc.assert(fc.property(
  91. fc.string(),
  92. fc.array(fc.string(), {minLength: 1}),
  93. fc.string(),
  94. function (prefix, params, postfix) {
  95. prefix = prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
  96. params[0] = params[0].replace(/%(s|d)/g, '%%');
  97. postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
  98. const tempDiv = document.createElement('textarea');
  99. tempDiv.textContent = (prefix + params[0] + postfix);
  100. const translation = tempDiv.textContent;
  101. let args = Array.prototype.slice.call(params);
  102. args.unshift(prefix + '%s' + postfix);
  103. let clean = globalThis.cleanup();
  104. document.body.innerHTML = '<div id="i18n"></div>';
  105. const i18nElement = document.getElementById('i18n');
  106. args.unshift(i18nElement);
  107. PrivateBin.I18n.translate.apply(this, args);
  108. const result = i18nElement.textContent;
  109. PrivateBin.I18n.reset();
  110. clean();
  111. clean = globalThis.cleanup();
  112. document.body.innerHTML = '<div id="i18n"></div>';
  113. args[0] = document.getElementById('i18n');
  114. PrivateBin.I18n._.apply(this, args);
  115. const alias = document.getElementById('i18n').textContent;
  116. PrivateBin.I18n.reset();
  117. clean();
  118. return translation === result && translation === alias;
  119. }
  120. ));
  121. });
  122. it('replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted', () => {
  123. fc.assert(fc.property(
  124. fc.string(),
  125. fc.array(fc.string(), {minLength: 1}),
  126. fc.string(),
  127. function (prefix, params, postfix) {
  128. prefix = prefix.replace(/%(s|d)/g, '%%').trim();
  129. params[0] = params[0].replace(/%(s|d)/g, '%%').trim();
  130. postfix = postfix.replace(/%(s|d)/g, '%%').trim();
  131. const translation = DOMPurify.sanitize(
  132. prefix + '<a href="' + params[0] + '"></a>' + postfix, {
  133. ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
  134. ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
  135. ALLOWED_ATTR: ['href', 'id']
  136. }
  137. );
  138. let args = Array.prototype.slice.call(params);
  139. args.unshift(prefix + '<a href="%s"></a>' + postfix);
  140. let clean = globalThis.cleanup();
  141. document.body.innerHTML = '<div id="i18n"></div>';
  142. const i18nElement2 = document.getElementById('i18n');
  143. args.unshift(i18nElement2);
  144. PrivateBin.I18n.translate.apply(this, args);
  145. const result = i18nElement2.innerHTML;
  146. PrivateBin.I18n.reset();
  147. clean();
  148. clean = globalThis.cleanup();
  149. document.body.innerHTML = '<div id="i18n"></div>';
  150. args[0] = document.getElementById('i18n');
  151. PrivateBin.I18n._.apply(this, args);
  152. const alias = document.getElementById('i18n').innerHTML;
  153. PrivateBin.I18n.reset();
  154. clean();
  155. return translation === result && translation === alias;
  156. }
  157. ));
  158. });
  159. });
  160. describe('getPluralForm', function () {
  161. before(function () {
  162. PrivateBin.I18n.reset();
  163. });
  164. it('returns valid key for plural form', () => {
  165. fc.assert(fc.property(
  166. common.fcSupportedLanguages(),
  167. fc.integer(),
  168. function(language, n) {
  169. PrivateBin.I18n.reset(language);
  170. var result = PrivateBin.I18n.getPluralForm(n);
  171. // arabic seems to have the highest plural count with 6 forms
  172. return result >= 0 && result <= 5;
  173. }
  174. ));
  175. });
  176. });
  177. // loading of JSON via AJAX needs to be tested in the browser, this just mocks it
  178. // TODO: This needs to be tested using a browser.
  179. describe('loadTranslations', function () {
  180. this.timeout(30000);
  181. before(function () {
  182. PrivateBin.I18n.reset();
  183. });
  184. it('downloads and handles any supported language', () => {
  185. fc.assert(fc.property(
  186. common.fcSupportedLanguages(),
  187. function(language) {
  188. // cleanup
  189. var clean = globalThis.cleanup('', {cookie: ['lang=en']});
  190. PrivateBin.I18n.reset('en');
  191. PrivateBin.I18n.loadTranslations();
  192. clean();
  193. // mock
  194. clean = globalThis.cleanup('', {cookie: ['lang=' + language]});
  195. // eslint-disable-next-line global-require
  196. PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
  197. var loadedLang = PrivateBin.I18n.getLanguage(),
  198. result = PrivateBin.I18n.translate('Never'),
  199. alias = PrivateBin.I18n._('Never');
  200. clean();
  201. return language === loadedLang && result === alias;
  202. }
  203. ));
  204. });
  205. it('should default to en', () => {
  206. var clean = globalThis.cleanup('', {url: 'https://privatebin.net/'});
  207. // when navigator.userLanguage is undefined and no default language
  208. // is specified, it would throw an error
  209. [ 'language', 'userLanguage' ].forEach(function (key) {
  210. Object.defineProperty(navigator, key, {
  211. value: undefined,
  212. configurable: true,
  213. enumerable: true,
  214. writable: false
  215. });
  216. });
  217. PrivateBin.I18n.reset('en');
  218. PrivateBin.I18n.loadTranslations();
  219. var result = PrivateBin.I18n.translate('Never'),
  220. alias = PrivateBin.I18n._('Never');
  221. clean();
  222. return 'Never' === result && 'Never' === alias;
  223. });
  224. });
  225. });