locale-packs.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. const glob = require('glob')
  2. const Uppy = require('../packages/@uppy/core')
  3. const chalk = require('chalk')
  4. const path = require('path')
  5. const flat = require('flat')
  6. const stringifyObject = require('stringify-object')
  7. const fs = require('fs')
  8. const uppy = Uppy()
  9. let localePack = {}
  10. const plugins = {}
  11. const sources = {}
  12. console.warn('\n--> Make sure to run `npm run build:lib` for this locale script to work properly. ')
  13. const mode = process.argv[2]
  14. if (mode === 'build') {
  15. build()
  16. } else if (mode === 'test') {
  17. test()
  18. } else {
  19. throw new Error(`First argument must be either 'build' or 'test'`)
  20. }
  21. function getSources (pluginName) {
  22. const dependencies = {
  23. // because e.g. 'companionAuthError' is used in provider-views but set in Core's defaultLocale
  24. 'core': ['provider-views'],
  25. // because e.g. 'emptyFolderAdded' is used in provider-views but set in Dashboard's defaultLocale
  26. 'dashboard': ['provider-views']
  27. }
  28. const globPath = path.join(__dirname, '..', 'packages', '@uppy', pluginName, 'lib', '**', '*.js')
  29. let contents = glob.sync(globPath).map((file) => {
  30. return fs.readFileSync(file, 'utf-8')
  31. })
  32. if (dependencies[pluginName]) {
  33. dependencies[pluginName].forEach((addPlugin) => {
  34. contents = contents.concat(getSources(addPlugin))
  35. })
  36. }
  37. return contents
  38. }
  39. function buildPluginsList () {
  40. // Go over all uppy plugins, check if they are constructors
  41. // and instanciate them, check for defaultLocale property,
  42. // then add to plugins object
  43. const packagesGlobPath = path.join(__dirname, '..', 'packages', '@uppy', '*', 'package.json')
  44. const files = glob.sync(packagesGlobPath)
  45. console.log(`--> Checked plugins could be instantiated and have defaultLocale in them:\n`)
  46. for (let file of files) {
  47. const dirName = path.dirname(file)
  48. const pluginName = path.basename(dirName)
  49. if (pluginName === 'locales' || pluginName === 'react-native') {
  50. continue
  51. }
  52. const Plugin = require(dirName)
  53. let plugin
  54. // A few hacks to emulate browser environment because e.g.:
  55. // GoldenRetrieves calls upon MetaDataStore in the constructor, which uses localStorage
  56. // @TODO Consider rewriting constructors so they don't make imperative calls that rely on
  57. // browser environment (OR: just keep this browser mocking, if it's only causing issues for this script, it doesn't matter)
  58. global.location = { protocol: 'https' }
  59. global.navigator = {}
  60. global.localStorage = {
  61. key: () => { },
  62. getItem: () => { }
  63. }
  64. global.window = {
  65. indexedDB: {
  66. open: () => { return {} }
  67. }
  68. }
  69. global.document = {
  70. createElement: () => {
  71. return { style: {} }
  72. }
  73. }
  74. try {
  75. if (pluginName === 'provider-views') {
  76. plugin = new Plugin(plugins['drag-drop'], {
  77. companionPattern: '',
  78. companionUrl: 'https://companion.uppy.io'
  79. })
  80. } else if (pluginName === 'store-redux') {
  81. plugin = new Plugin({ store: { dispatch: () => { } } })
  82. } else {
  83. plugin = new Plugin(uppy, {
  84. companionPattern: '',
  85. companionUrl: 'https://companion.uppy.io',
  86. params: {
  87. auth: {
  88. key: 'x'
  89. }
  90. }
  91. })
  92. }
  93. } catch (err) {
  94. if (err.message !== 'Plugin is not a constructor') {
  95. console.error(`--> While trying to instantiate plugin: ${pluginName}, this error was thrown: `)
  96. throw err
  97. }
  98. }
  99. if (plugin && plugin.defaultLocale) {
  100. console.log(`[x] Check plugin: ${pluginName}`)
  101. plugins[pluginName] = plugin
  102. sources[pluginName] = getSources(pluginName)
  103. } else {
  104. console.log(`[ ] Check plugin: ${pluginName}`)
  105. }
  106. }
  107. console.log(``)
  108. return { plugins, sources }
  109. }
  110. function addLocaleToPack (plugin, pluginName) {
  111. const localeStrings = plugin.defaultLocale.strings
  112. for (let key in localeStrings) {
  113. const valueInPlugin = JSON.stringify(localeStrings[key])
  114. const valueInPack = JSON.stringify(localePack[key])
  115. if (key in localePack && valueInPlugin !== valueInPack) {
  116. console.error(`⚠ Plugin ${chalk.magenta(pluginName)} has a duplicate key: ${chalk.magenta(key)}`)
  117. console.error(` Value in plugin: ${chalk.cyan(valueInPlugin)}`)
  118. console.error(` Value in pack : ${chalk.yellow(valueInPack)}`)
  119. console.error()
  120. }
  121. localePack[key] = localeStrings[key]
  122. }
  123. }
  124. function checkForUnused (fileContents, pluginName, localePack) {
  125. let buff = fileContents.join('\n')
  126. for (let key in localePack) {
  127. let regPat = new RegExp(`(i18n|i18nArray)\\([^\\)]*['\`"]${key}['\`"]`, 'g')
  128. if (!buff.match(regPat)) {
  129. console.error(`⚠ defaultLocale key: ${chalk.magenta(key)} not used in plugin: ${chalk.cyan(pluginName)}`)
  130. }
  131. }
  132. }
  133. function sortObjectAlphabetically (obj, sortFunc) {
  134. return Object.keys(obj).sort(sortFunc).reduce(function (result, key) {
  135. result[key] = obj[key]
  136. return result
  137. }, {})
  138. }
  139. function build () {
  140. const { plugins, sources } = buildPluginsList()
  141. for (let pluginName in plugins) {
  142. addLocaleToPack(plugins[pluginName], pluginName)
  143. }
  144. localePack = sortObjectAlphabetically(localePack)
  145. for (let pluginName in sources) {
  146. checkForUnused(sources[pluginName], pluginName, sortObjectAlphabetically(plugins[pluginName].defaultLocale.strings))
  147. }
  148. const prettyLocale = stringifyObject(localePack, {
  149. indent: ' ',
  150. singleQuotes: true,
  151. inlineCharacterLimit: 12
  152. })
  153. const localeTemplatePath = path.join(__dirname, '..', 'packages', '@uppy', 'locales', 'template.js')
  154. const template = fs.readFileSync(localeTemplatePath, 'utf-8')
  155. const finalLocale = template.replace('en_US.strings = {}', 'en_US.strings = ' + prettyLocale)
  156. const localePackagePath = path.join(__dirname, '..', 'packages', '@uppy', 'locales', 'src', 'en_US.js')
  157. fs.writeFileSync(localePackagePath, finalLocale, 'utf-8')
  158. console.log(`✅ Written '${localePackagePath}'`)
  159. }
  160. function test () {
  161. const leadingLocaleName = 'en_US'
  162. const followerLocales = {}
  163. const followerValues = {}
  164. const localePackagePath = path.join(__dirname, '..', 'packages', '@uppy', 'locales', 'src', '*.js')
  165. glob.sync(localePackagePath).forEach((localePath) => {
  166. const localeName = path.basename(localePath, '.js')
  167. // Builds array with items like: 'uploadingXFiles.2'
  168. followerValues[localeName] = flat(require(localePath).strings)
  169. followerLocales[localeName] = Object.keys(followerValues[localeName])
  170. })
  171. // Take aside our leading locale: en_US
  172. const leadingLocale = followerLocales[leadingLocaleName]
  173. const leadingValues = followerValues[leadingLocaleName]
  174. delete followerLocales[leadingLocaleName]
  175. // Compare all follower Locales (RU, DE, etc) with our leader en_US
  176. const warnings = []
  177. const fatals = []
  178. for (let followerName in followerLocales) {
  179. let followerLocale = followerLocales[followerName]
  180. let missing = leadingLocale.filter((key) => !followerLocale.includes(key))
  181. let excess = followerLocale.filter((key) => !leadingLocale.includes(key))
  182. missing.forEach((key) => {
  183. // Items missing are a non-fatal warning because we don't want CI to bum out over all languages
  184. // as soon as we add some English
  185. warnings.push(`${chalk.cyan(followerName)} locale has missing string: '${chalk.red(key)}' that is present in ${chalk.cyan(leadingLocaleName)} with value: ${chalk.yellow(leadingValues[key])}`)
  186. })
  187. excess.forEach((key) => {
  188. // Items in excess are a fatal because we should clean up follower languages once we remove English strings
  189. fatals.push(`${chalk.cyan(followerName)} locale has excess string: '${chalk.yellow(key)}' that is not present in ${chalk.cyan(leadingLocaleName)}. `)
  190. })
  191. }
  192. if (warnings.length) {
  193. console.error(`--> Locale warnings: `)
  194. console.error(warnings.join('\n'))
  195. console.error(``)
  196. }
  197. if (fatals.length) {
  198. console.error(`--> Locale fatal warnings: `)
  199. console.error(fatals.join('\n'))
  200. console.error(``)
  201. process.exit(1)
  202. }
  203. if (!warnings.length && !fatals.length) {
  204. console.log(`--> All locale strings have matching keys ${chalk.green(': )')}`)
  205. console.log(``)
  206. }
  207. }