locale-packs.js 9.1 KB

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