download.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. const path = require('path')
  2. const { promises: fs } = require('fs')
  3. const oneSky = require('@brainly/onesky-utils')
  4. const sanitizeHtml = require('sanitize-html')
  5. const { withAuth } = require('./config')
  6. async function run() {
  7. try {
  8. // The recommended OneSky set-up appears to require an API request to
  9. // generate files on their side, which you could then request and use. We
  10. // only have 1 such file that appears to be misnamed (en-US, despite our
  11. // translations being marked as GB) and very out-of-date.
  12. // However by requesting the "multilingual file" for this file, we get all
  13. // of the translations
  14. const content = await oneSky.getMultilingualFile(
  15. withAuth({
  16. fileName: 'en-US.json',
  17. })
  18. )
  19. const json = JSON.parse(content)
  20. for (const [code, lang] of Object.entries(json)) {
  21. if (code === 'en-GB') {
  22. // OneSky does not have read-after-write consistency.
  23. // Skip the dump of English locales, which may not include locales
  24. // that were just uploaded.
  25. continue
  26. }
  27. for (let [key, value] of Object.entries(lang.translation)) {
  28. // Handle multi-line strings as arrays by joining on newline
  29. if (Array.isArray(value)) {
  30. value = value.join('\n')
  31. }
  32. lang.translation[key] = sanitize(value)
  33. }
  34. await fs.writeFile(
  35. path.join(__dirname, `/../../locales/${code}.json`),
  36. JSON.stringify(lang.translation, null, 2) + '\n'
  37. )
  38. }
  39. } catch (error) {
  40. console.error(error)
  41. process.exit(1)
  42. }
  43. }
  44. run()
  45. /**
  46. * Sanitize a translation string to prevent injection attacks
  47. *
  48. * @param {string} input
  49. * @returns {string}
  50. */
  51. function sanitize(input) {
  52. // Block Angular XSS
  53. // Ticket: https://github.com/overleaf/issues/issues/4478
  54. input = input.replace(/'/g, '’')
  55. // Use left quote where (likely) appropriate.
  56. input.replace(/ ’/g, ' ‘')
  57. return sanitizeHtml(input, {
  58. // Allow "replacement" tags (in the format <0>, <1>, <2>, etc) used by
  59. // react-i18next to allow for HTML insertion via the Trans component.
  60. // See: https://github.com/overleaf/developer-manual/blob/master/code/translations.md
  61. // Unfortunately the sanitizeHtml library does not accept regexes or a
  62. // function for the allowedTags option, so we are limited to a hard-coded
  63. // number of "replacement" tags.
  64. allowedTags: ['b', 'strong', 'a', 'code', ...range(10)],
  65. allowedAttributes: {
  66. a: ['href', 'class'],
  67. },
  68. textFilter(text) {
  69. return text
  70. .replace(/\{\{/, '&#123;&#123;')
  71. .replace(/\}\}/, '&#125;&#125;')
  72. },
  73. })
  74. }
  75. /**
  76. * Generate a range of numbers as strings up to the given size
  77. *
  78. * @param {number} size Size of range
  79. * @returns {string[]}
  80. */
  81. function range(size) {
  82. return Array.from(Array(size).keys()).map(n => n.toString())
  83. }