locale-packs.js 9.6 KB

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