Browse Source

core: setOptions for Core and plugins (#1728)

* Add a method to update options in Core

* Add a method to update options in Plugin (for any plugins)

* Allow re-initializing i18n locales after they’ve been updated with .setOptions

* use rest spread instead of Object.assign

* override setOptions in plugins to include i18nInit

* merge restrictions object in setOptions

* check that newOpts exists

* add spread ...

* don’t double merge

* add i18nInit to all plugins that use translation strings

* add setOptions tests to Core and Dashboard

* add setOptions docs for Core and Plugins

* fix tests for thumbnail-generator by adding plugins: {} to mock core

cause ThumbnailGenerator now calls this.setPluginState, which expects `core.state.plugins`

* also update meta with setOptions if it’s passed, change the way this.opts is set in core

@goto-bus-stop does this look ok? merging restrictions opts in core

* if locale was passed to setOptions(), call plugin.setOptions() on all plugins, so that i18n updates

* add Dashboard test that checks if locale is updated from Core via setOptions()

* Reafactor website Dashboard example to use setOptions and allow selecting a locale

:tada:
Artur Paikin 5 years ago
parent
commit
4e54483e61

+ 1 - 0
.gitignore

@@ -26,6 +26,7 @@ website/src/_drafts
 website/themes/uppy/source/uppy/
 website/themes/uppy/source/uppy/
 website/themes/uppy/_config.yml
 website/themes/uppy/_config.yml
 website/src/_template/list_of_locale_packs.md
 website/src/_template/list_of_locale_packs.md
+website/src/examples/locale_list.json
 website/themes/uppy/layout/partials/generated_stargazers.ejs
 website/themes/uppy/layout/partials/generated_stargazers.ejs
 
 
 **/output/*
 **/output/*

+ 12 - 6
packages/@uppy/aws-s3/src/index.js

@@ -55,18 +55,24 @@ module.exports = class AwsS3 extends Plugin {
 
 
     this.opts = { ...defaultOptions, ...opts }
     this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this.client = new RequestClient(uppy, opts)
     this.client = new RequestClient(uppy, opts)
-
     this.prepareUpload = this.prepareUpload.bind(this)
     this.prepareUpload = this.prepareUpload.bind(this)
-
     this.requests = new RateLimitedQueue(this.opts.limit)
     this.requests = new RateLimitedQueue(this.opts.limit)
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   getUploadParameters (file) {
   getUploadParameters (file) {
     if (!this.opts.companionUrl) {
     if (!this.opts.companionUrl) {
       throw new Error('Expected a `companionUrl` option containing a Companion address.')
       throw new Error('Expected a `companionUrl` option containing a Companion address.')

+ 5 - 0
packages/@uppy/core/src/Plugin.js

@@ -62,6 +62,11 @@ module.exports = class Plugin {
     })
     })
   }
   }
 
 
+  setOptions (newOpts) {
+    this.opts = { ...this.opts, ...newOpts }
+    this.setPluginState() // so that UI re-renders with new options
+  }
+
   update (state) {
   update (state) {
     if (typeof this.el === 'undefined') {
     if (typeof this.el === 'undefined') {
       return
       return

+ 43 - 9
packages/@uppy/core/src/index.js

@@ -81,7 +81,6 @@ class Uppy {
       }
       }
     }
     }
 
 
-    // set default options
     const defaultOptions = {
     const defaultOptions = {
       id: 'uppy',
       id: 'uppy',
       autoProceed: false,
       autoProceed: false,
@@ -100,9 +99,16 @@ class Uppy {
       logger: nullLogger
       logger: nullLogger
     }
     }
 
 
-    // Merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOptions, opts)
-    this.opts.restrictions = Object.assign({}, defaultOptions.restrictions, this.opts.restrictions)
+    // Merge default options with the ones set by user,
+    // making sure to merge restrictions too
+    this.opts = {
+      ...defaultOptions,
+      ...opts,
+      restrictions: {
+        ...defaultOptions.restrictions,
+        ...(opts && opts.restrictions)
+      }
+    }
 
 
     // Support debug: true for backwards-compatability, unless logger is set in opts
     // Support debug: true for backwards-compatability, unless logger is set in opts
     // opts instead of this.opts to avoid comparing objects — we set logger: nullLogger in defaultOptions
     // opts instead of this.opts to avoid comparing objects — we set logger: nullLogger in defaultOptions
@@ -120,11 +126,7 @@ class Uppy {
       throw new TypeError('`restrictions.allowedFileTypes` must be an array')
       throw new TypeError('`restrictions.allowedFileTypes` must be an array')
     }
     }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.opts.locale])
-    this.locale = this.translator.locale
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     // Container for different types of plugins
     // Container for different types of plugins
     this.plugins = {}
     this.plugins = {}
@@ -261,6 +263,38 @@ class Uppy {
     })
     })
   }
   }
 
 
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.opts.locale])
+    this.locale = this.translator.locale
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+  }
+
+  setOptions (newOpts) {
+    this.opts = {
+      ...this.opts,
+      ...newOpts,
+      restrictions: {
+        ...this.opts.restrictions,
+        ...(newOpts && newOpts.restrictions)
+      }
+    }
+
+    if (newOpts.meta) {
+      this.setMeta(newOpts.meta)
+    }
+
+    this.i18nInit()
+
+    if (newOpts.locale) {
+      this.iteratePlugins((plugin) => {
+        plugin.setOptions()
+      })
+    }
+
+    this.setState() // so that UI re-renders with new options
+  }
+
   resetProgress () {
   resetProgress () {
     const defaultProgress = {
     const defaultProgress = {
       percentage: 0,
       percentage: 0,

+ 92 - 0
packages/@uppy/core/src/index.test.js

@@ -1028,6 +1028,98 @@ describe('src/Core', () => {
     })
     })
   })
   })
 
 
+  describe('setOptions', () => {
+    it('should change options on the fly', () => {
+      const core = new Core()
+      core.setOptions({
+        id: 'lolUppy',
+        autoProceed: true,
+        allowMultipleUploads: true
+      })
+
+      expect(core.opts.id).toEqual('lolUppy')
+      expect(core.opts.autoProceed).toEqual(true)
+      expect(core.opts.allowMultipleUploads).toEqual(true)
+    })
+
+    it('should change locale on the fly', () => {
+      const core = new Core()
+      expect(core.i18n('cancel')).toEqual('Cancel')
+
+      core.setOptions({
+        locale: {
+          strings: {
+            cancel: 'Отмена'
+          }
+        }
+      })
+
+      expect(core.i18n('cancel')).toEqual('Отмена')
+      expect(core.i18n('logOut')).toEqual('Log out')
+    })
+
+    it('should change meta on the fly', () => {
+      const core = new Core({
+        meta: {
+          foo: 'bar'
+        }
+      })
+      expect(core.state.meta).toMatchObject({
+        foo: 'bar'
+      })
+
+      core.setOptions({
+        meta: {
+          beep: 'boop'
+        }
+      })
+
+      expect(core.state.meta).toMatchObject({
+        foo: 'bar',
+        beep: 'boop'
+      })
+    })
+
+    it('should change restrictions on the fly', () => {
+      const core = new Core({
+        restrictions: {
+          allowedFileTypes: ['image/jpeg'],
+          maxNumberOfFiles: 2
+        }
+      })
+
+      try {
+        core.addFile({
+          source: 'jest',
+          name: 'foo1.png',
+          type: 'image/png',
+          data: new File([sampleImage], { type: 'image/png' })
+        })
+      } catch (err) {
+        expect(err).toMatchObject(new Error('You can only upload: image/jpeg'))
+      }
+
+      core.setOptions({
+        restrictions: {
+          allowedFileTypes: ['image/png']
+        }
+      })
+
+      expect(core.opts.restrictions.allowedFileTypes).toMatchObject(['image/png'])
+
+      expect(() => {
+        core.addFile({
+          source: 'jest',
+          name: 'foo1.png',
+          type: 'image/png',
+          data: new File([sampleImage], { type: 'image/png' })
+        })
+      }).not.toThrow()
+
+      expect(core.getFiles().length).toEqual(1)
+    })
+  })
+
   describe('meta data', () => {
   describe('meta data', () => {
     it('should set meta data by calling setMeta', () => {
     it('should set meta data by calling setMeta', () => {
       const core = new Core({
       const core = new Core({

+ 14 - 5
packages/@uppy/dashboard/src/index.js

@@ -126,10 +126,7 @@ module.exports = class Dashboard extends Plugin {
     // merge default options with the ones set by user
     // merge default options with the ones set by user
     this.opts = { ...defaultOptions, ...opts }
     this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this.openModal = this.openModal.bind(this)
     this.openModal = this.openModal.bind(this)
     this.closeModal = this.closeModal.bind(this)
     this.closeModal = this.closeModal.bind(this)
@@ -170,6 +167,18 @@ module.exports = class Dashboard extends Plugin {
     this.removeDragOverClassTimeout = null
     this.removeDragOverClassTimeout = null
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   removeTarget (plugin) {
   removeTarget (plugin) {
     const pluginState = this.getPluginState()
     const pluginState = this.getPluginState()
     // filter out the one we want to remove
     // filter out the one we want to remove
@@ -189,7 +198,7 @@ module.exports = class Dashboard extends Plugin {
         callerPluginType !== 'progressindicator' &&
         callerPluginType !== 'progressindicator' &&
         callerPluginType !== 'presenter') {
         callerPluginType !== 'presenter') {
       const msg = 'Dashboard: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
       const msg = 'Dashboard: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
-      this.uppy.log(msg)
+      this.uppy.log(msg, 'error')
       return
       return
     }
     }
 
 

+ 36 - 0
packages/@uppy/dashboard/src/index.test.js

@@ -55,4 +55,40 @@ describe('Dashboard', () => {
 
 
     core.close()
     core.close()
   })
   })
+
+  it('should change options on the fly', () => {
+    const core = new Core()
+    core.use(DashboardPlugin, {
+      inline: true,
+      target: 'body'
+    })
+
+    core.getPlugin('Dashboard').setOptions({
+      width: 300
+    })
+
+    expect(
+      core.getPlugin('Dashboard').opts.width
+    ).toEqual(300)
+  })
+
+  it('should use updated locale from Core, when it’s set via Core’s setOptions()', () => {
+    const core = new Core()
+    core.use(DashboardPlugin, {
+      inline: true,
+      target: 'body'
+    })
+
+    core.setOptions({
+      locale: {
+        strings: {
+          myDevice: 'Май дивайс'
+        }
+      }
+    })
+
+    expect(
+      core.getPlugin('Dashboard').i18n('myDevice')
+    ).toEqual('Май дивайс')
+  })
 })
 })

+ 14 - 5
packages/@uppy/drag-drop/src/index.js

@@ -35,16 +35,13 @@ module.exports = class DragDrop extends Plugin {
     }
     }
 
 
     // Merge default options with the ones set by user
     // Merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOpts, opts)
+    this.opts = { ...defaultOpts, ...opts }
 
 
     // Check for browser dragDrop support
     // Check for browser dragDrop support
     this.isDragDropSupported = isDragDropSupported()
     this.isDragDropSupported = isDragDropSupported()
     this.removeDragOverClassTimeout = null
     this.removeDragOverClassTimeout = null
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     // Bind `this` to class methods
     // Bind `this` to class methods
     this.handleInputChange = this.handleInputChange.bind(this)
     this.handleInputChange = this.handleInputChange.bind(this)
@@ -55,6 +52,18 @@ module.exports = class DragDrop extends Plugin {
     this.render = this.render.bind(this)
     this.render = this.render.bind(this)
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   addFile (file) {
   addFile (file) {
     try {
     try {
       this.uppy.addFile({
       this.uppy.addFile({

+ 14 - 5
packages/@uppy/file-input/src/index.js

@@ -29,18 +29,27 @@ module.exports = class FileInput extends Plugin {
     }
     }
 
 
     // Merge default options with the ones set by user
     // Merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOptions, opts)
+    this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this.render = this.render.bind(this)
     this.render = this.render.bind(this)
     this.handleInputChange = this.handleInputChange.bind(this)
     this.handleInputChange = this.handleInputChange.bind(this)
     this.handleClick = this.handleClick.bind(this)
     this.handleClick = this.handleClick.bind(this)
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   handleInputChange (event) {
   handleInputChange (event) {
     this.uppy.log('[FileInput] Something selected through input...')
     this.uppy.log('[FileInput] Something selected through input...')
 
 

+ 13 - 4
packages/@uppy/status-bar/src/index.js

@@ -65,16 +65,25 @@ module.exports = class StatusBar extends Plugin {
       hideAfterFinish: true
       hideAfterFinish: true
     }
     }
 
 
-    // merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOptions, opts)
+    this.opts = { ...defaultOptions, ...opts }
 
 
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nInit()
 
 
     this.render = this.render.bind(this)
     this.render = this.render.bind(this)
     this.install = this.install.bind(this)
     this.install = this.install.bind(this)
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   getTotalSpeed (files) {
   getTotalSpeed (files) {
     let totalSpeed = 0
     let totalSpeed = 0
     files.forEach((file) => {
     files.forEach((file) => {

+ 11 - 4
packages/@uppy/thumbnail-generator/src/index.js

@@ -34,13 +34,20 @@ module.exports = class ThumbnailGenerator extends Plugin {
       waitForThumbnailsBeforeUpload: false
       waitForThumbnailsBeforeUpload: false
     }
     }
 
 
-    this.opts = {
-      ...defaultOptions,
-      ...opts
-    }
+    this.opts = { ...defaultOptions, ...opts }
+
+    this.i18nInit()
+  }
+
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
 
 
+  i18nInit () {
     this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
     this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
     this.i18n = this.translator.translate.bind(this.translator)
     this.i18n = this.translator.translate.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
   }
   }
 
 
   /**
   /**

+ 16 - 3
packages/@uppy/thumbnail-generator/src/index.test.js

@@ -7,9 +7,15 @@ const delay = duration => new Promise(resolve => setTimeout(resolve, duration))
 function MockCore () {
 function MockCore () {
   const core = emitter()
   const core = emitter()
   const files = {}
   const files = {}
+  core.state = {
+    files,
+    plugins: {}
+  }
   core.mockFile = (id, f) => { files[id] = f }
   core.mockFile = (id, f) => { files[id] = f }
   core.getFile = (id) => files[id]
   core.getFile = (id) => files[id]
   core.log = () => null
   core.log = () => null
+  core.getState = () => core.state
+  core.setState = () => null
   return core
   return core
 }
 }
 
 
@@ -276,8 +282,15 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
             }
             }
           }
           }
         },
         },
-        setFileState: jest.fn()
+        setFileState: jest.fn(),
+        plugins: {}
       }
       }
+      core.state = {
+        plugins: {}
+      }
+      core.setState = () => null
+      core.getState = () => core.state
+
       const plugin = new ThumbnailGeneratorPlugin(core)
       const plugin = new ThumbnailGeneratorPlugin(core)
       plugin.setPreviewURL('file1', 'moo')
       plugin.setPreviewURL('file1', 'moo')
       expect(core.setFileState).toHaveBeenCalledTimes(1)
       expect(core.setFileState).toHaveBeenCalledTimes(1)
@@ -467,7 +480,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       }
       }
       const core = Object.assign(new MockCore(), {
       const core = Object.assign(new MockCore(), {
         getState () {
         getState () {
-          return { files }
+          return { files, plugins: {} }
         },
         },
         getFile (id) {
         getFile (id) {
           return files[id]
           return files[id]
@@ -491,7 +504,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       }
       }
       const core = Object.assign(new MockCore(), {
       const core = Object.assign(new MockCore(), {
         getState () {
         getState () {
-          return { files }
+          return { files, plugins: {} }
         },
         },
         getFile (id) {
         getFile (id) {
           return files[id]
           return files[id]

+ 14 - 8
packages/@uppy/transloadit/src/index.js

@@ -55,15 +55,9 @@ module.exports = class Transloadit extends Plugin {
       limit: 0
       limit: 0
     }
     }
 
 
-    this.opts = {
-      ...defaultOptions,
-      ...opts
-    }
+    this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this._prepareUpload = this._prepareUpload.bind(this)
     this._prepareUpload = this._prepareUpload.bind(this)
     this._afterUpload = this._afterUpload.bind(this)
     this._afterUpload = this._afterUpload.bind(this)
@@ -92,6 +86,18 @@ module.exports = class Transloadit extends Plugin {
     this.activeAssemblies = {}
     this.activeAssemblies = {}
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   _getClientVersion () {
   _getClientVersion () {
     const list = [
     const list = [
       `uppy-core:${this.uppy.constructor.VERSION}`,
       `uppy-core:${this.uppy.constructor.VERSION}`,

+ 14 - 5
packages/@uppy/url/src/index.js

@@ -39,12 +39,9 @@ module.exports = class Url extends Plugin {
 
 
     const defaultOptions = {}
     const defaultOptions = {}
 
 
-    this.opts = Object.assign({}, defaultOptions, opts)
+    this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this.hostname = this.opts.companionUrl
     this.hostname = this.opts.companionUrl
 
 
@@ -64,6 +61,18 @@ module.exports = class Url extends Plugin {
     })
     })
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   getFileNameFromUrl (url) {
   getFileNameFromUrl (url) {
     return url.substring(url.lastIndexOf('/') + 1)
     return url.substring(url.lastIndexOf('/') + 1)
   }
   }

+ 14 - 6
packages/@uppy/webcam/src/index.js

@@ -73,13 +73,9 @@ module.exports = class Webcam extends Plugin {
       preferredVideoMimeType: null
       preferredVideoMimeType: null
     }
     }
 
 
-    // merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOptions, opts)
+    this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this.install = this.install.bind(this)
     this.install = this.install.bind(this)
     this.setPluginState = this.setPluginState.bind(this)
     this.setPluginState = this.setPluginState.bind(this)
@@ -102,6 +98,18 @@ module.exports = class Webcam extends Plugin {
     }
     }
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   isSupported () {
   isSupported () {
     return !!this.mediaDevices
     return !!this.mediaDevices
   }
   }

+ 13 - 6
packages/@uppy/xhr-upload/src/index.js

@@ -102,13 +102,9 @@ module.exports = class XHRUpload extends Plugin {
       }
       }
     }
     }
 
 
-    // Merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOptions, opts)
+    this.opts = { ...defaultOptions, ...opts }
 
 
-    // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
 
     this.handleUpload = this.handleUpload.bind(this)
     this.handleUpload = this.handleUpload.bind(this)
 
 
@@ -127,6 +123,17 @@ module.exports = class XHRUpload extends Plugin {
     this.uploaderEvents = Object.create(null)
     this.uploaderEvents = Object.create(null)
   }
   }
 
 
+  setOptions (newOpts) {
+    super.setOptions(newOpts)
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = this.translator.translate.bind(this.translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
+  }
+
   getOptions (file) {
   getOptions (file) {
     const overrides = this.uppy.getState().xhrUpload
     const overrides = this.uppy.getState().xhrUpload
     const opts = {
     const opts = {

+ 6 - 0
website/inject.js

@@ -185,6 +185,7 @@ function injectLocaleList () {
     '| --------------- | ------------------ | ------------------- | ---------------- |'
     '| --------------- | ------------------ | ------------------- | ---------------- |'
   ]
   ]
   const mdRows = []
   const mdRows = []
+  const localeList = {}
 
 
   const localePackagePath = path.join(localesRoot, 'src', '*.js')
   const localePackagePath = path.join(localesRoot, 'src', '*.js')
   const localePackageVersion = require(path.join(localesRoot, 'package.json')).version
   const localePackageVersion = require(path.join(localesRoot, 'package.json')).version
@@ -209,13 +210,18 @@ function injectLocaleList () {
     const githubSource = `[\`${localeName}.js\`](https://github.com/transloadit/uppy/blob/master/packages/%40uppy/locales/src/${localeName}.js)`
     const githubSource = `[\`${localeName}.js\`](https://github.com/transloadit/uppy/blob/master/packages/%40uppy/locales/src/${localeName}.js)`
     const mdTableRow = `| ${languageName}<br/> <small>${countryName}</small>${variant ? `<br /><small>(${variant})</small>` : ''} | ${npmPath} | ${cdnPath} | ✏️ ${githubSource} |`
     const mdTableRow = `| ${languageName}<br/> <small>${countryName}</small>${variant ? `<br /><small>(${variant})</small>` : ''} | ${npmPath} | ${cdnPath} | ✏️ ${githubSource} |`
     mdRows.push(mdTableRow)
     mdRows.push(mdTableRow)
+
+    localeList[localeName] = `${languageName} (${countryName}${variant ? ` ${variant}` : ''})`
   })
   })
 
 
   const resultingMdTable = mdTable.concat(mdRows.sort()).join('\n').replace('%count%', mdRows.length)
   const resultingMdTable = mdTable.concat(mdRows.sort()).join('\n').replace('%count%', mdRows.length)
 
 
   const dstpath = path.join(webRoot, 'src', '_template', 'list_of_locale_packs.md')
   const dstpath = path.join(webRoot, 'src', '_template', 'list_of_locale_packs.md')
+  const localeListDstPath = path.join(webRoot, 'src', 'examples', 'locale_list.json')
   fs.writeFileSync(dstpath, resultingMdTable, 'utf-8')
   fs.writeFileSync(dstpath, resultingMdTable, 'utf-8')
   console.info(chalk.green('✓ injected: '), chalk.grey(dstpath))
   console.info(chalk.green('✓ injected: '), chalk.grey(dstpath))
+  fs.writeFileSync(localeListDstPath, JSON.stringify(localeList), 'utf-8')
+  console.info(chalk.green('✓ injected: '), chalk.grey(localeListDstPath))
 }
 }
 
 
 async function readConfig () {
 async function readConfig () {

+ 16 - 0
website/src/docs/plugin-common-options.md

@@ -54,6 +54,22 @@ Same as with Uppy.Core’s setting above, this allows you to override plugin’s
 
 
 See plugin documentation pages for other plugin-specific options.
 See plugin documentation pages for other plugin-specific options.
 
 
+## Methods
+
+### setOptions(opts)
+
+You can change options for a plugin on the fly, like this:
+
+```js
+// First get the plugin by its `id`,
+// then change, for example, `width` on the fly
+uppy.getPlugin('Dashboard').setOptions({
+  width: 300
+})
+```
+
+> ⚠️ This should work for most options, except for `limit` and some others related to an upload. This is because some objects/instances are created immediately upon initialization, and not updated later.
+
 <!-- Keep this heading, it is here to avoid breaking existing URLs -->
 <!-- Keep this heading, it is here to avoid breaking existing URLs -->
 <!-- Previously the content that is now at /docs/providers was here -->
 <!-- Previously the content that is now at /docs/providers was here -->
 ## Provider Plugins
 ## Provider Plugins

+ 29 - 0
website/src/docs/uppy.md

@@ -516,6 +516,35 @@ Update metadata for a specific file.
 uppy.setFileMeta('myfileID', { resize: 1500 })
 uppy.setFileMeta('myfileID', { resize: 1500 })
 ```
 ```
 
 
+### `uppy.setOptions(opts)`
+
+Change Uppy options on the fly. For example, to conditionally change `allowedFileTypes` or `locale`:
+
+```js
+const uppy = Uppy()
+uppy.setOptions({
+  restrictions: { maxNumberOfFiles: 3 },
+  autoProceed: true
+})
+
+uppy.setOptions({
+  locale: {
+    strings: {
+      'cancel': 'Отмена'
+    }
+  }
+})
+```
+
+You can also change options for plugin on the fly, like this:
+
+```js
+// Change width of the Dashboard drag-and-drop aread on the fly
+uppy.getPlugin('Dashboard').setOptions({
+  width: 300
+})
+```
+
 ### `uppy.reset()`
 ### `uppy.reset()`
 
 
 Stop all uploads in progress and clear file selection, set progress to 0. Basically, return things to the way they were before any user input.
 Stop all uploads in progress and clear file selection, set progress to 0. Basically, return things to the way they were before any user input.

+ 118 - 35
website/src/examples/dashboard/app.es6

@@ -8,78 +8,161 @@ const Instagram = require('@uppy/instagram')
 const Url = require('@uppy/url')
 const Url = require('@uppy/url')
 const Webcam = require('@uppy/webcam')
 const Webcam = require('@uppy/webcam')
 const Tus = require('@uppy/tus')
 const Tus = require('@uppy/tus')
+const localeList = require('../locale_list.json')
 
 
 const COMPANION = require('../env')
 const COMPANION = require('../env')
 
 
+if (typeof window !== 'undefined' && typeof window.Uppy === 'undefined') {
+  window.Uppy = {
+    locales: {}
+  }
+}
+
 function uppyInit () {
 function uppyInit () {
   if (window.uppy) {
   if (window.uppy) {
     window.uppy.close()
     window.uppy.close()
   }
   }
 
 
   const opts = window.uppyOptions
   const opts = window.uppyOptions
-  const dashboardEl = document.querySelector('.UppyDashboard')
-  if (dashboardEl) {
-    const dashboardElParent = dashboardEl.parentNode
-    dashboardElParent.removeChild(dashboardEl)
-  }
-
-  const restrictions = {
-    maxFileSize: 1000000,
-    maxNumberOfFiles: 3,
-    minNumberOfFiles: 2,
-    allowedFileTypes: ['image/*', 'video/*']
-  }
 
 
   const uppy = Uppy({
   const uppy = Uppy({
-    debug: true,
-    autoProceed: opts.autoProceed,
-    restrictions: opts.restrictions ? restrictions : ''
+    logger: Uppy.debugLogger
+  })
+
+  uppy.use(Tus, { endpoint: 'https://master.tus.io/files/', resume: true })
+
+  uppy.on('complete', result => {
+    console.log('successful files:')
+    console.log(result.successful)
+    console.log('failed files:')
+    console.log(result.failed)
   })
   })
 
 
   uppy.use(Dashboard, {
   uppy.use(Dashboard, {
     trigger: '.UppyModalOpenerBtn',
     trigger: '.UppyModalOpenerBtn',
-    inline: opts.DashboardInline,
     target: opts.DashboardInline ? '.DashboardContainer' : 'body',
     target: opts.DashboardInline ? '.DashboardContainer' : 'body',
+    inline: opts.DashboardInline,
     replaceTargetContent: opts.DashboardInline,
     replaceTargetContent: opts.DashboardInline,
-    note: opts.restrictions ? 'Images and video only, 2–3 files, up to 1 MB' : '',
     height: 470,
     height: 470,
     showProgressDetails: true,
     showProgressDetails: true,
     metaFields: [
     metaFields: [
       { id: 'name', name: 'Name', placeholder: 'file name' },
       { id: 'name', name: 'Name', placeholder: 'file name' },
       { id: 'caption', name: 'Caption', placeholder: 'add description' }
       { id: 'caption', name: 'Caption', placeholder: 'add description' }
-    ],
+    ]
+  })
+
+  window.uppy = uppy
+}
+
+function uppySetOptions () {
+  const opts = window.uppyOptions
+
+  const restrictions = {
+    maxFileSize: 1000000,
+    maxNumberOfFiles: 3,
+    minNumberOfFiles: 2,
+    allowedFileTypes: ['image/*', 'video/*']
+  }
+
+  window.uppy.setOptions({
+    autoProceed: opts.autoProceed,
+    restrictions: opts.restrictions ? restrictions : ''
+  })
+
+  window.uppy.getPlugin('Dashboard').setOptions({
+    note: opts.restrictions ? 'Images and video only, 2–3 files, up to 1 MB' : '',
     browserBackButtonClose: opts.browserBackButtonClose
     browserBackButtonClose: opts.browserBackButtonClose
   })
   })
 
 
-  if (opts.GoogleDrive) {
-    uppy.use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION })
+  const GoogleDriveInstance = window.uppy.getPlugin('GoogleDrive')
+  if (opts.GoogleDrive && !GoogleDriveInstance) {
+    window.uppy.use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION })
+  }
+  if (!opts.GoogleDrive && GoogleDriveInstance) {
+    window.uppy.removePlugin(GoogleDriveInstance)
   }
   }
 
 
-  if (opts.Dropbox) {
-    uppy.use(Dropbox, { target: Dashboard, companionUrl: COMPANION })
+  const DropboxInstance = window.uppy.getPlugin('Dropbox')
+  if (opts.Dropbox && !DropboxInstance) {
+    window.uppy.use(Dropbox, { target: Dashboard, companionUrl: COMPANION })
+  }
+  if (!opts.Dropbox && DropboxInstance) {
+    window.uppy.removePlugin(DropboxInstance)
   }
   }
 
 
-  if (opts.Instagram) {
-    uppy.use(Instagram, { target: Dashboard, companionUrl: COMPANION })
+  const InstagramInstance = window.uppy.getPlugin('Instagram')
+  if (opts.Instagram && !InstagramInstance) {
+    window.uppy.use(Instagram, { target: Dashboard, companionUrl: COMPANION })
+  }
+  if (!opts.Instagram && InstagramInstance) {
+    window.uppy.removePlugin(InstagramInstance)
   }
   }
 
 
-  if (opts.Url) {
-    uppy.use(Url, { target: Dashboard, companionUrl: COMPANION })
+  const UrlInstance = window.uppy.getPlugin('Url')
+  if (opts.Url && !UrlInstance) {
+    window.uppy.use(Url, { target: Dashboard, companionUrl: COMPANION })
+  }
+  if (!opts.Url && UrlInstance) {
+    window.uppy.removePlugin(UrlInstance)
   }
   }
 
 
-  if (opts.Webcam) {
-    uppy.use(Webcam, { target: Dashboard })
+  const WebcamInstance = window.uppy.getPlugin('Webcam')
+  if (opts.Webcam && !WebcamInstance) {
+    window.uppy.use(Webcam, { target: Dashboard, companionUrl: COMPANION })
+  }
+  if (!opts.Webcam && WebcamInstance) {
+    window.uppy.removePlugin(WebcamInstance)
   }
   }
+}
 
 
-  uppy.use(Tus, { endpoint: 'https://master.tus.io/files/', resume: true })
+function whenLocaleAvailable (localeName, callback) {
+  var interval = 100 // ms
+  var loop = setInterval(function () {
+    if (window.Uppy && window.Uppy.locales && window.Uppy.locales[localeName]) {
+      clearInterval(loop)
+      callback(window.Uppy.locales[localeName])
+    }
+  }, interval)
+}
 
 
-  uppy.on('complete', result => {
-    console.log('successful files:')
-    console.log(result.successful)
-    console.log('failed files:')
-    console.log(result.failed)
+function loadLocaleFromCDN (localeName) {
+  var head = document.getElementsByTagName('head')[0]
+  var js = document.createElement('script')
+  js.type = 'text/javascript'
+  js.src = `https://transloadit.edgly.net/releases/uppy/locales/v1.8.0/${localeName}.min.js`
+
+  head.appendChild(js)
+}
+
+function setLocale (localeName) {
+  if (typeof window.Uppy.locales[localeName] === 'undefined') {
+    loadLocaleFromCDN(localeName)
+  }
+  whenLocaleAvailable(localeName, (localeObj) => {
+    window.uppy.setOptions({
+      locale: localeObj
+    })
   })
   })
 }
 }
 
 
-uppyInit()
+function populateLocaleSelect () {
+  const localeSelect = document.getElementById('localeList')
+
+  Object.keys(localeList).forEach(localeName => {
+    if (localeName === 'en_US') return
+    localeSelect.innerHTML += `<option value="${localeName}">${localeList[localeName]} — (${localeName})</option>`
+  })
+
+  localeSelect.addEventListener('change', (event) => {
+    const localeName = event.target.value
+    setLocale(localeName)
+  })
+}
+
+window.uppySetOptions = uppySetOptions
 window.uppyInit = uppyInit
 window.uppyInit = uppyInit
+window.uppySetLocale = setLocale
+
+populateLocaleSelect()
+uppyInit()
+uppySetOptions()

+ 9 - 1
website/src/examples/dashboard/app.html

@@ -15,6 +15,11 @@
     <li><label for="opts-Instagram"><input type="checkbox" id="opts-Instagram" checked/> Instagram</label></li>
     <li><label for="opts-Instagram"><input type="checkbox" id="opts-Instagram" checked/> Instagram</label></li>
     <li><label for="opts-Url"><input type="checkbox" id="opts-Url" checked/> Url</label></li>
     <li><label for="opts-Url"><input type="checkbox" id="opts-Url" checked/> Url</label></li>
   </ul>
   </ul>
+
+  <label for="localeList">Change locale:</label>
+  <select id="localeList" name="locale list">
+    <option value="en_US" selected>English (US) — en_US</option>
+  </select>
 </div>
 </div>
 
 
 <!-- Modal trigger -->
 <!-- Modal trigger -->
@@ -82,7 +87,10 @@
       localStorage.setItem('uppyOptions', JSON.stringify(window.uppyOptions))
       localStorage.setItem('uppyOptions', JSON.stringify(window.uppyOptions))
 
 
       toggleModalBtn()
       toggleModalBtn()
-      window.uppyInit()
+      if (item === 'DashboardInline') {
+        window.uppyInit()
+      }
+      window.uppySetOptions()
     })
     })
   })
   })