extract-intl.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * This script will extract the internationalization messages from all components
  3. and package them in the translation json files in the translations file.
  4. */
  5. require('shelljs/global')
  6. const fs = require('fs')
  7. const nodeGlob = require('glob')
  8. const { transform } = require('@babel/core')
  9. const get = require('lodash/get')
  10. const animateProgress = require('./helpers/progress')
  11. const addCheckmark = require('./helpers/checkmark')
  12. const { appLocales, DEFAULT_LOCALE } = require('../../app/i18n')
  13. const babel = require('../../babel.config.js')
  14. const { presets } = babel
  15. let plugins = babel.plugins || []
  16. plugins.push('react-intl')
  17. // Glob to match all js files except test files
  18. const FILES_TO_PARSE = 'app/**/!(*.test).js'
  19. const newLine = () => process.stdout.write('\n')
  20. // Progress Logger
  21. let progress
  22. const task = (message) => {
  23. progress = animateProgress(message)
  24. process.stdout.write(message)
  25. return (error) => {
  26. if (error) {
  27. process.stderr.write(error)
  28. }
  29. clearTimeout(progress)
  30. return addCheckmark(() => newLine())
  31. }
  32. }
  33. // Wrap async functions below into a promise
  34. const glob = (pattern) =>
  35. new Promise((resolve, reject) => {
  36. nodeGlob(
  37. pattern,
  38. (error, value) => (error ? reject(error) : resolve(value))
  39. )
  40. })
  41. const readFile = (fileName) =>
  42. new Promise((resolve, reject) => {
  43. fs.readFile(
  44. fileName,
  45. 'utf8',
  46. (error, value) => (error ? reject(error) : resolve(value))
  47. )
  48. })
  49. // Store existing translations into memory
  50. const oldLocaleMappings = []
  51. const localeMappings = []
  52. // Loop to run once per locale
  53. for (const locale of appLocales) {
  54. oldLocaleMappings[locale] = {}
  55. localeMappings[locale] = {}
  56. // File to store translation messages into
  57. const translationFileName = `app/translations/${locale}.json`
  58. try {
  59. // Parse the old translation message JSON files
  60. const messages = JSON.parse(fs.readFileSync(translationFileName))
  61. const messageKeys = Object.keys(messages)
  62. for (const messageKey of messageKeys) {
  63. oldLocaleMappings[locale][messageKey] = messages[messageKey]
  64. }
  65. } catch (error) {
  66. if (error.code !== 'ENOENT') {
  67. process.stderr.write(
  68. `There was an error loading this translation file: ${translationFileName}
  69. \n${error}`
  70. )
  71. }
  72. }
  73. }
  74. const extractFromFile = async filename => {
  75. try {
  76. const code = await readFile(filename)
  77. const output = await transform(code, { filename, presets, plugins })
  78. const messages = get(output, 'metadata.react-intl.messages', [])
  79. for (const message of messages) {
  80. for (const locale of appLocales) {
  81. const oldLocaleMapping = oldLocaleMappings[locale][message.id]
  82. // Merge old translations into the babel extracted instances where react-intl is used
  83. const newMsg = locale === DEFAULT_LOCALE ? message.defaultMessage : ''
  84. localeMappings[locale][message.id] = oldLocaleMapping || newMsg
  85. }
  86. }
  87. } catch (error) {
  88. process.stderr.write(`\nError transforming file: ${filename}\n${error}\n`)
  89. }
  90. }
  91. const memoryTask = glob(FILES_TO_PARSE)
  92. const memoryTaskDone = task('Storing language files in memory')
  93. memoryTask.then(files => {
  94. memoryTaskDone()
  95. const extractTask = Promise.all(
  96. files.map(fileName => extractFromFile(fileName)),
  97. )
  98. const extractTaskDone = task('Run extraction on all files')
  99. // Run extraction on all files that match the glob on line 16
  100. extractTask.then(result => {
  101. extractTaskDone()
  102. // Make the directory if it doesn't exist, especially for first run
  103. mkdir('-p', 'app/translations')
  104. let localeTaskDone
  105. let translationFileName
  106. for (const locale of appLocales) {
  107. translationFileName = `app/translations/${locale}.json`
  108. localeTaskDone = task(
  109. `Writing translation messages for ${locale} to: ${translationFileName}`
  110. )
  111. // Sort the translation JSON file so that git diffing is easier
  112. // Otherwise the translation messages will jump around every time we extract
  113. const messages = {}
  114. Object.keys(localeMappings[locale])
  115. .sort()
  116. .forEach((key) => {
  117. messages[key] = localeMappings[locale][key]
  118. })
  119. // Write to file the JSON representation of the translation messages
  120. const prettified = `${JSON.stringify(messages, null, 2)}\n`
  121. try {
  122. fs.writeFileSync(translationFileName, prettified)
  123. localeTaskDone()
  124. } catch (error) {
  125. localeTaskDone(
  126. `There was an error saving this translation file: ${translationFileName}
  127. \n${error}`,
  128. )
  129. }
  130. }
  131. process.exit()
  132. })
  133. })