diff --git a/package.json b/package.json
index c51758645a007e87d64233a070982d4bfdcd8b55..c41835048ff4df542c9e64af7ce2b788eb455ca1 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",
@@ -94,7 +93,7 @@
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "0.1.0-prerelease.20180618171838",
-    "scratch-blocks": "0.1.0-prerelease.1529499405",
+    "scratch-blocks": "0.1.0-prerelease.1529616842",
     "scratch-l10n": "3.0.20180611175036",
     "scratch-paint": "0.2.0-prerelease.20180619182645",
     "scratch-render": "0.1.0-prerelease.20180618173030",
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 03639242144a9c67369d98b9dc04831e291e3bd1..1b411d6336e9291efabdbca2959ef0f85a7f1d96 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.locales.locale,
+    messages: state.locales.messages,
     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..c3f108efbe3315dd732a3b450479efddc7bec00d 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,11 +15,15 @@ class LanguageSelector extends React.Component {
         ]);
     }
     handleChange (e) {
-        this.props.onChangeLanguage(e.target.value);
+        const newLocale = e.target.value;
+        if (this.props.supportedLocales.includes(newLocale)) {
+            this.props.onChangeLanguage(newLocale);
+        }
     }
     render () {
         const {
             onChangeLanguage, // eslint-disable-line no-unused-vars
+            supportedLocales, // eslint-disable-line no-unused-vars
             children,
             ...props
         } = this.props;
@@ -36,16 +40,19 @@ class LanguageSelector extends React.Component {
 
 LanguageSelector.propTypes = {
     children: PropTypes.node,
-    onChangeLanguage: PropTypes.func.isRequired
+    currentLocale: PropTypes.string.isRequired,
+    onChangeLanguage: PropTypes.func.isRequired,
+    supportedLocales: PropTypes.arrayOf(PropTypes.string)
 };
 
 const mapStateToProps = state => ({
-    currentLocale: state.intl.locale
+    currentLocale: state.locales.locale,
+    supportedLocales: Object.keys(state.locales.messagesByLocale)
 });
 
 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..dfd32a399de9c132135e2cb0f0596bd8eecc74a2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,12 +1,12 @@
 import GUI from './containers/gui.jsx';
 import GuiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from './reducers/gui';
+import LocalesReducer, {localesInitialState, initLocale} from './reducers/locales';
 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,
+    locales: LocalesReducer,
     scratchGui: GuiReducer,
     scratchPaint: ScratchPaintReducer
 };
@@ -19,6 +19,8 @@ export {
     guiMiddleware,
     initPlayer,
     initFullScreen,
+    initLocale,
+    localesInitialState,
     setFullScreen,
     setPlayer
 };
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
index dc62aa31bca70f8a05ea90cc698d8050c811ea7c..20e2852a131c5ad70a878d4d0d4b3038df6c8548 100644
--- a/src/lib/app-state-hoc.jsx
+++ b/src/lib/app-state-hoc.jsx
@@ -2,12 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {Provider} from 'react-redux';
 import {createStore, combineReducers, compose} from 'redux';
-
-import {intlShape} from 'react-intl';
-import {IntlProvider, updateIntl} from 'react-intl-redux';
-import intlReducer from '../reducers/intl.js';
+import ConnectedIntlProvider from './connected-intl-provider.jsx';
 
 import guiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from '../reducers/gui';
+import localesReducer, {initLocale, localesInitialState} from '../reducers/locales';
 
 import {setPlayer, setFullScreen} from '../reducers/mode.js';
 
@@ -33,21 +31,28 @@ const AppStateHOC = function (WrappedComponent) {
             if (props.isPlayerOnly) {
                 initializedGui = initPlayer(initializedGui);
             }
+
+            let initializedLocales = localesInitialState;
+            if (window.location.search.indexOf('locale=') !== -1 ||
+                window.location.search.indexOf('lang=') !== -1) {
+                const locale = window.location.search.match(/(?:locale|lang)=([\w]+)/)[1];
+                initializedLocales = initLocale(initializedLocales, locale);
+            }
+
             const reducer = combineReducers({
-                intl: intlReducer,
+                locales: localesReducer,
                 scratchGui: guiReducer,
                 scratchPaint: ScratchPaintReducer
             });
-
             this.store = createStore(
                 reducer,
-                {scratchGui: initializedGui},
+                {
+                    locales: initializedLocales,
+                    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 +62,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..e4150ef1b39fe73dd1e5db92c1ab427cb16298d5
--- /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.locales.locale,
+    locale: state.locales.locale,
+    messages: state.locales.messages
+});
+
+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..7d98ae2f3f74a757bc101910bbabe6fd491fc192 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -58,6 +58,7 @@ const initFullScreen = function (currentState) {
         }}
     );
 };
+
 const guiReducer = combineReducers({
     assetDrag: assetDragReducer,
     blockDrag: blockDragReducer,
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..fda8f9dc36d14dac09f72fad62f704617b87e0cf
--- /dev/null
+++ b/src/reducers/locales.js
@@ -0,0 +1,71 @@
+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',
+    messagesByLocale: editorMessages,
+    messages: editorMessages.en
+};
+
+const reducer = function (state, action) {
+    if (typeof state === 'undefined') state = initialState;
+    switch (action.type) {
+    case SELECT_LOCALE:
+        return Object.assign({}, state, {
+            locale: action.locale,
+            messagesByLocale: state.messagesByLocale,
+            messages: state.messagesByLocale[action.locale]
+        });
+    case UPDATE_LOCALES:
+        return Object.assign({}, state, {
+            locale: state.locale,
+            messagesByLocale: action.messagesByLocale,
+            messages: action.messagesByLocale[state.locale]
+        });
+    default:
+        return state;
+    }
+};
+
+const selectLocale = function (locale) {
+    return {
+        type: SELECT_LOCALE,
+        locale: locale
+    };
+};
+
+const setLocales = function (localesMessages) {
+    return {
+        type: UPDATE_LOCALES,
+        messagesByLocale: localesMessages
+    };
+};
+const initLocale = function (currentState, locale) {
+    if (currentState.messagesByLocale.hasOwnProperty(locale)) {
+        return Object.assign(
+            {},
+            currentState,
+            {
+                locale: locale,
+                messagesByLocale: currentState.messagesByLocale,
+                messages: currentState.messagesByLocale[locale]
+            }
+        );
+    }
+    // don't change locale if it's not in the current messages
+    return currentState;
+};
+export {
+    reducer as default,
+    initialState as localesInitialState,
+    initLocale,
+    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');
     });
 });