浏览代码

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 年之前
父节点
当前提交
4e54483e61

+ 1 - 0
.gitignore

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

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

@@ -55,18 +55,24 @@ module.exports = class AwsS3 extends Plugin {
 
     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.prepareUpload = this.prepareUpload.bind(this)
-
     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) {
     if (!this.opts.companionUrl) {
       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) {
     if (typeof this.el === 'undefined') {
       return

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

@@ -81,7 +81,6 @@ class Uppy {
       }
     }
 
-    // set default options
     const defaultOptions = {
       id: 'uppy',
       autoProceed: false,
@@ -100,9 +99,16 @@ class Uppy {
       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
     // 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')
     }
 
-    // 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
     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 () {
     const defaultProgress = {
       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', () => {
     it('should set meta data by calling setMeta', () => {
       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
     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.closeModal = this.closeModal.bind(this)
@@ -170,6 +167,18 @@ module.exports = class Dashboard extends Plugin {
     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) {
     const pluginState = this.getPluginState()
     // filter out the one we want to remove
@@ -189,7 +198,7 @@ module.exports = class Dashboard extends Plugin {
         callerPluginType !== 'progressindicator' &&
         callerPluginType !== '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
     }
 

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

@@ -55,4 +55,40 @@ describe('Dashboard', () => {
 
     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
-    this.opts = Object.assign({}, defaultOpts, opts)
+    this.opts = { ...defaultOpts, ...opts }
 
     // Check for browser dragDrop support
     this.isDragDropSupported = isDragDropSupported()
     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
     this.handleInputChange = this.handleInputChange.bind(this)
@@ -55,6 +52,18 @@ module.exports = class DragDrop extends Plugin {
     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) {
     try {
       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
-    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.handleInputChange = this.handleInputChange.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) {
     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
     }
 
-    // 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.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) {
     let totalSpeed = 0
     files.forEach((file) => {

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

@@ -34,13 +34,20 @@ module.exports = class ThumbnailGenerator extends Plugin {
       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.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 () {
   const core = emitter()
   const files = {}
+  core.state = {
+    files,
+    plugins: {}
+  }
   core.mockFile = (id, f) => { files[id] = f }
   core.getFile = (id) => files[id]
   core.log = () => null
+  core.getState = () => core.state
+  core.setState = () => null
   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)
       plugin.setPreviewURL('file1', 'moo')
       expect(core.setFileState).toHaveBeenCalledTimes(1)
@@ -467,7 +480,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       }
       const core = Object.assign(new MockCore(), {
         getState () {
-          return { files }
+          return { files, plugins: {} }
         },
         getFile (id) {
           return files[id]
@@ -491,7 +504,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       }
       const core = Object.assign(new MockCore(), {
         getState () {
-          return { files }
+          return { files, plugins: {} }
         },
         getFile (id) {
           return files[id]

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

@@ -55,15 +55,9 @@ module.exports = class Transloadit extends Plugin {
       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._afterUpload = this._afterUpload.bind(this)
@@ -92,6 +86,18 @@ module.exports = class Transloadit extends Plugin {
     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 () {
     const list = [
       `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 = {}
 
-    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
 
@@ -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) {
     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
     }
 
-    // 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.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 () {
     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)
 
@@ -127,6 +123,17 @@ module.exports = class XHRUpload extends Plugin {
     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) {
     const overrides = this.uppy.getState().xhrUpload
     const opts = {

+ 6 - 0
website/inject.js

@@ -185,6 +185,7 @@ function injectLocaleList () {
     '| --------------- | ------------------ | ------------------- | ---------------- |'
   ]
   const mdRows = []
+  const localeList = {}
 
   const localePackagePath = path.join(localesRoot, 'src', '*.js')
   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 mdTableRow = `| ${languageName}<br/> <small>${countryName}</small>${variant ? `<br /><small>(${variant})</small>` : ''} | ${npmPath} | ${cdnPath} | ✏️ ${githubSource} |`
     mdRows.push(mdTableRow)
+
+    localeList[localeName] = `${languageName} (${countryName}${variant ? ` ${variant}` : ''})`
   })
 
   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 localeListDstPath = path.join(webRoot, 'src', 'examples', 'locale_list.json')
   fs.writeFileSync(dstpath, resultingMdTable, 'utf-8')
   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 () {

+ 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.
 
+## 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 -->
 <!-- Previously the content that is now at /docs/providers was here -->
 ## 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.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()`
 
 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 Webcam = require('@uppy/webcam')
 const Tus = require('@uppy/tus')
+const localeList = require('../locale_list.json')
 
 const COMPANION = require('../env')
 
+if (typeof window !== 'undefined' && typeof window.Uppy === 'undefined') {
+  window.Uppy = {
+    locales: {}
+  }
+}
+
 function uppyInit () {
   if (window.uppy) {
     window.uppy.close()
   }
 
   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({
-    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, {
     trigger: '.UppyModalOpenerBtn',
-    inline: opts.DashboardInline,
     target: opts.DashboardInline ? '.DashboardContainer' : 'body',
+    inline: opts.DashboardInline,
     replaceTargetContent: opts.DashboardInline,
-    note: opts.restrictions ? 'Images and video only, 2–3 files, up to 1 MB' : '',
     height: 470,
     showProgressDetails: true,
     metaFields: [
       { id: 'name', name: 'Name', placeholder: 'file name' },
       { 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
   })
 
-  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.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-Url"><input type="checkbox" id="opts-Url" checked/> Url</label></li>
   </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>
 
 <!-- Modal trigger -->
@@ -82,7 +87,10 @@
       localStorage.setItem('uppyOptions', JSON.stringify(window.uppyOptions))
 
       toggleModalBtn()
-      window.uppyInit()
+      if (item === 'DashboardInline') {
+        window.uppyInit()
+      }
+      window.uppySetOptions()
     })
   })