diff --git a/package.json b/package.json
index c598483d7c9475bca9f1260e7c91246c5624403a..e2121c84bd3caf84306129d59ea0ca41c07918cc 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,6 @@
     "react-draggable": "3.0.5",
     "react-ga": "2.5.3",
     "react-intl": "2.4.0",
-    "react-intl-redux": "0.7.0",
     "react-modal": "3.4.4",
     "react-popover": "0.5.7",
     "react-redux": "5.0.7",
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 03639242144a9c67369d98b9dc04831e291e3bd1..fa51f47733859039615b280c0dddb21b3b03bd47 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -52,7 +52,8 @@ class Blocks extends React.Component {
             'onVisualReport',
             'onWorkspaceUpdate',
             'onWorkspaceMetricsChange',
-            'setBlocks'
+            'setBlocks',
+            'setLocale'
         ]);
         this.ScratchBlocks.prompt = this.handlePromptStart;
         this.state = {
@@ -86,7 +87,7 @@ class Blocks extends React.Component {
         addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange);
 
         this.attachVM();
-        this.props.vm.setLocale(this.props.locale, this.props.messages);
+        this.setLocale();
 
         analytics.pageview('/editors/blocks');
     }
@@ -109,7 +110,7 @@ class Blocks extends React.Component {
         }
 
         if (prevProps.locale !== this.props.locale) {
-            this.props.vm.setLocale(this.props.locale, this.props.messages);
+            this.setLocale();
         }
 
         if (prevProps.toolboxXML !== this.props.toolboxXML) {
@@ -144,6 +145,16 @@ class Blocks extends React.Component {
         clearTimeout(this.toolboxUpdateTimeout);
     }
 
+    setLocale () {
+        this.workspace.getFlyout().setRecyclingEnabled(false);
+        this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
+        this.props.vm.setLocale(this.props.locale, this.props.messages);
+
+        this.workspace.updateToolbox(this.props.toolboxXML);
+        this.props.vm.refreshWorkspace();
+        this.workspace.getFlyout().setRecyclingEnabled(true);
+    }
+
     updateToolbox () {
         this.toolboxUpdateTimeout = false;
 
@@ -461,8 +472,8 @@ const mapStateToProps = state => ({
         state.scratchGui.mode.isFullScreen
     ),
     extensionLibraryVisible: state.scratchGui.modals.extensionLibrary,
-    locale: state.intl.locale,
-    messages: state.intl.messages,
+    locale: state.scratchGui.locales.locale,
+    messages: state.scratchGui.locales.messages[state.scratchGui.locales.locale],
     toolboxXML: state.scratchGui.toolbox.toolboxXML,
     customProceduresVisible: state.scratchGui.customProcedures.active
 });
diff --git a/src/containers/language-selector.jsx b/src/containers/language-selector.jsx
index 49fb60943ada6bdefa77f95548dc40e2b3080d79..0b61984becbf6a0751c7bd93c6128091ebfbf195 100644
--- a/src/containers/language-selector.jsx
+++ b/src/containers/language-selector.jsx
@@ -2,7 +2,7 @@ import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
 import {connect} from 'react-redux';
-import {updateIntl} from 'react-intl-redux';
+import {selectLocale} from '../reducers/locales';
 import {closeLanguageMenu} from '../reducers/menus';
 
 import LanguageSelectorComponent from '../components/language-selector/language-selector.jsx';
@@ -15,7 +15,10 @@ class LanguageSelector extends React.Component {
         ]);
     }
     handleChange (e) {
-        this.props.onChangeLanguage(e.target.value);
+        const newLocale = e.target.value;
+        if (this.props.locales.hasOwnProperty(newLocale)) {
+            this.props.onChangeLanguage(newLocale);
+        }
     }
     render () {
         const {
@@ -36,16 +39,19 @@ class LanguageSelector extends React.Component {
 
 LanguageSelector.propTypes = {
     children: PropTypes.node,
+    currentLocale: PropTypes.string.isRequired,
+    locales: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
     onChangeLanguage: PropTypes.func.isRequired
 };
 
 const mapStateToProps = state => ({
-    currentLocale: state.intl.locale
+    currentLocale: state.scratchGui.locales.locale,
+    locales: state.scratchGui.locales.messages
 });
 
 const mapDispatchToProps = dispatch => ({
     onChangeLanguage: locale => {
-        dispatch(updateIntl({locale: locale, messages: {}}));
+        dispatch(selectLocale(locale));
         dispatch(closeLanguageMenu());
     }
 });
diff --git a/src/containers/menu.jsx b/src/containers/menu.jsx
index 865ad19d3d1181b62c0125a7efbc7ca8c7302b57..e9a51cd4ced969134675d81635388e16c0d5507a 100644
--- a/src/containers/menu.jsx
+++ b/src/containers/menu.jsx
@@ -13,12 +13,17 @@ class Menu extends React.Component {
             'handleClick',
             'ref'
         ]);
-        if (props.open) this.addListeners();
+    }
+    componentDidMount () {
+        if (this.props.open) this.addListeners();
     }
     componentDidUpdate (prevProps) {
         if (this.props.open && !prevProps.open) this.addListeners();
         if (!this.props.open && prevProps.open) this.removeListeners();
     }
+    componentWillUnmount () {
+        this.removeListeners();
+    }
     addListeners () {
         document.addEventListener('mouseup', this.handleClick);
     }
diff --git a/src/index.js b/src/index.js
index 3bfa737f52db778b0196009bcfecb83c96f94795..f47b279bce033d037381b77641801a60d567e269 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,12 +1,10 @@
 import GUI from './containers/gui.jsx';
-import GuiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from './reducers/gui';
+import GuiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer, initLocale} from './reducers/gui';
 import {ScratchPaintReducer} from 'scratch-paint';
-import IntlReducer from './reducers/intl';
 import {setFullScreen, setPlayer} from './reducers/mode';
 import {setAppElement} from 'react-modal';
 
 const guiReducers = {
-    intl: IntlReducer,
     scratchGui: GuiReducer,
     scratchPaint: ScratchPaintReducer
 };
@@ -19,6 +17,7 @@ export {
     guiMiddleware,
     initPlayer,
     initFullScreen,
+    initLocale,
     setFullScreen,
     setPlayer
 };
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
index dc62aa31bca70f8a05ea90cc698d8050c811ea7c..7c23c48393394aa6e08dadcb07e707a4199b52ea 100644
--- a/src/lib/app-state-hoc.jsx
+++ b/src/lib/app-state-hoc.jsx
@@ -2,12 +2,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {Provider} from 'react-redux';
 import {createStore, combineReducers, compose} from 'redux';
+import ConnectedIntlProvider from './connected-intl-provider.jsx';
 
-import {intlShape} from 'react-intl';
-import {IntlProvider, updateIntl} from 'react-intl-redux';
-import intlReducer from '../reducers/intl.js';
-
-import guiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from '../reducers/gui';
+import guiReducer, {guiInitialState, guiMiddleware, initFullScreen, initLocale, initPlayer} from '../reducers/gui';
 
 import {setPlayer, setFullScreen} from '../reducers/mode.js';
 
@@ -34,20 +31,22 @@ const AppStateHOC = function (WrappedComponent) {
                 initializedGui = initPlayer(initializedGui);
             }
             const reducer = combineReducers({
-                intl: intlReducer,
                 scratchGui: guiReducer,
                 scratchPaint: ScratchPaintReducer
             });
 
+            if (window.location.search.indexOf('locale=') !== -1 ||
+                window.location.search.indexOf('lang=') !== -1) {
+                const locale = window.location.search.match(/(?:locale|lang)=([\w]+)/)[1];
+                initializedGui = initLocale(initializedGui, locale);
+            }
+
             this.store = createStore(
                 reducer,
                 {scratchGui: initializedGui},
                 enhancer);
         }
         componentDidUpdate (prevProps) {
-            if (prevProps.intl !== this.props.intl) {
-                this.store.dispatch(updateIntl(this.props.intl));
-            }
             if (prevProps.isPlayerOnly !== this.props.isPlayerOnly) {
                 this.store.dispatch(setPlayer(this.props.isPlayerOnly));
             }
@@ -57,22 +56,20 @@ const AppStateHOC = function (WrappedComponent) {
         }
         render () {
             const {
-                intl, // eslint-disable-line no-unused-vars
                 isFullScreen, // eslint-disable-line no-unused-vars
                 isPlayerOnly, // eslint-disable-line no-unused-vars
                 ...componentProps
             } = this.props;
             return (
                 <Provider store={this.store}>
-                    <IntlProvider>
+                    <ConnectedIntlProvider>
                         <WrappedComponent {...componentProps} />
-                    </IntlProvider>
+                    </ConnectedIntlProvider>
                 </Provider>
             );
         }
     }
     AppStateWrapper.propTypes = {
-        intl: intlShape,
         isFullScreen: PropTypes.bool,
         isPlayerOnly: PropTypes.bool
     };
diff --git a/src/lib/connected-intl-provider.jsx b/src/lib/connected-intl-provider.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e1eacb3adc077a83ca2bb52b631d6453e1c316d0
--- /dev/null
+++ b/src/lib/connected-intl-provider.jsx
@@ -0,0 +1,10 @@
+import {IntlProvider as ReactIntlProvider} from 'react-intl';
+import {connect} from 'react-redux';
+
+const mapStateToProps = state => ({
+    key: state.scratchGui.locales.locale,
+    locale: state.scratchGui.locales.locale,
+    messages: state.scratchGui.locales.messages[state.scratchGui.locales.locale]
+});
+
+export default connect(mapStateToProps)(ReactIntlProvider);
diff --git a/src/lib/hash-parser-hoc.jsx b/src/lib/hash-parser-hoc.jsx
index 04250aa6e9f1297f662574d649469e3bb610c333..f3ac92542808f2e6e7ee1f9acbdf3978c3b4e7c8 100644
--- a/src/lib/hash-parser-hoc.jsx
+++ b/src/lib/hash-parser-hoc.jsx
@@ -24,9 +24,9 @@ const HashParserHOC = function (WrappedComponent) {
             window.removeEventListener('hashchange', this.handleHashChange);
         }
         handleHashChange () {
-            let projectId = window.location.hash.substring(1);
+            const hashMatch = window.location.hash.match(/#(\d+)/);
+            const projectId = hashMatch === null ? 0 : hashMatch[1];
             if (projectId !== this.state.projectId) {
-                if (projectId.length < 1) projectId = 0;
                 this.setState({projectId: projectId});
             }
         }
diff --git a/src/playground/intl.js b/src/playground/intl.js
deleted file mode 100644
index 42b72b003e59fed430ea5ae7d1a73f50e9f40bad..0000000000000000000000000000000000000000
--- a/src/playground/intl.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import {addLocaleData} from 'react-intl';
-
-import localeData from 'scratch-l10n';
-import editorMessages from 'scratch-l10n/locales/editor-msgs';
-
-Object.keys(localeData).forEach(locale => {
-    addLocaleData(localeData[locale].localeData);
-});
-
-const intlDefault = {
-    defaultLocale: 'en',
-    locale: 'en',
-    messages: editorMessages.en
-};
-
-export {
-    intlDefault as default,
-    editorMessages
-};
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index 829f0b0446171e56ca2c3157ea316c5611727d2a..810fce7c213ac30aa8ce2fae0ce957b182271631 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -7,6 +7,7 @@ import blockDragReducer, {blockDragInitialState} from './block-drag';
 import editorTabReducer, {editorTabInitialState} from './editor-tab';
 import hoveredTargetReducer, {hoveredTargetInitialState} from './hovered-target';
 import menuReducer, {menuInitialState} from './menus';
+import localesReducer, {localesInitialState} from './locales';
 import modalReducer, {modalsInitialState} from './modals';
 import modeReducer, {modeInitialState} from './mode';
 import monitorReducer, {monitorsInitialState} from './monitors';
@@ -26,6 +27,7 @@ const guiInitialState = {
     colorPicker: colorPickerInitialState,
     customProcedures: customProceduresInitialState,
     editorTab: editorTabInitialState,
+    locales: localesInitialState,
     mode: modeInitialState,
     hoveredTarget: hoveredTargetInitialState,
     stageSize: stageSizeInitialState,
@@ -58,6 +60,20 @@ const initFullScreen = function (currentState) {
         }}
     );
 };
+const initLocale = function (currentState, locale) {
+    if (currentState.locales.messages.hasOwnProperty(locale)) {
+        return Object.assign(
+            {},
+            currentState,
+            {locales: {
+                locale: locale,
+                messages: currentState.locales.messages
+            }}
+        );
+    }
+    // don't change locale if it's not in the current messages
+    return currentState;
+};
 const guiReducer = combineReducers({
     assetDrag: assetDragReducer,
     blockDrag: blockDragReducer,
@@ -65,6 +81,7 @@ const guiReducer = combineReducers({
     colorPicker: colorPickerReducer,
     customProcedures: customProceduresReducer,
     editorTab: editorTabReducer,
+    locales: localesReducer,
     mode: modeReducer,
     hoveredTarget: hoveredTargetReducer,
     stageSize: stageSizeReducer,
@@ -82,5 +99,6 @@ export {
     guiInitialState,
     guiMiddleware,
     initFullScreen,
+    initLocale,
     initPlayer
 };
diff --git a/src/reducers/intl.js b/src/reducers/intl.js
deleted file mode 100644
index 8c49ee10475c5a960c35d6eba723203dbb772091..0000000000000000000000000000000000000000
--- a/src/reducers/intl.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import {intlReducer} from 'react-intl-redux';
-
-const intlInitialState = {
-    intl: {
-        defaultLocale: 'en',
-        locale: 'en',
-        messages: {}
-    }
-};
-
-export {
-    intlReducer as default,
-    intlInitialState
-};
diff --git a/src/reducers/locales.js b/src/reducers/locales.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1336a91e3e76beb755ac9e094e22d4a173707a7
--- /dev/null
+++ b/src/reducers/locales.js
@@ -0,0 +1,53 @@
+import {addLocaleData} from 'react-intl';
+
+import {localeData} from 'scratch-l10n';
+import editorMessages from 'scratch-l10n/locales/editor-msgs';
+
+addLocaleData(localeData);
+
+const UPDATE_LOCALES = 'scratch-gui/locales/UPDATE_LOCALES';
+const SELECT_LOCALE = 'scratch-gui/locales/SELECT_LOCALE';
+
+const initialState = {
+    locale: 'en',
+    messages: editorMessages
+};
+
+const reducer = function (state, action) {
+    if (typeof state === 'undefined') state = initialState;
+    switch (action.type) {
+    case SELECT_LOCALE:
+        return Object.assign({}, state, {
+            locale: action.locale,
+            messages: state.messages
+        });
+    case UPDATE_LOCALES:
+        return Object.assign({}, state, {
+            locale: state.locale,
+            messages: action.messages
+        });
+    default:
+        return state;
+    }
+};
+
+const selectLocale = function (locale) {
+    return {
+        type: SELECT_LOCALE,
+        locale: locale
+    };
+};
+
+const setLocales = function (localesMessages) {
+    return {
+        type: UPDATE_LOCALES,
+        messages: localesMessages
+    };
+};
+
+export {
+    reducer as default,
+    initialState as localesInitialState,
+    selectLocale,
+    setLocales
+};
diff --git a/test/unit/util/hash-project-loader-hoc.test.jsx b/test/unit/util/hash-project-loader-hoc.test.jsx
index 5cf78747ad9b580c2aa506f1e949daa57f7f26e1..1242385d62241588f60b89ea9be57f169f1516fd 100644
--- a/test/unit/util/hash-project-loader-hoc.test.jsx
+++ b/test/unit/util/hash-project-loader-hoc.test.jsx
@@ -8,9 +8,9 @@ describe('HashParserHOC', () => {
     test('when there is a hash, it passes the hash as projectId', () => {
         const Component = ({projectId}) => <div>{projectId}</div>;
         const WrappedComponent = HashParserHOC(Component);
-        window.location.hash = '#winning';
+        window.location.hash = '#1234567';
         const mounted = mount(<WrappedComponent />);
-        expect(mounted.state().projectId).toEqual('winning');
+        expect(mounted.state().projectId).toEqual('1234567');
     });
 
     test('when there is no hash, it passes 0 as the projectId', () => {
@@ -21,14 +21,22 @@ describe('HashParserHOC', () => {
         expect(mounted.state().projectId).toEqual(0);
     });
 
+    test('when the hash is not a number, it passes 0 as projectId', () => {
+        const Component = ({projectId}) => <div>{projectId}</div>;
+        const WrappedComponent = HashParserHOC(Component);
+        window.location.hash = '#winning';
+        const mounted = mount(<WrappedComponent />);
+        expect(mounted.state().projectId).toEqual(0);
+    });
+
     test('when hash change happens, the projectId state is changed', () => {
         const Component = ({projectId}) => <div>{projectId}</div>;
         const WrappedComponent = HashParserHOC(Component);
         window.location.hash = '';
         const mounted = mount(<WrappedComponent />);
         expect(mounted.state().projectId).toEqual(0);
-        window.location.hash = '#winning';
+        window.location.hash = '#1234567';
         mounted.instance().handleHashChange();
-        expect(mounted.state().projectId).toEqual('winning');
+        expect(mounted.state().projectId).toEqual('1234567');
     });
 });