locale-packs.js 9.1 KB

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