diff --git a/bigbluebutton-html5/.gitignore b/bigbluebutton-html5/.gitignore
index 3a833eaf4f800bfece09c1cd77ed8cd112714fc2..b8bd4a6520ec7750e5c0d810ffc70d79e9e3e98a 100755
--- a/bigbluebutton-html5/.gitignore
+++ b/bigbluebutton-html5/.gitignore
@@ -3,5 +3,7 @@ npm-debug.log
 node_modules/
 .meteor/dev_bundle
 tests/webdriverio/.testing-env
+public/locales/de_DE.json
+public/locales/ja_JP.json
 
 
diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx
index 0d44ca2d67522c69ce2c6bcefdf016f449523f8f..c56fac96baa696728478bc9e458028b0b98e5016 100644
--- a/bigbluebutton-html5/imports/startup/client/intl.jsx
+++ b/bigbluebutton-html5/imports/startup/client/intl.jsx
@@ -63,6 +63,7 @@ class IntlStartup extends Component {
 
   fetchLocalizedMessages(locale, init = false) {
     const url = `./locale?locale=${locale}&init=${init}`;
+    const localesPath = 'locales';
 
     this.setState({ fetching: true }, () => {
       fetch(url)
@@ -73,11 +74,65 @@ class IntlStartup extends Component {
 
           return response.json();
         })
-        .then(({ messages, normalizedLocale }) => {
-          const dasherizedLocale = normalizedLocale.replace('_', '-');
-          this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
-            IntlStartup.saveLocale(dasherizedLocale);
-          });
+        .then(({ normalizedLocale, regionDefaultLocale }) => {
+          fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
+            .then((response) => {
+              if (!response.ok) {
+                return Promise.reject();
+              }
+              return response.json();
+            })
+            .then((messages) => {
+              if (regionDefaultLocale !== '') {
+                fetch(`${localesPath}/${regionDefaultLocale}.json`)
+                  .then((response) => {
+                    if (!response.ok) {
+                      return Promise.resolve();
+                    }
+                    return response.json();
+                  })
+                  .then((regionDefaultMessages) => {
+                    messages = Object.assign(messages, regionDefaultMessages);
+                    return messages;
+                  });
+              }
+
+              if (normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) {
+                fetch(`${localesPath}/${normalizedLocale}.json`)
+                  .then((response) => {
+                    if (!response.ok) {
+                      return Promise.reject();
+                    }
+                    return response.json();
+                  })
+                  .then((localeMessages) => {
+                    messages = Object.assign(messages, localeMessages);
+                    return messages;
+                  })
+                  .catch(() => {
+                    normalizedLocale = (regionDefaultLocale) || DEFAULT_LANGUAGE;
+                    const dasherizedLocale = normalizedLocale.replace('_', '-');
+                    this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
+                      IntlStartup.saveLocale(normalizedLocale);
+                    });
+                  });
+              }
+
+              return messages;
+            })
+            .then((messages) => {
+              const dasherizedLocale = normalizedLocale.replace('_', '-');
+              this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
+                IntlStartup.saveLocale(dasherizedLocale);
+              });
+            })
+            .catch(() => {
+              normalizedLocale = DEFAULT_LANGUAGE;
+              const dasherizedLocale = normalizedLocale.replace('_', '-');
+              this.setState({ fetching: false, normalizedLocale: dasherizedLocale }, () => {
+                IntlStartup.saveLocale(normalizedLocale);
+              });
+            });
         })
         .catch(() => {
           this.setState({ fetching: false, normalizedLocale: null }, () => {
diff --git a/bigbluebutton-html5/imports/startup/server/index.js b/bigbluebutton-html5/imports/startup/server/index.js
index 7ad0f829dd88987b6f9723b24333698fd15aefb7..5f6508e6a32a1c6ac6d7cf89defff9dae9f2a45d 100755
--- a/bigbluebutton-html5/imports/startup/server/index.js
+++ b/bigbluebutton-html5/imports/startup/server/index.js
@@ -12,7 +12,16 @@ import Redis from './redis';
 import setMinBrowserVersions from './minBrowserVersion';
 
 let guestWaitHtml = '';
-const AVAILABLE_LOCALES = fs.readdirSync('assets/app/locales');
+
+const env = Meteor.isDevelopment ? 'development' : 'production';
+
+const meteorRoot = fs.realpathSync(`${process.cwd()}/../`);
+
+const applicationRoot = (env === 'development')
+  ? fs.realpathSync(`${meteorRoot}'/../../../../public/locales/`)
+  : fs.realpathSync(`${meteorRoot}/../programs/web.browser/app/locales/`);
+
+const AVAILABLE_LOCALES = fs.readdirSync(`${applicationRoot}`);
 const FALLBACK_LOCALES = JSON.parse(Assets.getText('config/fallbackLocales.json'));
 
 process.on('uncaughtException', (err) => {
@@ -27,7 +36,6 @@ process.on('uncaughtException', (err) => {
 
 Meteor.startup(() => {
   const APP_CONFIG = Meteor.settings.public.app;
-  const env = Meteor.isDevelopment ? 'development' : 'production';
   const CDN_URL = APP_CONFIG.cdn;
   const instanceId = parseInt(process.env.INSTANCE_ID, 10) || 1;
 
@@ -106,40 +114,39 @@ Meteor.startup(() => {
         session.bbbFixApplied = true;
       }
     }, 5000);
+  }
+  if (CDN_URL.trim()) {
+    // Add CDN
+    BrowserPolicy.content.disallowEval();
+    BrowserPolicy.content.allowInlineScripts();
+    BrowserPolicy.content.allowInlineStyles();
+    BrowserPolicy.content.allowImageDataUrl(CDN_URL);
+    BrowserPolicy.content.allowFontDataUrl(CDN_URL);
+    BrowserPolicy.content.allowOriginForAll(CDN_URL);
+    WebAppInternals.setBundledJsCssPrefix(CDN_URL + APP_CONFIG.basename + Meteor.settings.public.app.instanceId);
+
+    const fontRegExp = /\.(eot|ttf|otf|woff|woff2)$/;
+
+    WebApp.rawConnectHandlers.use('/', (req, res, next) => {
+      if (fontRegExp.test(req._parsedUrl.pathname)) {
+        res.setHeader('Access-Control-Allow-Origin', '*');
+        res.setHeader('Vary', 'Origin');
+        res.setHeader('Pragma', 'public');
+        res.setHeader('Cache-Control', '"public"');
+      }
+      return next();
+    });
+  }
 
-    if (CDN_URL.trim()) {
-      // Add CDN
-      BrowserPolicy.content.disallowEval();
-      BrowserPolicy.content.allowInlineScripts();
-      BrowserPolicy.content.allowInlineStyles();
-      BrowserPolicy.content.allowImageDataUrl(CDN_URL);
-      BrowserPolicy.content.allowFontDataUrl(CDN_URL);
-      BrowserPolicy.content.allowOriginForAll(CDN_URL);
-      WebAppInternals.setBundledJsCssPrefix(CDN_URL + APP_CONFIG.basename + Meteor.settings.public.app.instanceId);
-
-      const fontRegExp = /\.(eot|ttf|otf|woff|woff2)$/;
-
-      WebApp.rawConnectHandlers.use('/', (req, res, next) => {
-        if (fontRegExp.test(req._parsedUrl.pathname)) {
-          res.setHeader('Access-Control-Allow-Origin', '*');
-          res.setHeader('Vary', 'Origin');
-          res.setHeader('Pragma', 'public');
-          res.setHeader('Cache-Control', '"public"');
-        }
-        return next();
-      });
-    }
-
-    setMinBrowserVersions();
+  setMinBrowserVersions();
 
-    Logger.warn(`SERVER STARTED.
-    ENV=${env}
-    nodejs version=${process.version}
-    BBB_HTML5_ROLE=${process.env.BBB_HTML5_ROLE}
-    INSTANCE_ID=${instanceId}
-    PORT=${process.env.PORT}
-    CDN=${CDN_URL}\n`, APP_CONFIG);
-  }
+  Logger.warn(`SERVER STARTED.
+  ENV=${env}
+  nodejs version=${process.version}
+  BBB_HTML5_ROLE=${process.env.BBB_HTML5_ROLE}
+  INSTANCE_ID=${instanceId}
+  PORT=${process.env.PORT}
+  CDN=${CDN_URL}\n`, APP_CONFIG);
 });
 
 
@@ -192,7 +199,7 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
   const browserLocale = override && req.query.init === 'true'
     ? override.split(/[-_]/g) : req.query.locale.split(/[-_]/g);
 
-  const localeList = [fallback];
+  let localeFile = fallback;
 
   const usableLocales = AVAILABLE_LOCALES
     .map(file => file.replace('.json', ''))
@@ -200,35 +207,29 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
       ? [...locales, locale]
       : locales), []);
 
-  const regionDefault = usableLocales.find(locale => browserLocale[0] === locale);
-
-  if (regionDefault) localeList.push(regionDefault);
-  if (!regionDefault && usableLocales.length) localeList.push(usableLocales[0]);
-
   let normalizedLocale;
-  let messages = {};
 
   if (browserLocale.length > 1) {
     normalizedLocale = `${browserLocale[0]}_${browserLocale[1].toUpperCase()}`;
-    localeList.push(normalizedLocale);
+
+    const normDefault = usableLocales.find(locale => normalizedLocale === locale);
+    if (normDefault) localeFile = normDefault;
   }
 
-  localeList.forEach((locale) => {
-    try {
-      const data = Assets.getText(`locales/${locale}.json`);
-      messages = Object.assign(messages, JSON.parse(data));
-      normalizedLocale = locale;
-    } catch (e) {
-      Logger.info(`'Could not process locale ${locale}:${e}`);
-      // Getting here means the locale is not available in the current locale files.
-    }
-  });
+  const regionDefault = usableLocales.find(locale => browserLocale[0] === locale);
+
+  if (localeFile === fallback && regionDefault !== localeFile) {
+    localeFile = regionDefault;
+  }
 
   res.setHeader('Content-Type', 'application/json');
-  res.end(JSON.stringify({ normalizedLocale, messages }));
+  res.end(JSON.stringify({
+    normalizedLocale: localeFile,
+    regionDefaultLocale: (regionDefault && regionDefault !== localeFile) ? regionDefault : '',
+  }));
 });
 
-WebApp.connectHandlers.use('/locales', (req, res) => {
+WebApp.connectHandlers.use('/locale-list', (req, res) => {
   if (!avaibleLocalesNamesJSON) {
     avaibleLocalesNamesJSON = JSON.stringify(generateLocaleOptions());
   }
diff --git a/bigbluebutton-html5/imports/ui/components/legacy/component.jsx b/bigbluebutton-html5/imports/ui/components/legacy/component.jsx
index f6c049f3452dc5cc425822c770f4ba119e834c2d..b859270bd6d4c88bf16b98e5fec0126d5cf2b665 100755
--- a/bigbluebutton-html5/imports/ui/components/legacy/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/legacy/component.jsx
@@ -69,16 +69,17 @@ const FETCHING = 'fetching';
 const FALLBACK = 'fallback';
 const READY = 'ready';
 const supportedBrowsers = ['chrome', 'firefox', 'safari', 'opera', 'edge', 'yandex'];
+const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
 
 export default class Legacy extends Component {
   constructor(props) {
     super(props);
 
     const locale = navigator.languages ? navigator.languages[0] : false
-      || navigator.language
-      || Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
+      || navigator.language;
 
     const url = `./locale?locale=${locale}`;
+    const localesPath = 'locales';
 
     const that = this;
     this.state = { viewState: FETCHING };
@@ -90,9 +91,56 @@ export default class Legacy extends Component {
 
         return response.json();
       })
-      .then(({ messages, normalizedLocale }) => {
-        const dasherizedLocale = normalizedLocale.replace('_', '-');
-        that.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+      .then(({ normalizedLocale, regionDefaultLocale }) => {
+        fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
+          .then((response) => {
+            if (!response.ok) {
+              return Promise.reject();
+            }
+            return response.json();
+          })
+          .then((messages) => {
+            if (regionDefaultLocale !== '') {
+              fetch(`${localesPath}/${regionDefaultLocale}.json`)
+                .then((response) => {
+                  if (!response.ok) {
+                    return Promise.resolve();
+                  }
+                  return response.json();
+                })
+                .then((regionDefaultMessages) => {
+                  messages = Object.assign(messages, regionDefaultMessages);
+                  this.setState({ messages});
+                });
+            }
+
+            if (normalizedLocale && normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) {
+              fetch(`${localesPath}/${normalizedLocale}.json`)
+                .then((response) => {
+                  if (!response.ok) {
+                    return Promise.reject();
+                  }
+                  return response.json();
+                })
+                .then((localeMessages) => {
+                  messages = Object.assign(messages, localeMessages);
+                  this.setState({ messages});
+                })
+                .catch(() => {
+                  normalizedLocale = (regionDefaultLocale) || DEFAULT_LANGUAGE;
+                  const dasherizedLocale = normalizedLocale.replace('_', '-');
+                  this.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+                });
+            }
+            return messages;
+          })
+          .then((messages) => {
+            const dasherizedLocale = normalizedLocale.replace('_', '-');
+            this.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+          })
+          .catch(() => {
+            that.setState({ viewState: FALLBACK });
+          });
       })
       .catch(() => {
         that.setState({ viewState: FALLBACK });
diff --git a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
index a47575fcd678a49504151ef6deb05c5d0257c27b..4c9cf5a67b3915746a348d3e62737ccb0b79f445 100644
--- a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
@@ -489,6 +489,7 @@ class WebcamDraggable extends PureComponent {
             style={{
               marginLeft: 0,
               marginRight: 0,
+              zIndex: 2,
               display: hideWebcams ? 'none' : undefined,
             }}
           >
diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx
index 479238daf3a535469deafb983858a2435b24680a..5ff55f15e9d32531557f0cfc17b9684f67823e0d 100644
--- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx
@@ -9,6 +9,7 @@ import cx from 'classnames';
 import Button from '/imports/ui/components/button/component';
 import LiveResult from './live-result/component';
 import { styles } from './styles.scss';
+import DragAndDrop from './dragAndDrop/component';
 
 const intlMessages = defineMessages({
   pollPaneTitle: {
@@ -31,6 +32,10 @@ const intlMessages = defineMessages({
     id: 'app.poll.activePollInstruction',
     description: 'instructions displayed when a poll is active',
   },
+  dragDropPollInstruction: {
+    id: 'app.poll.dragDropPollInstruction',
+    description: 'instructions for upload poll options via drag and drop',
+  },
   ariaInputCount: {
     id: 'app.poll.ariaInputCount',
     description: 'aria label for custom poll input field',
@@ -148,6 +153,7 @@ const intlMessages = defineMessages({
 const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
 const MAX_CUSTOM_FIELDS = Meteor.settings.public.poll.max_custom;
 const MAX_INPUT_CHARS = 45;
+const FILE_DRAG_AND_DROP_ENABLED = Meteor.settings.public.poll.allowDragAndDropFile;
 
 const validateInput = (i) => {
   let _input = i;
@@ -206,6 +212,21 @@ class Poll extends Component {
     });
   }
 
+  handleInputChange(index, event) {
+    this.handleInputTextChange(index, event.target.value);
+  }
+
+  handleInputTextChange(index, text) {
+    const { optList } = this.state;
+    // This regex will replace any instance of 2 or more consecutive white spaces
+    // with a single white space character.
+    const option = text.replace(/\s{2,}/g, ' ').trim();
+
+    if (index < optList.length) optList[index].val = option === '' ? '' : option;
+
+    this.setState({ optList });
+  }
+
   handleInputChange(e, index) {
     const { optList, type, error } = this.state;
     const list = [...optList];
@@ -222,6 +243,24 @@ class Poll extends Component {
     this.setState({ question: validateInput(e.target.value), error: clearError ? null : error });
   }
 
+  pushToCustomPollValues(text) {
+    const lines = text.split('\n');
+    for (let i = 0; i < MAX_CUSTOM_FIELDS; i += 1) {
+      let line = '';
+      if (i < lines.length) {
+        line = lines[i];
+        line = line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line;
+      }
+      this.handleInputTextChange(i, line);
+    }
+  }
+
+  handlePollValuesText(text) {
+    if (text && text.length > 0) {
+      this.pushToCustomPollValues(text);
+    }
+  }
+
   handleRemoveOption(index) {
     const { optList } = this.state;
     const list = [...optList];
@@ -496,6 +535,9 @@ class Poll extends Component {
                           });
                         }}
                       />
+                      {
+                        FILE_DRAG_AND_DROP_ENABLED && this.renderDragDrop()
+                      }
                     </div>
                     )
               }
@@ -537,6 +579,25 @@ class Poll extends Component {
     return this.renderPollOptions();
   }
 
+
+  renderDragDrop() {
+    const { intl } = this.props;
+    return (
+      <div>
+        <div className={styles.instructions}>
+          {intl.formatMessage(intlMessages.dragDropPollInstruction)}
+        </div>
+        <DragAndDrop
+          {...{ intl, MAX_INPUT_CHARS }}
+          handlePollValuesText={e => this.handlePollValuesText(e)}
+        >
+          <div className={styles.dragAndDropPollContainer} />
+        </DragAndDrop>
+      </div>
+    );
+  }
+
+
   render() {
     const {
       intl,
diff --git a/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..b31318d63f85cf7ac179fb68bf256a5796513ca4
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx
@@ -0,0 +1,143 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+
+import { defineMessages, injectIntl } from 'react-intl';
+import { styles } from './styles.scss';
+import Button from '/imports/ui/components/button/component';
+
+
+// src: https://medium.com/@650egor/simple-drag-and-drop-file-upload-in-react-2cb409d88929
+
+const intlMessages = defineMessages({
+  customPollTextArea: {
+    id: 'app.poll.customPollTextArea',
+    description: 'label for button to submit custom poll values',
+  },
+});
+
+class DragAndDrop extends Component {
+  static handleDrag(e) {
+    e.preventDefault();
+    e.stopPropagation();
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      drag: false,
+      pollValueText: '',
+    };
+
+    this.dropRef = React.createRef();
+  }
+
+  componentDidMount() {
+    this.dragCounter = 0;
+    const div = this.dropRef.current;
+    div.addEventListener('dragenter', e => this.handleDragIn(e));
+    div.addEventListener('dragleave', e => this.handleDragOut(e));
+    div.addEventListener('dragover', e => DragAndDrop.handleDrag(e));
+    div.addEventListener('drop', e => this.handleDrop(e));
+  }
+
+  componentWillUnmount() {
+    const div = this.dropRef.current;
+    div.removeEventListener('dragenter', e => this.handleDragIn(e));
+    div.removeEventListener('dragleave', e => this.handleDragOut(e));
+    div.removeEventListener('dragover', e => DragAndDrop.handleDrag(e));
+    div.removeEventListener('drop', e => this.handleDrop(e));
+  }
+
+  setPollValues() {
+    const { pollValueText } = this.state;
+    const { handlePollValuesText } = this.props;
+    if (pollValueText) {
+      handlePollValuesText(pollValueText);
+    }
+  }
+
+  setPollValuesFromFile(file) {
+    const reader = new FileReader();
+    reader.onload = async (e) => {
+      const text = e.target.result;
+      this.setPollValueText(text);
+      this.setPollValues();
+    };
+    reader.readAsText(file);
+  }
+
+  setPollValueText(pollText) {
+    const { MAX_INPUT_CHARS } = this.props;
+    const arr = pollText.split('\n');
+    const text = arr.map(line => (line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line)).join('\n');
+    this.setState({ pollValueText: text });
+  }
+
+
+  handleTextInput(e) {
+    this.setPollValueText(e.target.value);
+  }
+
+
+  handleDragIn(e) {
+    DragAndDrop.handleDrag(e);
+    this.dragCounter += 1;
+    if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+      this.setState({ drag: true });
+    }
+  }
+
+  handleDragOut(e) {
+    DragAndDrop.handleDrag(e);
+    this.dragCounter -= 1;
+    if (this.dragCounter > 0) return;
+    this.setState({ drag: false });
+  }
+
+  handleDrop(e) {
+    DragAndDrop.handleDrag(e);
+    this.setState({ drag: false });
+    if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+      this.setPollValuesFromFile(e.dataTransfer.files[0]);
+      this.dragCounter = 0;
+    }
+  }
+
+
+  render() {
+    const { intl, children } = this.props;
+    const { pollValueText, drag } = this.state;
+    return (
+      <div
+        className={styles.dndContainer}
+        ref={this.dropRef}
+      >
+        <textarea
+          value={pollValueText}
+          className={drag ? styles.dndActive : styles.dndInActive}
+          onChange={e => this.handleTextInput(e)}
+        />
+        <Button
+          onClick={() => this.setPollValues()}
+          label={intl.formatMessage(intlMessages.customPollTextArea)}
+          color="primary"
+          disabled={pollValueText < 1}
+          className={styles.btn}
+        />
+        {children}
+      </div>
+
+    );
+  }
+} export default withModalMounter(injectIntl(DragAndDrop));
+
+DragAndDrop.propTypes = {
+  intl: PropTypes.shape({
+    formatMessage: PropTypes.func.isRequired,
+  }).isRequired,
+  MAX_INPUT_CHARS: PropTypes.number.isRequired,
+  handlePollValuesText: PropTypes.func.isRequired,
+  children: PropTypes.element.isRequired,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss
new file mode 100755
index 0000000000000000000000000000000000000000..abd06217ecf4e5d8ce4d9b48333dfc7ab4eb1b12
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss
@@ -0,0 +1,26 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.dndContainer {
+  height: 200px;
+}
+
+.customPollValuesTextfield {
+  width: 100%;
+  height: 100%;
+  resize: none;
+  font-size: var(--font-size-small);
+}
+
+.dndActive {
+  @extend .customPollValuesTextfield;
+  background: grey;
+}
+
+.dndInActive {
+  @extend .customPollValuesTextfield;
+  background: white;
+}
+
+.btn {
+  width: 100%;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/poll/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/styles.scss
index d2e1953cdb266325c91c8c6d45ced32528841697..775d7d23a23700f4938566666300363e47b9f21a 100644
--- a/bigbluebutton-html5/imports/ui/components/poll/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/poll/styles.scss
@@ -356,3 +356,8 @@
         color: var(--color-white) !important;
     }
 }
+
+.dragAndDropPollContainer {
+    width: 200px !important;
+    height: 200px !important;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/settings/service.js b/bigbluebutton-html5/imports/ui/components/settings/service.js
index 85af45dddd425ef6af2f48f04cd3115870e57262..42698cb30c268933700445e9841c7c8ee30ad0a4 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/service.js
+++ b/bigbluebutton-html5/imports/ui/components/settings/service.js
@@ -27,7 +27,7 @@ const updateSettings = (obj, msg) => {
   }
 };
 
-const getAvailableLocales = () => fetch('./locales').then(locales => locales.json());
+const getAvailableLocales = () => fetch('./locale-list').then(locales => locales.json());
 
 export {
   getUserRoles,
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 478f587da062616d9e447f9a31ff7edbd11725b0..38ac61e6cfa011baab6b10ff88c3f8878aedf4f3 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -295,6 +295,7 @@ public:
   poll:
     enabled: true
     max_custom: 5
+    allowDragAndDropFile: false
   captions:
     enabled: true
     enableDictation: false
diff --git a/bigbluebutton-html5/private/locales/ar.json b/bigbluebutton-html5/public/locales/ar.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ar.json
rename to bigbluebutton-html5/public/locales/ar.json
diff --git a/bigbluebutton-html5/private/locales/az.json b/bigbluebutton-html5/public/locales/az.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/az.json
rename to bigbluebutton-html5/public/locales/az.json
diff --git a/bigbluebutton-html5/private/locales/bg_BG.json b/bigbluebutton-html5/public/locales/bg_BG.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/bg_BG.json
rename to bigbluebutton-html5/public/locales/bg_BG.json
diff --git a/bigbluebutton-html5/private/locales/ca.json b/bigbluebutton-html5/public/locales/ca.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ca.json
rename to bigbluebutton-html5/public/locales/ca.json
diff --git a/bigbluebutton-html5/private/locales/cs_CZ.json b/bigbluebutton-html5/public/locales/cs_CZ.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/cs_CZ.json
rename to bigbluebutton-html5/public/locales/cs_CZ.json
diff --git a/bigbluebutton-html5/private/locales/da.json b/bigbluebutton-html5/public/locales/da.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/da.json
rename to bigbluebutton-html5/public/locales/da.json
diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/public/locales/de.json
similarity index 99%
rename from bigbluebutton-html5/private/locales/de.json
rename to bigbluebutton-html5/public/locales/de.json
index 53555c2f9daf9d8546add45088bbbbc0e4cf9455..a2776b6914f13f8c6182961753fa6f71c630d896 100644
--- a/bigbluebutton-html5/private/locales/de.json
+++ b/bigbluebutton-html5/public/locales/de.json
@@ -219,6 +219,10 @@
     "app.poll.hidePollDesc": "Versteckt das Umfragemenü",
     "app.poll.quickPollInstruction": "Wählen Sie eine der unten stehenden Optionen, um die Umfrage zu starten.",
     "app.poll.activePollInstruction": "Lassen Sie dieses Fenster offen, um auf die Antworten der Teilnehmer zu warten. Sobald Sie auf 'Umfrageergebnisse veröffentlichen' klicken, werden die Ergebnisse angezeigt und die Umfrage beendet.",
+    "app.poll.customPollLabel": "Benutzerdefinierte Umfrage",
+    "app.poll.startCustomLabel": "Benutzerdefinierte Umfrage starten",
+    "app.poll.dragDropPollInstruction": "Ziehen Sie per drag and Drop eine Textdatei mit den Umfrageoptionen auf das markierte Feld",
+    "app.poll.customPollTextArea": "Umfrageoptionen ausfüllen",
     "app.poll.publishLabel": "Umfrageergebnisse veröffentlichen",
     "app.poll.backLabel": "Umfrage starten",
     "app.poll.closeLabel": "Schließen",
diff --git a/bigbluebutton-html5/private/locales/el_GR.json b/bigbluebutton-html5/public/locales/el_GR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/el_GR.json
rename to bigbluebutton-html5/public/locales/el_GR.json
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/public/locales/en.json
similarity index 99%
rename from bigbluebutton-html5/private/locales/en.json
rename to bigbluebutton-html5/public/locales/en.json
index b7d464fd7237de9bdf7fce11fccfa28f6e0f2ed7..36074d87d8ed2a5de5726783b66e62a967b37969 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -221,6 +221,8 @@
     "app.poll.hidePollDesc": "Hides the poll menu pane",
     "app.poll.quickPollInstruction": "Select an option below to start your poll.",
     "app.poll.activePollInstruction": "Leave this panel open to see live responses to your poll. When you are ready, select 'Publish polling results' to publish the results and end the poll.",
+    "app.poll.dragDropPollInstruction": "To fill the poll values, drag a text file with the poll values onto the highlighted field",
+    "app.poll.customPollTextArea": "Fill poll values",
     "app.poll.publishLabel": "Publish polling results",
     "app.poll.backLabel": "Start A Poll",
     "app.poll.closeLabel": "Close",
diff --git a/bigbluebutton-html5/private/locales/eo.json b/bigbluebutton-html5/public/locales/eo.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/eo.json
rename to bigbluebutton-html5/public/locales/eo.json
diff --git a/bigbluebutton-html5/private/locales/es.json b/bigbluebutton-html5/public/locales/es.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/es.json
rename to bigbluebutton-html5/public/locales/es.json
diff --git a/bigbluebutton-html5/private/locales/es_ES.json b/bigbluebutton-html5/public/locales/es_ES.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/es_ES.json
rename to bigbluebutton-html5/public/locales/es_ES.json
diff --git a/bigbluebutton-html5/private/locales/es_MX.json b/bigbluebutton-html5/public/locales/es_MX.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/es_MX.json
rename to bigbluebutton-html5/public/locales/es_MX.json
diff --git a/bigbluebutton-html5/private/locales/et.json b/bigbluebutton-html5/public/locales/et.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/et.json
rename to bigbluebutton-html5/public/locales/et.json
diff --git a/bigbluebutton-html5/private/locales/eu.json b/bigbluebutton-html5/public/locales/eu.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/eu.json
rename to bigbluebutton-html5/public/locales/eu.json
diff --git a/bigbluebutton-html5/private/locales/fa_IR.json b/bigbluebutton-html5/public/locales/fa_IR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/fa_IR.json
rename to bigbluebutton-html5/public/locales/fa_IR.json
diff --git a/bigbluebutton-html5/private/locales/fi.json b/bigbluebutton-html5/public/locales/fi.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/fi.json
rename to bigbluebutton-html5/public/locales/fi.json
diff --git a/bigbluebutton-html5/private/locales/fr.json b/bigbluebutton-html5/public/locales/fr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/fr.json
rename to bigbluebutton-html5/public/locales/fr.json
diff --git a/bigbluebutton-html5/private/locales/gl.json b/bigbluebutton-html5/public/locales/gl.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/gl.json
rename to bigbluebutton-html5/public/locales/gl.json
diff --git a/bigbluebutton-html5/private/locales/he.json b/bigbluebutton-html5/public/locales/he.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/he.json
rename to bigbluebutton-html5/public/locales/he.json
diff --git a/bigbluebutton-html5/private/locales/hi_IN.json b/bigbluebutton-html5/public/locales/hi_IN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hi_IN.json
rename to bigbluebutton-html5/public/locales/hi_IN.json
diff --git a/bigbluebutton-html5/private/locales/hr.json b/bigbluebutton-html5/public/locales/hr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hr.json
rename to bigbluebutton-html5/public/locales/hr.json
diff --git a/bigbluebutton-html5/private/locales/hu_HU.json b/bigbluebutton-html5/public/locales/hu_HU.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hu_HU.json
rename to bigbluebutton-html5/public/locales/hu_HU.json
diff --git a/bigbluebutton-html5/private/locales/hy.json b/bigbluebutton-html5/public/locales/hy.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hy.json
rename to bigbluebutton-html5/public/locales/hy.json
diff --git a/bigbluebutton-html5/private/locales/id.json b/bigbluebutton-html5/public/locales/id.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/id.json
rename to bigbluebutton-html5/public/locales/id.json
diff --git a/bigbluebutton-html5/private/locales/it_IT.json b/bigbluebutton-html5/public/locales/it_IT.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/it_IT.json
rename to bigbluebutton-html5/public/locales/it_IT.json
diff --git a/bigbluebutton-html5/private/locales/ja.json b/bigbluebutton-html5/public/locales/ja.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ja.json
rename to bigbluebutton-html5/public/locales/ja.json
diff --git a/bigbluebutton-html5/private/locales/ka.json b/bigbluebutton-html5/public/locales/ka.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ka.json
rename to bigbluebutton-html5/public/locales/ka.json
diff --git a/bigbluebutton-html5/private/locales/kk.json b/bigbluebutton-html5/public/locales/kk.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/kk.json
rename to bigbluebutton-html5/public/locales/kk.json
diff --git a/bigbluebutton-html5/private/locales/km.json b/bigbluebutton-html5/public/locales/km.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/km.json
rename to bigbluebutton-html5/public/locales/km.json
diff --git a/bigbluebutton-html5/private/locales/kn.json b/bigbluebutton-html5/public/locales/kn.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/kn.json
rename to bigbluebutton-html5/public/locales/kn.json
diff --git a/bigbluebutton-html5/private/locales/ko_KR.json b/bigbluebutton-html5/public/locales/ko_KR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ko_KR.json
rename to bigbluebutton-html5/public/locales/ko_KR.json
diff --git a/bigbluebutton-html5/private/locales/lo_LA.json b/bigbluebutton-html5/public/locales/lo_LA.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/lo_LA.json
rename to bigbluebutton-html5/public/locales/lo_LA.json
diff --git a/bigbluebutton-html5/private/locales/lt_LT.json b/bigbluebutton-html5/public/locales/lt_LT.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/lt_LT.json
rename to bigbluebutton-html5/public/locales/lt_LT.json
diff --git a/bigbluebutton-html5/private/locales/lv.json b/bigbluebutton-html5/public/locales/lv.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/lv.json
rename to bigbluebutton-html5/public/locales/lv.json
diff --git a/bigbluebutton-html5/private/locales/mn_MN.json b/bigbluebutton-html5/public/locales/mn_MN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/mn_MN.json
rename to bigbluebutton-html5/public/locales/mn_MN.json
diff --git a/bigbluebutton-html5/private/locales/nb_NO.json b/bigbluebutton-html5/public/locales/nb_NO.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/nb_NO.json
rename to bigbluebutton-html5/public/locales/nb_NO.json
diff --git a/bigbluebutton-html5/private/locales/nl.json b/bigbluebutton-html5/public/locales/nl.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/nl.json
rename to bigbluebutton-html5/public/locales/nl.json
diff --git a/bigbluebutton-html5/private/locales/oc.json b/bigbluebutton-html5/public/locales/oc.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/oc.json
rename to bigbluebutton-html5/public/locales/oc.json
diff --git a/bigbluebutton-html5/private/locales/pl_PL.json b/bigbluebutton-html5/public/locales/pl_PL.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/pl_PL.json
rename to bigbluebutton-html5/public/locales/pl_PL.json
diff --git a/bigbluebutton-html5/private/locales/pt.json b/bigbluebutton-html5/public/locales/pt.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/pt.json
rename to bigbluebutton-html5/public/locales/pt.json
diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/public/locales/pt_BR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/pt_BR.json
rename to bigbluebutton-html5/public/locales/pt_BR.json
diff --git a/bigbluebutton-html5/private/locales/ro_RO.json b/bigbluebutton-html5/public/locales/ro_RO.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ro_RO.json
rename to bigbluebutton-html5/public/locales/ro_RO.json
diff --git a/bigbluebutton-html5/private/locales/ru.json b/bigbluebutton-html5/public/locales/ru.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ru.json
rename to bigbluebutton-html5/public/locales/ru.json
diff --git a/bigbluebutton-html5/private/locales/ru_RU.json b/bigbluebutton-html5/public/locales/ru_RU.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ru_RU.json
rename to bigbluebutton-html5/public/locales/ru_RU.json
diff --git a/bigbluebutton-html5/private/locales/sk_SK.json b/bigbluebutton-html5/public/locales/sk_SK.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sk_SK.json
rename to bigbluebutton-html5/public/locales/sk_SK.json
diff --git a/bigbluebutton-html5/private/locales/sl.json b/bigbluebutton-html5/public/locales/sl.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sl.json
rename to bigbluebutton-html5/public/locales/sl.json
diff --git a/bigbluebutton-html5/private/locales/sr.json b/bigbluebutton-html5/public/locales/sr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sr.json
rename to bigbluebutton-html5/public/locales/sr.json
diff --git a/bigbluebutton-html5/private/locales/sv_SE.json b/bigbluebutton-html5/public/locales/sv_SE.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sv_SE.json
rename to bigbluebutton-html5/public/locales/sv_SE.json
diff --git a/bigbluebutton-html5/private/locales/te.json b/bigbluebutton-html5/public/locales/te.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/te.json
rename to bigbluebutton-html5/public/locales/te.json
diff --git a/bigbluebutton-html5/private/locales/th.json b/bigbluebutton-html5/public/locales/th.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/th.json
rename to bigbluebutton-html5/public/locales/th.json
diff --git a/bigbluebutton-html5/private/locales/th_TH.json b/bigbluebutton-html5/public/locales/th_TH.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/th_TH.json
rename to bigbluebutton-html5/public/locales/th_TH.json
diff --git a/bigbluebutton-html5/private/locales/tr.json b/bigbluebutton-html5/public/locales/tr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/tr.json
rename to bigbluebutton-html5/public/locales/tr.json
diff --git a/bigbluebutton-html5/private/locales/tr_TR.json b/bigbluebutton-html5/public/locales/tr_TR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/tr_TR.json
rename to bigbluebutton-html5/public/locales/tr_TR.json
diff --git a/bigbluebutton-html5/private/locales/uk_UA.json b/bigbluebutton-html5/public/locales/uk_UA.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/uk_UA.json
rename to bigbluebutton-html5/public/locales/uk_UA.json
diff --git a/bigbluebutton-html5/private/locales/vi.json b/bigbluebutton-html5/public/locales/vi.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/vi.json
rename to bigbluebutton-html5/public/locales/vi.json
diff --git a/bigbluebutton-html5/private/locales/vi_VN.json b/bigbluebutton-html5/public/locales/vi_VN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/vi_VN.json
rename to bigbluebutton-html5/public/locales/vi_VN.json
diff --git a/bigbluebutton-html5/private/locales/zh_CN.json b/bigbluebutton-html5/public/locales/zh_CN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/zh_CN.json
rename to bigbluebutton-html5/public/locales/zh_CN.json
diff --git a/bigbluebutton-html5/private/locales/zh_TW.json b/bigbluebutton-html5/public/locales/zh_TW.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/zh_TW.json
rename to bigbluebutton-html5/public/locales/zh_TW.json
diff --git a/bigbluebutton-html5/transifex.sh b/bigbluebutton-html5/transifex.sh
index de1efe3eaa73f91ca62b80f75b450bf59da70587..b11dc50f7ac3f69058c0106abd1dfa425230b819 100755
--- a/bigbluebutton-html5/transifex.sh
+++ b/bigbluebutton-html5/transifex.sh
@@ -5,6 +5,14 @@ RED='\033[0;31m'
 GREEN='\033[1;32m'
 NC='\033[0m'
 SOURCE_LANGUAGE="en"
+LOCALES_DIRECTORY="./public/locales"
+PULL_SOURCE=false
+
+if [[ ! -e $LOCALES_DIRECTORY ]]; then
+    echo -e "Directory ${RED}$LOCALES_DIRECTORY${NC} does not exist, creating"
+    mkdir $LOCALES_DIRECTORY
+    PULL_SOURCE=true
+fi
 
 if [ "$#" = 0 ]
 then
@@ -33,19 +41,19 @@ else
           echo "$AVAILABLE_TRANSLATIONS" | while read l
             do
               LOCALE=$( echo "$l" | tr -d '[:space:]' )
-              if [ "$LOCALE" == "$SOURCE_LANGUAGE" ]; then
-                continue # do not pull the source file
+              if [ "$LOCALE" == "$SOURCE_LANGUAGE" ] && [ "$PULL_SOURCE" == false ]; then
+                continue # only pull source file if locales folder did not exist
               fi
               TRANSLATION=$(curl -L --user "$USER":"$PW" -X GET "https://www.transifex.com/api/2/project/bigbluebutton-v23-html5-client/resource/enjson/translation/$LOCALE/?mode=onlytranslated&file")
               NO_EMPTY_STRINGS=$(echo "$TRANSLATION" | sed '/: *\"\"/D' | sed '/}$/D')
-              if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) == 1 ]
+              if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) -lt 100 ]
               then
-                echo -e "${RED}WARN:${NC} translation file $LOCALE.json is empty\n${RED}WARN:${NC} $LOCALE.json not created"
+                echo -e "${RED}WARN:${NC} translation file $LOCALE.json contains less than 100 lines\n${RED}WARN:${NC} $LOCALE.json not created"
                 continue
               else
                 NO_TRAILING_COMMA=$(echo "$NO_EMPTY_STRINGS" | sed  '$ s/,$//')
-                echo "$NO_TRAILING_COMMA" > ./private/locales/"$LOCALE".json
-                echo -e "\n}\n" >> ./private/locales/"$LOCALE".json
+                echo "$NO_TRAILING_COMMA" > "$LOCALES_DIRECTORY/$LOCALE".json
+                echo -e "\n}\n" >> "$LOCALES_DIRECTORY/$LOCALE".json
                 echo -e "Added translation file $LOCALE.json : ${GREEN}✓${NC}"
               fi
             done
@@ -56,13 +64,13 @@ else
             echo -e "${RED}Err${NC}: Translations not found for locale ->${RED}$ARG${NC}<-"
           else
             NO_EMPTY_STRINGS=$(echo "$TRANSLATION" | sed '/: *\"\"/D' | sed '/}$/D')
-            if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) == 1 ]
+            if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) -lt 100 ]
             then
-              echo -e "${RED}WARN:${NC} translation file $ARG.json is empty\n${RED}WARN:${NC} $ARG.json not created"
+              echo -e "${RED}WARN:${NC} translation file $ARG.json contains less than 100 lines\n${RED}WARN:${NC} $ARG.json not created"
             else
               NO_TRAILING_COMMA=$(echo "$NO_EMPTY_STRINGS" | sed  '$ s/,//')
-              echo "$NO_TRAILING_COMMA" > ./private/locales/"$ARG".json
-              echo -e "\n}\n" >> ./private/locales/"$ARG".json
+              echo "$NO_TRAILING_COMMA" > "$LOCALES_DIRECTORY/$ARG".json
+              echo -e "\n}\n" >> "$LOCALES_DIRECTORY/$ARG".json
               echo -e "Added translation file $ARG.json :${GREEN} ✓${NC}"
             fi
           fi