diff --git a/.travis.yml b/.travis.yml
index 33c4de439700bd708bdd20c6e783e63d97992dc0..13ccbc30b19e7a92ed2c25a39bf0f5037daa651a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,12 +11,9 @@ node_js:
 cache:
   directories:
   - node_modules
-env:
-  global:
-  - NODE_ENV=production
 install:
-- npm --production=false install
-- npm --production=false update
+- npm install
+- npm update
 before_deploy:
 - npm --no-git-tag-version version 0.1.0-prerelease.$(date +%Y%m%d%H%M%S)
 - git config --global user.email $(git log --pretty=format:"%ae" -n1)
@@ -39,4 +36,3 @@ deploy:
   skip_cleanup: true
   email: $NPM_EMAIL
   api_key: $NPM_TOKEN
-
diff --git a/.tx/config b/.tx/config
new file mode 100644
index 0000000000000000000000000000000000000000..de8d6bb3d68424fb997ea2fb0ef3947464d94960
--- /dev/null
+++ b/.tx/config
@@ -0,0 +1,8 @@
+[main]
+host = https://www.transifex.com
+
+[experimental-scratch.scratch-gui]
+file_filter = translations/<lang>.json
+source_file = translations/en.json
+source_lang = en
+type = CHROME
diff --git a/package.json b/package.json
index 3bd394669ea5a1f494e992c47cba1f9998e9c0b7..13a4b8c1f0dd219702d475c71207e4b18c4dfd87 100644
--- a/package.json
+++ b/package.json
@@ -4,16 +4,15 @@
   "description": "GraphicaL User Interface for creating and running Scratch 3.0 projects",
   "main": "./src/index.js",
   "scripts": {
-    "build": "npm run clean && npm run i18n:msgs && webpack --progress --colors --bail",
+    "build": "npm run clean && webpack --progress --colors --bail",
     "clean": "rimraf ./build && mkdirp build",
     "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
-    "i18n:msgs": "node ./scripts/generate-locale-messages.js",
     "i18n:src": "babel src > tmp.js && rimraf tmp.js && ./scripts/build-i18n-source.js ./translations/messages/ ./translations/",
-    "lint": "eslint . --ext .js,.jsx",
-    "start": "npm run i18n:msgs && webpack-dev-server",
-    "unit-test": "jest test/unit",
-    "integration-test": "jest --runInBand test/integration",
-    "test": "npm run lint && npm run unit-test && npm run build",
+    "start": "webpack-dev-server",
+    "test": "npm run test:lint && npm run test:unit && NODE_ENV=production npm run build && npm run test:integration",
+    "test:integration": "jest --runInBand test[\\\\/]integration",
+    "test:lint": "eslint . --ext .js,.jsx",
+    "test:unit": "jest test[\\\\/]unit",
     "watch": "webpack --progress --colors --watch"
   },
   "author": "Massachusetts Institute of Technology",
@@ -24,14 +23,14 @@
     "url": "git+ssh://git@github.com/LLK/scratch-gui.git"
   },
   "peerDependencies": {
-    "react": "^15.6.1",
-    "react-dom": "^15.6.1"
+    "react": "^16.0.0",
+    "react-dom": "^16.0.0"
   },
   "devDependencies": {
     "autoprefixer": "^7.1.3",
     "babel-cli": "^6.26.0",
     "babel-core": "^6.23.1",
-    "babel-eslint": "^7.2.3",
+    "babel-eslint": "^8.0.1",
     "babel-loader": "^7.1.0",
     "babel-plugin-react-intl": "^2.3.1",
     "babel-plugin-syntax-dynamic-import": "^6.18.0",
@@ -39,15 +38,18 @@
     "babel-plugin-transform-object-rest-spread": "^6.22.0",
     "babel-preset-es2015": "^6.22.0",
     "babel-preset-react": "^6.22.0",
-    "chromedriver": "^2.32.3",
+    "buffer-loader": "0.0.1",
+    "chromedriver": "2.33.1",
     "classnames": "2.2.5",
     "copy-webpack-plugin": "^4.0.1",
     "css-loader": "^0.28.7",
-    "enzyme": "^2.8.2",
+    "enzyme": "^3.1.0",
+    "enzyme-adapter-react-16": "1.0.3",
     "eslint": "^4.7.1",
-    "eslint-config-scratch": "^4.0.0",
+    "eslint-config-scratch": "^5.0.0",
     "eslint-plugin-import": "^2.7.0",
     "eslint-plugin-react": "^7.2.1",
+    "file-loader": "1.1.5",
     "get-float-time-domain-data": "0.1.0",
     "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
     "html-webpack-plugin": "^2.30.0",
@@ -61,36 +63,40 @@
     "lodash.pick": "4.4.0",
     "minilog": "3.1.0",
     "mkdirp": "^0.5.1",
-    "postcss-import": "^10.0.0",
+    "postcss-import": "^11.0.0",
     "postcss-loader": "^2.0.5",
     "postcss-simple-vars": "^4.0.0",
     "prop-types": "^15.5.10",
-    "react": "15.6.1",
-    "react-contextmenu": "2.7.0",
-    "react-dom": "15.6.1",
+    "raf": "^3.4.0",
+    "raw-loader": "0.5.1",
+    "react": "16.0.0",
+    "react-contextmenu": "2.8.0",
+    "react-dom": "16.0.0",
     "react-draggable": "3.0.3",
     "react-intl": "2.4.0",
     "react-intl-redux": "0.6.0",
-    "react-modal": "2.3.1",
+    "react-modal": "3.0.2",
     "react-redux": "5.0.6",
-    "react-responsive": "1.3.4",
+    "react-responsive": "3.0.0",
     "react-style-proptype": "3.0.0",
-    "react-tabs": "2.0.0",
-    "react-test-renderer": "^15.5.4",
+    "react-tabs": "2.1.0",
+    "react-test-renderer": "16.0.0",
     "redux": "3.7.0",
     "redux-mock-store": "^1.2.3",
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "latest",
     "scratch-blocks": "latest",
+    "scratch-l10n": "^2.0.0",
+    "scratch-paint": "latest",
     "scratch-render": "latest",
-    "scratch-storage": "^0.2.0",
+    "scratch-storage": "^0.3.0",
     "scratch-vm": "latest",
-    "selenium-webdriver": "^3.5.0",
+    "selenium-webdriver": "3.5.0",
     "startaudiocontext": "1.2.1",
-    "style-loader": "^0.18.0",
+    "style-loader": "^0.19.0",
     "svg-to-image": "1.1.3",
-    "svg-url-loader": "^2.1.0",
+    "text-encoding": "0.6.4",
     "wav-encoder": "1.3.0",
     "web-audio-test-api": "^0.5.2",
     "webpack": "^3.6.0",
@@ -98,6 +104,10 @@
     "xhr": "2.4.0"
   },
   "jest": {
+    "setupFiles": [
+      "raf/polyfill",
+      "<rootDir>/test/helpers/enzyme-setup.js"
+    ],
     "testPathIgnorePatterns": [
       "src/test.js"
     ],
diff --git a/scripts/generate-locale-messages.js b/scripts/generate-locale-messages.js
deleted file mode 100755
index fb6bf359a23db0c4dc583476fc18896befe8683a..0000000000000000000000000000000000000000
--- a/scripts/generate-locale-messages.js
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env node
-
-/*
-Generates locale/messages.json from current translastion files
-
-Translations are expected to be in the ./translations directory.
-Translation files are in Chrome i18n json format:
-'''
-{
-    "message.id": {
-        "message": "The translated text",
-        "description": "Tips for translators"
-    },
-    ...
-}
-'''
-They are named by locale, for example: 'fr.json' or 'zh-cn.json'
-
-Current languages supported are listed in ../src/languages.json
-
-Converts the collection of translation files to a single set of messages.
-Example output:
-'''
-{
-  "en": {
-    "action.addBackdrop": "Add Backdrop",
-    "action.addCostume": "Add Costume",
-    "action.recordSound": "Record Sound",
-    "action.addSound": "Add Sound"
-  },
-  "fr": {
-    "action.addSound": "Ajouter Son",
-    "action.addCostume": "Ajouter Costume",
-    "action.addBackdrop": "Ajouter Arrière-plan",
-    "action.recordSound": "Enregistrement du Son"
-  }
-}
-'''
-
-Missing locales are ignored, react-intl will use the default messages for them.
- */
-const fs = require('fs');
-const path = require('path');
-const mkdirp = require('mkdirp');
-
-const locales = ['en', 'es', 'fr'];
-const LANG_DIR = './translations/';
-const MSGS_DIR = './locale/';
-
-let messages = locales.reduce((collection, lang) => {
-    let langMessages = {};
-    try {
-        let langData = JSON.parse(
-            fs.readFileSync(path.resolve(LANG_DIR, lang + '.json'), 'utf8')
-        );
-        Object.keys(langData).forEach((id) => {
-            langMessages[id] = langData[id].message;
-        });
-        collection[lang] = langMessages;
-    } catch (e) {
-        process.stdout.write(lang + ' translation file missing, will use defaults.\n');
-    }
-    return collection;
-}, {});
-
-mkdirp.sync(MSGS_DIR);
-fs.writeFileSync(MSGS_DIR + 'messages.json', JSON.stringify(messages, null, 2));
diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index bb336c40d7687ced45ce96dc511fab866a20812d..9f9b84743fb3a861f3b0ba2d36fb55e9d25b28be 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -150,7 +150,7 @@
 }
 
 .extension-button-container {
-    width: 60px;
+    width: 3.25rem;
     height: 3.25rem;
     position: absolute;
     bottom: 0;
diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 2257c2ceea73eeee4e4c97d661035ec50dba5d8c..354baecdb2758cd26f59c525e042a4a9caba5e85 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -26,7 +26,7 @@ const addExtensionMessage = (
     <FormattedMessage
         defaultMessage="Extensions"
         description="Button to add an extension in the target pane"
-        id="targetPane.addExtension"
+        id="gui.gui.addExtension"
     />
 );
 
diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx
index 81f2b596d1649b45791d9088414f64187008772c..3be8abaece2178eb3a88cb8e6d8c5fcace0bba98 100644
--- a/src/components/language-selector/language-selector.jsx
+++ b/src/components/language-selector/language-selector.jsx
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import Box from '../box/box.jsx';
-import languages from '../../locale.js';
+import locales from 'scratch-l10n';
 import languageIcon from './language-icon.svg';
 import styles from './language-selector.css';
 
@@ -22,12 +22,12 @@ const LanguageSelector = ({
                 value={currentLocale}
                 onChange={onChange}
             >
-                {Object.keys(languages).map(locale => (
+                {Object.keys(locales).map(locale => (
                     <option
                         key={locale}
                         value={locale}
                     >
-                        {languages[locale].name}
+                        {locales[locale].name}
                     </option>
                 ))}
             </select>
diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css
index 067b2c1af3328a80dd5e228d810d6129f1788617..4fe6ae6a849dfedbdaa60a6c612a4060933d9390 100644
--- a/src/components/library-item/library-item.css
+++ b/src/components/library-item/library-item.css
@@ -58,3 +58,27 @@
     white-space: nowrap;
     min-width: 0;
 }
+
+.featured-item {
+    flex-basis: 300px;
+    max-width: 300px;
+    height: 320px;
+    overflow: hidden;
+    padding: 0;
+}
+
+.featured-image {
+    width: 100%;
+}
+
+.featured-text {
+    font-weight: bold;
+    padding: 10px;
+    height: 140px;
+    width: 300px;
+}
+
+.featured-description {
+    font-weight: normal;
+    line-height: 2;
+}
diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx
index 5e0fa5edccc905313fa9529ad083d9db9ec76683..0f1443e36363fbba8dbbc89c9474163ef64ac23c 100644
--- a/src/components/library-item/library-item.jsx
+++ b/src/components/library-item/library-item.jsx
@@ -4,6 +4,7 @@ import React from 'react';
 
 import Box from '../box/box.jsx';
 import styles from './library-item.css';
+import classNames from 'classnames';
 
 class LibraryItem extends React.PureComponent {
     constructor (props) {
@@ -25,7 +26,26 @@ class LibraryItem extends React.PureComponent {
         this.props.onMouseLeave(this.props.id);
     }
     render () {
-        return (
+        return this.props.featured ? (
+            <div
+                className={classNames(styles.libraryItem, styles.featuredItem)}
+                onClick={this.handleClick}
+            >
+                <div>
+                    <img
+                        className={styles.featuredImage}
+                        src={this.props.iconURL}
+                    />
+                </div>
+                <div
+                    className={styles.featuredText}
+                >
+                    <span className={styles.libraryItemName}>{this.props.name}</span>
+                    <br />
+                    <span className={styles.featuredDescription}>{this.props.description}</span>
+                </div>
+            </div>
+        ) : (
             <Box
                 className={styles.libraryItem}
                 onClick={this.handleClick}
@@ -48,6 +68,8 @@ class LibraryItem extends React.PureComponent {
 }
 
 LibraryItem.propTypes = {
+    description: PropTypes.string,
+    featured: PropTypes.bool,
     iconURL: PropTypes.string.isRequired,
     id: PropTypes.number.isRequired,
     name: PropTypes.string.isRequired,
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index 165716e5b59b6537ce453bd849024ed96bbd36c7..91ac1117b6606e4b900de26f433f5342f4ee634b 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -59,6 +59,8 @@ class LibraryComponent extends React.Component {
                             dataItem.rawURL;
                         return (
                             <LibraryItem
+                                description={dataItem.description}
+                                featured={dataItem.featured}
                                 iconURL={scratchURL}
                                 id={index}
                                 key={`item_${index}`}
diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx
index 21c2613bfa21511fc4e0aa0f3f1e616c0efe41e5..9a0e551cbac9097c8d8cc936ce6e74c6e3638f7f 100644
--- a/src/components/sound-editor/sound-editor.jsx
+++ b/src/components/sound-editor/sound-editor.jsx
@@ -30,72 +30,72 @@ const BufferedInput = BufferedInputHOC(Input);
 
 const messages = defineMessages({
     sound: {
-        id: 'soundEditor.sound',
-        description: 'Lable for the name of the sound',
+        id: 'gui.soundEditor.sound',
+        description: 'Label for the name of the sound',
         defaultMessage: 'Sound'
     },
     play: {
-        id: 'soundEditor.play',
+        id: 'gui.soundEditor.play',
         description: 'Title of the button to start playing the sound',
         defaultMessage: 'Play'
     },
     stop: {
-        id: 'soundEditor.stop',
+        id: 'gui.soundEditor.stop',
         description: 'Title of the button to stop the sound',
         defaultMessage: 'Stop'
     },
     trim: {
-        id: 'soundEditor.trim',
+        id: 'gui.soundEditor.trim',
         description: 'Title of the button to start trimminging the sound',
         defaultMessage: 'Trim'
     },
     save: {
-        id: 'soundEditor.save',
+        id: 'gui.soundEditor.save',
         description: 'Title of the button to save trimmed sound',
         defaultMessage: 'Save'
     },
     undo: {
-        id: 'soundEditor.undo',
+        id: 'gui.soundEditor.undo',
         description: 'Title of the button to undo',
         defaultMessage: 'Undo'
     },
     redo: {
-        id: 'soundEditor.redo',
+        id: 'gui.soundEditor.redo',
         description: 'Title of the button to redo',
         defaultMessage: 'Redo'
     },
     faster: {
-        id: 'soundEditor.faster',
+        id: 'gui.soundEditor.faster',
         description: 'Title of the button to apply the faster effect',
         defaultMessage: 'Faster'
     },
     slower: {
-        id: 'soundEditor.slower',
+        id: 'gui.soundEditor.slower',
         description: 'Title of the button to apply the slower effect',
         defaultMessage: 'Slower'
     },
     echo: {
-        id: 'soundEditor.echo',
+        id: 'gui.soundEditor.echo',
         description: 'Title of the button to apply the echo effect',
         defaultMessage: 'Echo'
     },
     robot: {
-        id: 'soundEditor.robot',
+        id: 'gui.soundEditor.robot',
         description: 'Title of the button to apply the robot effect',
         defaultMessage: 'Robot'
     },
     louder: {
-        id: 'soundEditor.louder',
+        id: 'gui.soundEditor.louder',
         description: 'Title of the button to apply the louder effect',
         defaultMessage: 'Louder'
     },
     softer: {
-        id: 'soundEditor.softer',
+        id: 'gui.soundEditor.softer',
         description: 'Title of the button to apply thr.softer effect',
         defaultMessage: 'Softer'
     },
     reverse: {
-        id: 'soundEditor.reverse',
+        id: 'gui.soundEditor.reverse',
         description: 'Title of the button to apply the reverse effect',
         defaultMessage: 'Reverse'
     }
diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx
index 2db6a7feacdb0b0b04babe8e07aa7bea9ecb2990..3fc6e440e74ee390e1bdaa7d9348b90e31dc0ad0 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.jsx
+++ b/src/components/sprite-selector-item/sprite-selector-item.jsx
@@ -39,11 +39,20 @@ const SpriteSelectorItem = props => (
         ) : null}
         <div className={styles.spriteName}>{props.name}</div>
         <ContextMenu id={`${props.name}-${contextMenuId++}`}>
+            {props.onDuplicateButtonClick ? (
+                <MenuItem onClick={props.onDuplicateButtonClick}>
+                    <FormattedMessage
+                        defaultMessage="duplicate"
+                        description="Menu item to duplicate in the right click menu"
+                        id="gui.spriteSelectorItem.contextMenuDuplicate"
+                    />
+                </MenuItem>
+            ) : null}
             <MenuItem onClick={props.onDeleteButtonClick}>
                 <FormattedMessage
                     defaultMessage="delete"
                     description="Menu item to delete in the right click menu"
-                    id="contextMenu.delete"
+                    id="gui.spriteSelectorItem.contextMenuDelete"
                 />
             </MenuItem>
         </ContextMenu>
@@ -55,7 +64,8 @@ SpriteSelectorItem.propTypes = {
     costumeURL: PropTypes.string,
     name: PropTypes.string.isRequired,
     onClick: PropTypes.func,
-    onDeleteButtonClick: PropTypes.func,
+    onDeleteButtonClick: PropTypes.func.isRequired,
+    onDuplicateButtonClick: PropTypes.func,
     selected: PropTypes.bool.isRequired
 };
 
diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
index 399d8062b4253cac9f9c77bc1a31abac0e0522f1..0110bea1ff97619027f0236799bfe35fb9351325 100644
--- a/src/components/sprite-selector/sprite-selector.jsx
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -14,7 +14,7 @@ const addSpriteMessage = (
     <FormattedMessage
         defaultMessage="Add Sprite"
         description="Button to add a sprite in the target pane"
-        id="targetPane.addSprite"
+        id="gui.spriteSelector.addSprite"
     />
 );
 
@@ -27,6 +27,7 @@ const SpriteSelectorComponent = function (props) {
         onChangeSpriteX,
         onChangeSpriteY,
         onDeleteSprite,
+        onDuplicateSprite,
         onNewSpriteClick,
         onSelectSprite,
         selectedId,
@@ -77,6 +78,7 @@ const SpriteSelectorComponent = function (props) {
                                 selected={sprite.id === selectedId}
                                 onClick={onSelectSprite}
                                 onDeleteButtonClick={onDeleteSprite}
+                                onDuplicateButtonClick={onDuplicateSprite}
                             />
                         ))
                     }
@@ -100,6 +102,7 @@ SpriteSelectorComponent.propTypes = {
     onChangeSpriteX: PropTypes.func,
     onChangeSpriteY: PropTypes.func,
     onDeleteSprite: PropTypes.func,
+    onDuplicateSprite: PropTypes.func,
     onNewSpriteClick: PropTypes.func,
     onSelectSprite: PropTypes.func,
     selectedId: PropTypes.string,
diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx
index 93002f9efb461e2b141104024711df8f5bc0b625..5ffad63e57a8701a11f2bcde648fb45fa97587a2 100644
--- a/src/components/stage-selector/stage-selector.jsx
+++ b/src/components/stage-selector/stage-selector.jsx
@@ -13,7 +13,7 @@ const addBackdropMessage = (
     <FormattedMessage
         defaultMessage="Add Backdrop"
         description="Button to add a backdrop in the target pane"
-        id="targetPane.addBackdrop"
+        id="gui.stageSelector.targetPaneAddBackdrop"
     />
 );
 
@@ -49,7 +49,7 @@ const StageSelector = props => {
                 <FormattedMessage
                     defaultMessage="Backdrops"
                     description="Label for the backdrops in the stage selector"
-                    id="stageSelector.backdrops"
+                    id="gui.stageSelector.backdrops"
                 />
             </div>
             <div className={styles.count}>{backdropCount}</div>
diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css
index f13554a027d46eabb3371396e29a6ff59b2cd478..9c784e9560d0ba06f371ea06941c29b627963d53 100644
--- a/src/components/stage/stage.css
+++ b/src/components/stage/stage.css
@@ -1,4 +1,5 @@
 @import "../../css/units.css";
+@import "../../css/colors.css";
 
 .stage {
     /*
@@ -7,8 +8,6 @@
     */
     display: block;
 
-    border-radius: $space;
-
     /* @todo: This is for overriding the value being set somewhere. Where is it being set? */
     background-color: transparent;
 }
@@ -31,6 +30,10 @@
 
 .stage-wrapper {
     position: relative;
+    border-radius: $space;
+    border: 1px solid $ui-pane-border;
+    /* Keep the canvas inside the border radius */
+    overflow: hidden;
 }
 
 .monitor-wrapper, .color-picker-wrapper {
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index 07281756df4729331ef5b7429ec9c26238404d3b..2f147447d13280cbc63dd825840bb4f33b955708 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -7,7 +7,6 @@ import BackdropLibrary from '../../containers/backdrop-library.jsx';
 import CostumeLibrary from '../../containers/costume-library.jsx';
 import SoundLibrary from '../../containers/sound-library.jsx';
 import SpriteLibrary from '../../containers/sprite-library.jsx';
-import ExtensionLibrary from '../../containers/extension-library.jsx';
 
 import SpriteSelectorComponent from '../sprite-selector/sprite-selector.jsx';
 import StageSelector from '../../containers/stage-selector.jsx';
@@ -22,7 +21,6 @@ import styles from './target-pane.css';
  */
 const TargetPane = ({
     editingTarget,
-    extensionLibraryVisible,
     backdropLibraryVisible,
     costumeLibraryVisible,
     soundLibraryVisible,
@@ -34,12 +32,12 @@ const TargetPane = ({
     onChangeSpriteX,
     onChangeSpriteY,
     onDeleteSprite,
+    onDuplicateSprite,
     onNewSpriteClick,
     onRequestCloseBackdropLibrary,
     onRequestCloseCostumeLibrary,
     onRequestCloseSoundLibrary,
     onRequestCloseSpriteLibrary,
-    onRequestCloseExtensionLibrary,
     onSelectSprite,
     stage,
     sprites,
@@ -61,6 +59,7 @@ const TargetPane = ({
             onChangeSpriteX={onChangeSpriteX}
             onChangeSpriteY={onChangeSpriteY}
             onDeleteSprite={onDeleteSprite}
+            onDuplicateSprite={onDuplicateSprite}
             onNewSpriteClick={onNewSpriteClick}
             onSelectSprite={onSelectSprite}
         />
@@ -76,12 +75,6 @@ const TargetPane = ({
                 onSelect={onSelectSprite}
             />}
             <div>
-                {extensionLibraryVisible ? (
-                    <ExtensionLibrary
-                        vm={vm}
-                        onRequestClose={onRequestCloseExtensionLibrary}
-                    />
-                ) : null}
                 {spriteLibraryVisible ? (
                     <SpriteLibrary
                         vm={vm}
@@ -141,6 +134,7 @@ TargetPane.propTypes = {
     onChangeSpriteX: PropTypes.func,
     onChangeSpriteY: PropTypes.func,
     onDeleteSprite: PropTypes.func,
+    onDuplicateSprite: PropTypes.func,
     onNewSpriteClick: PropTypes.func,
     onRequestCloseBackdropLibrary: PropTypes.func,
     onRequestCloseCostumeLibrary: PropTypes.func,
diff --git a/src/components/turbo-mode/turbo-mode.jsx b/src/components/turbo-mode/turbo-mode.jsx
index 724af24d1e1527c2acb79de0df0083d3ef1de4e7..4475b86d6a3bca7816beaa35bdfb667a725a07ec 100644
--- a/src/components/turbo-mode/turbo-mode.jsx
+++ b/src/components/turbo-mode/turbo-mode.jsx
@@ -15,7 +15,7 @@ const TurboMode = () => (
             <FormattedMessage
                 defaultMessage="Turbo Mode"
                 description="Label indicating turbo mode is active"
-                id="controls.turboMode"
+                id="gui.turboMode.active"
             />
         </div>
     </div>
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 7a2a4d908f70d51656744fd9e6a218722ec6b91d..471dc6cd08643341c290f66227b723ef28dbffb1 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -8,10 +8,12 @@ import VMScratchBlocks from '../lib/blocks';
 import VM from 'scratch-vm';
 import Prompt from './prompt.jsx';
 import BlocksComponent from '../components/blocks/blocks.jsx';
+import ExtensionLibrary from './extension-library.jsx';
 
 import {connect} from 'react-redux';
 import {updateToolbox} from '../reducers/toolbox';
 import {activateColorPicker} from '../reducers/color-picker';
+import {closeExtensionLibrary} from '../reducers/modals';
 
 const addFunctionListener = (object, property, callback) => {
     const oldFn = object[property];
@@ -29,6 +31,7 @@ class Blocks extends React.Component {
         bindAll(this, [
             'attachVM',
             'detachVM',
+            'handleCategorySelected',
             'handlePromptStart',
             'handlePromptCallback',
             'handlePromptClose',
@@ -53,12 +56,13 @@ class Blocks extends React.Component {
     componentDidMount () {
         this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
 
-        const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
+        const workspaceConfig = defaultsDeep({},
+            Blocks.defaultOptions,
+            this.props.options,
+            {toolbox: this.props.toolboxXML}
+        );
         this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
 
-        // Load the toolbox from the GUI (otherwise we get the scratch-blocks default toolbox)
-        this.workspace.updateToolbox(this.props.toolboxXML);
-
         // @todo change this when blockly supports UI events
         addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange);
         addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange);
@@ -69,28 +73,25 @@ class Blocks extends React.Component {
         return (
             this.state.prompt !== nextState.prompt ||
             this.props.isVisible !== nextProps.isVisible ||
-            this.props.toolboxXML !== nextProps.toolboxXML
+            this.props.toolboxXML !== nextProps.toolboxXML ||
+            this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible
         );
     }
     componentDidUpdate (prevProps) {
         if (prevProps.toolboxXML !== this.props.toolboxXML) {
             const selectedCategoryName = this.workspace.toolbox_.getSelectedItem().name_;
             this.workspace.updateToolbox(this.props.toolboxXML);
-            // Blockly throws if we don't select a category after updating the toolbox.
-            /** @TODO Find a way to avoid the exception without accessing private properties. */
-            this.setToolboxSelectedItemByName(selectedCategoryName);
+            this.workspace.toolbox_.setSelectedCategoryByName(selectedCategoryName);
         }
         if (this.props.isVisible === prevProps.isVisible) {
             return;
         }
-
         // @todo hack to resize blockly manually in case resize happened while hidden
         // @todo hack to reload the workspace due to gui bug #413
         if (this.props.isVisible) { // Scripts tab
             this.workspace.setVisible(true);
             this.props.vm.refreshWorkspace();
             window.dispatchEvent(new Event('resize'));
-            this.workspace.toolbox_.refreshSelection();
         } else {
             this.workspace.setVisible(false);
         }
@@ -99,20 +100,6 @@ class Blocks extends React.Component {
         this.detachVM();
         this.workspace.dispose();
     }
-    /**
-     * Select a particular category in the toolbox by specifying the category name.
-     * This is a workaround for a bug: @see {@link componentDidUpdate} above.
-     * @TODO Remove this or reimplement using only public APIs.
-     * @param {string} name - the name of the category to select.
-     */
-    setToolboxSelectedItemByName (name) {
-        const categories = this.workspace.toolbox_.categoryMenu_.categories_;
-        for (let i = 0; i < categories.length; i++) {
-            if (categories[i].name_ === name) {
-                this.workspace.toolbox_.setSelectedItem(categories[i]);
-            }
-        }
-    }
     attachVM () {
         this.workspace.addChangeListener(this.props.vm.blockListener);
         this.flyoutWorkspace = this.workspace
@@ -189,13 +176,11 @@ class Blocks extends React.Component {
             this.onWorkspaceMetricsChange();
         }
 
-        this.ScratchBlocks.Events.disable();
-        this.workspace.clear();
-
+        // Remove and reattach the workspace listener (but allow flyout events)
+        this.workspace.removeChangeListener(this.props.vm.blockListener);
         const dom = this.ScratchBlocks.Xml.textToDom(data.xml);
-        this.ScratchBlocks.Xml.domToWorkspace(dom, this.workspace);
-        this.ScratchBlocks.Events.enable();
-        this.workspace.toolbox_.refreshSelection();
+        this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace);
+        this.workspace.addChangeListener(this.props.vm.blockListener);
 
         if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
             const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id];
@@ -211,6 +196,9 @@ class Blocks extends React.Component {
         const toolboxXML = makeToolboxXML(dynamicBlocksXML);
         this.props.onExtensionAdded(toolboxXML);
     }
+    handleCategorySelected (categoryName) {
+        this.workspace.toolbox_.setSelectedCategoryByName(categoryName);
+    }
     setBlocks (blocks) {
         this.blocks = blocks;
     }
@@ -227,11 +215,13 @@ class Blocks extends React.Component {
     render () {
         /* eslint-disable no-unused-vars */
         const {
+            extensionLibraryVisible,
             options,
             vm,
             isVisible,
             onActivateColorPicker,
             onExtensionAdded,
+            onRequestCloseExtensionLibrary,
             toolboxXML,
             ...props
         } = this.props;
@@ -251,15 +241,24 @@ class Blocks extends React.Component {
                         onOk={this.handlePromptCallback}
                     />
                 ) : null}
+                {extensionLibraryVisible ? (
+                    <ExtensionLibrary
+                        vm={vm}
+                        onCategorySelected={this.handleCategorySelected}
+                        onRequestClose={onRequestCloseExtensionLibrary}
+                    />
+                ) : null}
             </div>
         );
     }
 }
 
 Blocks.propTypes = {
+    extensionLibraryVisible: PropTypes.bool,
     isVisible: PropTypes.bool,
     onActivateColorPicker: PropTypes.func,
     onExtensionAdded: PropTypes.func,
+    onRequestCloseExtensionLibrary: PropTypes.func,
     options: PropTypes.shape({
         media: PropTypes.string,
         zoom: PropTypes.shape({
@@ -317,6 +316,7 @@ Blocks.defaultProps = {
 };
 
 const mapStateToProps = state => ({
+    extensionLibraryVisible: state.modals.extensionLibrary,
     toolboxXML: state.toolbox.toolboxXML
 });
 
@@ -324,6 +324,9 @@ const mapDispatchToProps = dispatch => ({
     onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
     onExtensionAdded: toolboxXML => {
         dispatch(updateToolbox(toolboxXML));
+    },
+    onRequestCloseExtensionLibrary: () => {
+        dispatch(closeExtensionLibrary());
     }
 });
 
diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx
index 594f1a7b94c9c74d46517f4ae289ba3b13962acd..d87d4964a90373f1da2789861a08fb6480ce516e 100644
--- a/src/containers/costume-tab.jsx
+++ b/src/containers/costume-tab.jsx
@@ -6,6 +6,7 @@ import VM from 'scratch-vm';
 
 import AssetPanel from '../components/asset-panel/asset-panel.jsx';
 import addCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
+import PaintEditorWrapper from './paint-editor-wrapper.jsx';
 
 import {connect} from 'react-redux';
 
@@ -47,14 +48,19 @@ class CostumeTab extends React.Component {
     }
 
     render () {
+        // For paint wrapper
         const {
-            editingTarget,
-            sprites,
-            stage,
+            onNewBackdropClick,
             onNewCostumeClick,
-            onNewBackdropClick
+            ...props
         } = this.props;
 
+        const {
+            editingTarget,
+            sprites,
+            stage
+        } = props;
+
         const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
 
         if (!target) {
@@ -65,14 +71,14 @@ class CostumeTab extends React.Component {
             <FormattedMessage
                 defaultMessage="Add Backdrop"
                 description="Button to add a backdrop in the editor tab"
-                id="action.addBackdrop"
+                id="gui.costumeTab.addBackdrop"
             />
         );
         const addCostumeMsg = (
             <FormattedMessage
                 defaultMessage="Add Costume"
                 description="Button to add a costume in the editor tab"
-                id="action.addCostume"
+                id="gui.costumeTab.addCostume"
             />
         );
 
@@ -90,7 +96,15 @@ class CostumeTab extends React.Component {
                 selectedItemIndex={this.state.selectedCostumeIndex}
                 onDeleteClick={this.handleDeleteCostume}
                 onItemClick={this.handleSelectCostume}
-            />
+            >
+                {target.costumes ?
+                    <PaintEditorWrapper
+                        {...props}
+                        selectedCostumeIndex={this.state.selectedCostumeIndex}
+                    /> :
+                    null
+                }
+            </AssetPanel>
         );
     }
 }
diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx
index 343d0747d8a273c9eaca2d5bfbb86dfee5fa2284..09fd65ff35d9f3af78019f622fa0ed3d58b19d0b 100644
--- a/src/containers/extension-library.jsx
+++ b/src/containers/extension-library.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import VM from 'scratch-vm';
 
-import extensionLibraryContent from '../lib/libraries/extensions.json';
+import extensionLibraryContent from '../lib/libraries/extensions/index';
 
 import LibraryComponent from '../components/library/library.jsx';
 import extensionIcon from '../components/sprite-selector/icon--sprite.svg';
@@ -19,7 +19,13 @@ class ExtensionLibrary extends React.PureComponent {
         // eslint-disable-next-line no-alert
         const url = item.extensionURL || prompt('Enter the URL of the extension');
         if (url) {
-            this.props.vm.extensionManager.loadExtensionURL(url);
+            if (this.props.vm.extensionManager.isExtensionLoaded(url)) {
+                this.props.onCategorySelected(item.name);
+            } else {
+                this.props.vm.extensionManager.loadExtensionURL(url).then(() => {
+                    this.props.onCategorySelected(item.name);
+                });
+            }
         }
     }
     render () {
@@ -40,6 +46,7 @@ class ExtensionLibrary extends React.PureComponent {
 }
 
 ExtensionLibrary.propTypes = {
+    onCategorySelected: PropTypes.func,
     onRequestClose: PropTypes.func,
     visible: PropTypes.bool,
     vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types
diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..6448f3e9e2b8901f392517728ccac4f8bdc74190
--- /dev/null
+++ b/src/containers/paint-editor-wrapper.jsx
@@ -0,0 +1,64 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import VM from 'scratch-vm';
+
+import PaintEditor from 'scratch-paint';
+
+import {connect} from 'react-redux';
+
+class PaintEditorWrapper extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleUpdateName',
+            'handleUpdateSvg'
+        ]);
+    }
+    handleUpdateName (name) {
+        this.props.vm.renameCostume(this.props.selectedCostumeIndex, name);
+    }
+    handleUpdateSvg (svg, rotationCenterX, rotationCenterY) {
+        this.props.vm.updateSvg(this.props.selectedCostumeIndex, svg, rotationCenterX, rotationCenterY);
+    }
+    render () {
+        if (!this.props.svgId) return null;
+        return (
+            <PaintEditor
+                {...this.props}
+                svg={this.props.vm.getCostumeSvg(this.props.selectedCostumeIndex)}
+                onUpdateName={this.handleUpdateName}
+                onUpdateSvg={this.handleUpdateSvg}
+            />
+        );
+    }
+}
+
+PaintEditorWrapper.propTypes = {
+    name: PropTypes.string,
+    rotationCenterX: PropTypes.number,
+    rotationCenterY: PropTypes.number,
+    selectedCostumeIndex: PropTypes.number.isRequired,
+    svgId: PropTypes.string,
+    vm: PropTypes.instanceOf(VM)
+};
+
+const mapStateToProps = (state, {selectedCostumeIndex}) => {
+    const {
+        editingTarget,
+        sprites,
+        stage
+    } = state.targets;
+    const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
+    const costume = target && target.costumes[selectedCostumeIndex];
+    return {
+        name: costume && costume.name,
+        rotationCenterX: costume && costume.rotationCenterX,
+        rotationCenterY: costume && costume.rotationCenterY,
+        svgId: editingTarget && `${editingTarget}${costume.skinId}`
+    };
+};
+
+export default connect(
+    mapStateToProps
+)(PaintEditorWrapper);
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 3edde50a01c17d553497fa4af460427f8e5e5182..1984923d541427b16a82980d1c6bc627f4541413 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -76,14 +76,14 @@ class SoundTab extends React.Component {
             <FormattedMessage
                 defaultMessage="Record Sound"
                 description="Button to record a sound in the editor tab"
-                id="action.recordSound"
+                id="gui.soundTab.recordSound"
             />
         );
         const addSoundMsg = (
             <FormattedMessage
                 defaultMessage="Add Sound"
                 description="Button to add a sound in the editor tab"
-                id="action.addSound"
+                id="gui.soundTab.addSound"
             />
         );
 
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index aa19efc1f6bb6f2180501fd378f10766de01932b..0bc01382cac731a6bcef3b52a16fe4fb4c2d757b 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -11,7 +11,8 @@ class SpriteSelectorItem extends React.Component {
         super(props);
         bindAll(this, [
             'handleClick',
-            'handleDelete'
+            'handleDelete',
+            'handleDuplicate'
         ]);
     }
     handleClick (e) {
@@ -24,6 +25,10 @@ class SpriteSelectorItem extends React.Component {
             this.props.onDeleteButtonClick(this.props.id);
         }
     }
+    handleDuplicate (e) {
+        e.stopPropagation(); // To prevent from bubbling back to handleClick
+        this.props.onDuplicateButtonClick(this.props.id);
+    }
     render () {
         const {
             /* eslint-disable no-unused-vars */
@@ -31,6 +36,7 @@ class SpriteSelectorItem extends React.Component {
             id,
             onClick,
             onDeleteButtonClick,
+            onDuplicateButtonClick,
             /* eslint-enable no-unused-vars */
             ...props
         } = this.props;
@@ -38,6 +44,7 @@ class SpriteSelectorItem extends React.Component {
             <SpriteSelectorItemComponent
                 onClick={this.handleClick}
                 onDeleteButtonClick={this.handleDelete}
+                onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
                 {...props}
             />
         );
@@ -50,7 +57,8 @@ SpriteSelectorItem.propTypes = {
     id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     name: PropTypes.string,
     onClick: PropTypes.func,
-    onDeleteButtonClick: PropTypes.func,
+    onDeleteButtonClick: PropTypes.func.isRequired,
+    onDuplicateButtonClick: PropTypes.func,
     selected: PropTypes.bool
 };
 
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index 6d3d58211c4bda939c12a687d42ab3ef8c77f3a0..1de93d23d36bbc49ad6a3930c3520a6041af96d3 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -179,14 +179,16 @@ class Stage extends React.Component {
         this.updateRect();
         const {x, y} = getEventXY(e);
         const mousePosition = [x - this.rect.left, y - this.rect.top];
-        this.setState({
-            mouseDown: true,
-            mouseDownPosition: mousePosition,
-            mouseDownTimeoutId: setTimeout(
-                this.onStartDrag.bind(this, mousePosition[0], mousePosition[1]),
-                500
-            )
-        });
+        if (e.button === 0) {
+            this.setState({
+                mouseDown: true,
+                mouseDownPosition: mousePosition,
+                mouseDownTimeoutId: setTimeout(
+                    this.onStartDrag.bind(this, mousePosition[0], mousePosition[1]),
+                    500
+                )
+            });
+        }
         const data = {
             isDown: true,
             x: mousePosition[0],
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index 0ec1c43ff16e053aa19509f34bc2aa1e1469aa0e..ba0438fae049144130cbcae35e4e8abf7fbdee3b 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -7,7 +7,6 @@ import {
     openSpriteLibrary,
     closeBackdropLibrary,
     closeCostumeLibrary,
-    closeExtensionLibrary,
     closeSoundLibrary,
     closeSpriteLibrary
 } from '../reducers/modals';
@@ -25,6 +24,7 @@ class TargetPane extends React.Component {
             'handleChangeSpriteX',
             'handleChangeSpriteY',
             'handleDeleteSprite',
+            'handleDuplicateSprite',
             'handleSelectSprite'
         ]);
     }
@@ -49,6 +49,9 @@ class TargetPane extends React.Component {
     handleDeleteSprite (id) {
         this.props.vm.deleteSprite(id);
     }
+    handleDuplicateSprite (id) {
+        this.props.vm.duplicateSprite(id);
+    }
     handleSelectSprite (id) {
         this.props.vm.setEditingTarget(id);
     }
@@ -63,6 +66,7 @@ class TargetPane extends React.Component {
                 onChangeSpriteX={this.handleChangeSpriteX}
                 onChangeSpriteY={this.handleChangeSpriteY}
                 onDeleteSprite={this.handleDeleteSprite}
+                onDuplicateSprite={this.handleDuplicateSprite}
                 onSelectSprite={this.handleSelectSprite}
             />
         );
@@ -92,8 +96,7 @@ const mapStateToProps = state => ({
     soundLibraryVisible: state.modals.soundLibrary,
     spriteLibraryVisible: state.modals.spriteLibrary,
     costumeLibraryVisible: state.modals.costumeLibrary,
-    backdropLibraryVisible: state.modals.backdropLibrary,
-    extensionLibraryVisible: state.modals.extensionLibrary
+    backdropLibraryVisible: state.modals.backdropLibrary
 });
 const mapDispatchToProps = dispatch => ({
     onNewSpriteClick: e => {
@@ -106,9 +109,6 @@ const mapDispatchToProps = dispatch => ({
     onRequestCloseCostumeLibrary: () => {
         dispatch(closeCostumeLibrary());
     },
-    onRequestCloseExtensionLibrary: () => {
-        dispatch(closeExtensionLibrary());
-    },
     onRequestCloseSoundLibrary: () => {
         dispatch(closeSoundLibrary());
     },
diff --git a/src/examples/compatibility-testing.jsx b/src/examples/compatibility-testing.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3fa8e71d94e34e4722ec1a1f329c9df46de2688c
--- /dev/null
+++ b/src/examples/compatibility-testing.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {connect} from 'react-redux';
+
+import AppStateHOC from '../lib/app-state-hoc.jsx';
+import Controls from '../containers/controls.jsx';
+import Stage from '../containers/stage.jsx';
+import Box from '../components/box/box.jsx';
+import GUI from '../containers/gui.jsx';
+import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
+
+const mapStateToProps = state => ({vm: state.vm});
+
+const VMStage = connect(mapStateToProps)(Stage);
+const VMControls = connect(mapStateToProps)(Controls);
+
+const DEFAULT_PROJECT_ID = '10015059';
+
+class Player extends React.Component {
+    constructor (props) {
+        super(props);
+        this.updateProject = this.updateProject.bind(this);
+
+        this.state = {
+            projectId: window.location.hash.substring(1) || DEFAULT_PROJECT_ID
+        };
+    }
+    componentDidMount () {
+        window.addEventListener('hashchange', this.updateProject);
+        if (!window.location.hash.substring(1)) {
+            window.location.hash = DEFAULT_PROJECT_ID;
+        }
+    }
+    componentWillUnmount () {
+        window.addEventListener('hashchange', this.updateProject);
+    }
+    updateProject () {
+        this.setState({projectId: window.location.hash.substring(1)});
+    }
+    render () {
+        const width = 480;
+        const height = 360;
+        return (
+            <div style={{display: 'flex'}}>
+                <GUI
+                    {...this.props}
+                    width={width}
+                >
+                    <Box height={40}>
+                        <VMControls
+                            style={{
+                                marginRight: 10,
+                                height: 40
+                            }}
+                        />
+                    </Box>
+                    <VMStage
+                        height={height}
+                        width={width}
+                    />
+                </GUI>
+                <iframe
+                    allowFullScreen
+                    allowTransparency
+                    frameBorder="0"
+                    height="402"
+                    src={`https://scratch.mit.edu/projects/embed/${this.state.projectId}/?autostart=true`}
+                    width="485"
+                />
+            </div>
+        );
+    }
+}
+
+const App = AppStateHOC(ProjectLoaderHOC(Player));
+
+const appTarget = document.createElement('div');
+document.body.appendChild(appTarget);
+
+ReactDOM.render(<App />, appTarget);
diff --git a/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg
new file mode 100755
index 0000000000000000000000000000000000000000..d449b3d15b955647f2198116743d0f4df619b24b
Binary files /dev/null and b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg differ
diff --git a/src/lib/default-project/3696356a03a8d938318876a593572843.svg b/src/lib/default-project/3696356a03a8d938318876a593572843.svg
new file mode 100755
index 0000000000000000000000000000000000000000..0ecb2de81d9e76af3920bab9afd30cec1fa9f8d6
Binary files /dev/null and b/src/lib/default-project/3696356a03a8d938318876a593572843.svg differ
diff --git a/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png b/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png
new file mode 100755
index 0000000000000000000000000000000000000000..da373d2cf3ab7c4c617d307eec5a044ae43917ee
Binary files /dev/null and b/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png differ
diff --git a/src/lib/default-project/739b5e2a2435f6e1ec2993791b423146.png b/src/lib/default-project/739b5e2a2435f6e1ec2993791b423146.png
new file mode 100755
index 0000000000000000000000000000000000000000..b395ac94c246fd4de8368a156f92010baa2abe21
Binary files /dev/null and b/src/lib/default-project/739b5e2a2435f6e1ec2993791b423146.png differ
diff --git a/src/lib/default-project/83a9787d4cb6f3b7632b4ddfebf74367.wav b/src/lib/default-project/83a9787d4cb6f3b7632b4ddfebf74367.wav
new file mode 100755
index 0000000000000000000000000000000000000000..fc3b2724a9c7cfef378eeb65499d44236ad2add8
Binary files /dev/null and b/src/lib/default-project/83a9787d4cb6f3b7632b4ddfebf74367.wav differ
diff --git a/src/lib/default-project/83c36d806dc92327b9e7049a565c6bff.wav b/src/lib/default-project/83c36d806dc92327b9e7049a565c6bff.wav
new file mode 100755
index 0000000000000000000000000000000000000000..45742d5ef6f09d05b0f0788cb055ffe54abfd9ad
Binary files /dev/null and b/src/lib/default-project/83c36d806dc92327b9e7049a565c6bff.wav differ
diff --git a/src/lib/default-project/index.js b/src/lib/default-project/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc1ace99984c8fc8982f59dcba5fdcb3f3bb7386
--- /dev/null
+++ b/src/lib/default-project/index.js
@@ -0,0 +1,49 @@
+import {TextEncoder} from 'text-encoding';
+import projectJson from './project.json';
+
+/* eslint-disable import/no-unresolved */
+import popWav from '!buffer-loader!./83a9787d4cb6f3b7632b4ddfebf74367.wav';
+import meowWav from '!buffer-loader!./83c36d806dc92327b9e7049a565c6bff.wav';
+import backdrop from '!buffer-loader!./739b5e2a2435f6e1ec2993791b423146.png';
+import penLayer from '!buffer-loader!./5c81a336fab8be57adc039a8a2b33ca9.png';
+import costume1 from '!raw-loader!./09dc888b0b7df19f70d81588ae73420e.svg';
+import costume2 from '!raw-loader!./3696356a03a8d938318876a593572843.svg';
+/* eslint-enable import/no-unresolved */
+
+const encoder = new TextEncoder();
+export default [{
+    id: 0,
+    assetType: 'Project',
+    dataFormat: 'JSON',
+    data: JSON.stringify(projectJson)
+}, {
+    id: '83a9787d4cb6f3b7632b4ddfebf74367',
+    assetType: 'Sound',
+    dataFormat: 'WAV',
+    data: popWav
+}, {
+    id: '83c36d806dc92327b9e7049a565c6bff',
+    assetType: 'Sound',
+    dataFormat: 'WAV',
+    data: meowWav
+}, {
+    id: '739b5e2a2435f6e1ec2993791b423146',
+    assetType: 'ImageBitmap',
+    dataFormat: 'PNG',
+    data: backdrop
+}, {
+    id: '5c81a336fab8be57adc039a8a2b33ca9',
+    assetType: 'ImageBitmap',
+    dataFormat: 'PNG',
+    data: penLayer
+}, {
+    id: '09dc888b0b7df19f70d81588ae73420e',
+    assetType: 'ImageVector',
+    dataFormat: 'SVG',
+    data: encoder.encode(costume1)
+}, {
+    id: '3696356a03a8d938318876a593572843',
+    assetType: 'ImageVector',
+    dataFormat: 'SVG',
+    data: encoder.encode(costume2)
+}];
diff --git a/src/lib/empty-project.json b/src/lib/default-project/project.json
similarity index 100%
rename from src/lib/empty-project.json
rename to src/lib/default-project/project.json
diff --git a/src/lib/libraries/extensions.json b/src/lib/libraries/extensions.json
deleted file mode 100644
index d23bb965a1876e8029af5f9a6593f09ab565b109..0000000000000000000000000000000000000000
--- a/src/lib/libraries/extensions.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
-    {
-        "name": "Some Blocks",
-        "extensionURL": "static/extensions/example-extension.js",
-        "md5": "76f99773dd4eb59f86cd11bba6ad3cb9.svg"
-    }
-]
diff --git a/src/lib/libraries/extensions/index.js b/src/lib/libraries/extensions/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a03a896bba145c04175765e3b2b7ad9fa41557f
--- /dev/null
+++ b/src/lib/libraries/extensions/index.js
@@ -0,0 +1,19 @@
+import penImage from './pen.png';
+import wedoImage from './wedo.png';
+
+export default [
+    {
+        name: 'Pen',
+        extensionURL: 'pen',
+        iconURL: penImage,
+        description: 'Draw with your sprites.',
+        featured: true
+    },
+    {
+        name: 'Lego WeDo 2.0',
+        extensionURL: 'wedo2',
+        iconURL: wedoImage,
+        description: 'Build with motors and sensors.',
+        featured: true
+    }
+];
diff --git a/src/lib/libraries/extensions/pen.png b/src/lib/libraries/extensions/pen.png
new file mode 100644
index 0000000000000000000000000000000000000000..f02064c425e89cbac6962b36ee0b90e50d145e03
Binary files /dev/null and b/src/lib/libraries/extensions/pen.png differ
diff --git a/src/lib/libraries/extensions/wedo.png b/src/lib/libraries/extensions/wedo.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8519256752af7d5cbbbe86b74df1cce7fe5614d
Binary files /dev/null and b/src/lib/libraries/extensions/wedo.png differ
diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 7561bb2b04bdbfcbc3f709d7fb564b84ad9e8543..b186ba8e45041d41bec540b603124b6b5977cd7e 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -1,66 +1,6 @@
-const separator = '<sep gap="45"/>';
+const categorySeparator = '<sep gap="36"/>';
 
-const top = `
-    <category name="Top" colour="#FFFFFF" secondaryColour="#CCCCCC">
-        <block type="event_whenflagclicked"/>
-        <block type="event_whenkeypressed">
-        </block>
-        <block type="event_whenthisspriteclicked"/>
-        <block type="motion_movesteps">
-            <value name="STEPS">
-                <shadow type="math_number">
-                    <field name="NUM">10</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="motion_turnright">
-            <value name="DEGREES">
-                <shadow type="math_number">
-                    <field name="NUM">15</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="motion_ifonedgebounce"/>
-        <block type="sound_playuntildone">
-            <value name="SOUND_MENU">
-                <shadow type="sound_sounds_menu"/>
-            </value>
-        </block>
-        <block type="looks_changeeffectby">
-            <value name="CHANGE">
-                <shadow type="math_number">
-                    <field name="NUM">10</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="control_repeat">
-            <value name="TIMES">
-                <shadow type="math_whole_number">
-                    <field name="NUM">10</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="control_wait">
-            <value name="DURATION">
-                <shadow type="math_positive_number">
-                    <field name="NUM">1</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="operator_random">
-            <value name="FROM">
-                <shadow type="math_number">
-                    <field name="NUM">1</field>
-                </shadow>
-            </value>
-            <value name="TO">
-                <shadow type="math_number">
-                    <field name="NUM">10</field>
-                </shadow>
-            </value>
-        </block>
-    </category>
-`;
+const blockSeparator = '<sep gap="36"/>'; // At default scale, about 28px
 
 const motion = `
     <category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
@@ -85,6 +25,7 @@ const motion = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="motion_pointindirection">
             <value name="DIRECTION">
                 <shadow type="math_angle">
@@ -98,14 +39,15 @@ const motion = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="motion_gotoxy">
             <value name="X">
-                <shadow type="math_number">
+                <shadow id="movex" type="math_number">
                     <field name="NUM">0</field>
                 </shadow>
             </value>
             <value name="Y">
-                <shadow type="math_number">
+                <shadow id="movey" type="math_number">
                     <field name="NUM">0</field>
                 </shadow>
             </value>
@@ -123,12 +65,12 @@ const motion = `
                 </shadow>
             </value>
             <value name="X">
-                <shadow type="math_number">
+                <shadow id="glidex" type="math_number">
                     <field name="NUM">0</field>
                 </shadow>
             </value>
             <value name="Y">
-                <shadow type="math_number">
+                <shadow id="glidey" type="math_number">
                     <field name="NUM">0</field>
                 </shadow>
             </value>
@@ -144,6 +86,7 @@ const motion = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="motion_changexby">
             <value name="DX">
                 <shadow type="math_number">
@@ -153,7 +96,7 @@ const motion = `
         </block>
         <block type="motion_setx">
             <value name="X">
-                <shadow type="math_number">
+                <shadow id="setx" type="math_number">
                     <field name="NUM">0</field>
                 </shadow>
             </value>
@@ -167,23 +110,67 @@ const motion = `
         </block>
         <block type="motion_sety">
             <value name="Y">
-                <shadow type="math_number">
+                <shadow id="sety" type="math_number">
                     <field name="NUM">0</field>
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="motion_ifonedgebounce"/>
+        ${blockSeparator}
         <block type="motion_setrotationstyle"/>
-        <block type="motion_xposition"/>
-        <block type="motion_yposition"/>
-        <block type="motion_direction"/>
+        ${blockSeparator}
+        <block id="xposition" type="motion_xposition"/>
+        <block id="yposition" type="motion_yposition"/>
+        <block id="direction" type="motion_direction"/>
+        ${categorySeparator}
     </category>
 `;
 
 const looks = `
     <category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
+        <block type="looks_sayforsecs">
+            <value name="MESSAGE">
+                <shadow type="text">
+                    <field name="TEXT">Hello!</field>
+                </shadow>
+            </value>
+            <value name="SECS">
+                <shadow type="math_number">
+                    <field name="NUM">2</field>
+                </shadow>
+            </value>
+        </block>
+        <block type="looks_say">
+            <value name="MESSAGE">
+                <shadow type="text">
+                    <field name="TEXT">Hello!</field>
+                </shadow>
+            </value>
+        </block>
+        <block type="looks_thinkforsecs">
+            <value name="MESSAGE">
+                <shadow type="text">
+                    <field name="TEXT">Hmm...</field>
+                </shadow>
+            </value>
+            <value name="SECS">
+                <shadow type="math_number">
+                    <field name="NUM">2</field>
+                </shadow>
+            </value>
+        </block>
+        <block type="looks_think">
+            <value name="MESSAGE">
+                <shadow type="text">
+                    <field name="TEXT">Hmm...</field>
+                </shadow>
+            </value>
+        </block>
+        ${blockSeparator}
         <block type="looks_show"/>
         <block type="looks_hide"/>
+        ${blockSeparator}
         <block type="looks_switchcostumeto">
             <value name="COSTUME">
                 <shadow type="looks_costume"/>
@@ -201,6 +188,7 @@ const looks = `
                 <shadow type="looks_backdrops"/>
             </value>
         </block>
+        ${blockSeparator}
         <block type="looks_changeeffectby">
             <value name="CHANGE">
                 <shadow type="math_number">
@@ -216,6 +204,7 @@ const looks = `
             </value>
         </block>
         <block type="looks_cleargraphiceffects"/>
+        ${blockSeparator}
         <block type="looks_changesizeby">
             <value name="CHANGE">
                 <shadow type="math_number">
@@ -230,6 +219,7 @@ const looks = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="looks_gotofront"/>
         <block type="looks_gobacklayers">
             <value name="NUM">
@@ -238,10 +228,12 @@ const looks = `
                 </shadow>
             </value>
         </block>
-        <block type="looks_costumeorder"/>
-        <block type="looks_backdroporder"/>
-        <block type="looks_backdropname"/>
-        <block type="looks_size"/>
+        ${blockSeparator}
+        <block id="costumeorder" type="looks_costumeorder"/>
+        <block id="backdroporder" type="looks_backdroporder"/>
+        <block id="backdropname" type="looks_backdropname"/>
+        <block id="size" type="looks_size"/>
+        ${categorySeparator}
     </category>
 `;
 
@@ -258,6 +250,7 @@ const sound = `
             </value>
         </block>
         <block type="sound_stopallsounds"/>
+        ${blockSeparator}
         <block type="sound_playdrumforbeats">
             <value name="DRUM">
                 <shadow type="sound_drums_menu"/>
@@ -275,6 +268,7 @@ const sound = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="sound_playnoteforbeats">
             <value name="NOTE">
                 <shadow type="math_number">
@@ -292,6 +286,7 @@ const sound = `
                 <shadow type="sound_instruments_menu"/>
             </value>
         </block>
+        ${blockSeparator}
         <block type="sound_changeeffectby">
             <value name="VALUE">
                 <shadow type="math_number">
@@ -307,6 +302,7 @@ const sound = `
             </value>
         </block>
         <block type="sound_cleareffects"/>
+        ${blockSeparator}
         <block type="sound_changevolumeby">
             <value name="VOLUME">
                 <shadow type="math_number">
@@ -321,7 +317,8 @@ const sound = `
                 </shadow>
             </value>
         </block>
-        <block type="sound_volume"/>
+        <block id="volume" type="sound_volume"/>
+        ${blockSeparator}
         <block type="sound_changetempoby">
             <value name="TEMPO">
                 <shadow type="math_number">
@@ -336,78 +333,8 @@ const sound = `
                 </shadow>
             </value>
         </block>
-        <block type="sound_tempo"/>
-    </category>
-`;
-
-const pen = `
-    <category name="Pen" colour="#00B295" secondaryColour="#0B8E69">
-        <block type="pen_clear"/>
-        <block type="pen_stamp"/>
-        <block type="pen_pendown"/>
-        <block type="pen_penup"/>
-        <block type="pen_setpencolortocolor">
-            <value name="COLOR">
-                <shadow type="colour_picker">
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_changepencolorby">
-            <value name="COLOR">
-                <shadow type="math_number">
-                    <field name="NUM">10</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_setpencolortonum">
-            <value name="COLOR">
-                <shadow type="math_number">
-                    <field name="NUM">0</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_changepenshadeby">
-            <value name="SHADE">
-                <shadow type="math_number">
-                    <field name="NUM">10</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_setpenshadeto">
-            <value name="SHADE">
-                <shadow type="math_number">
-                    <field name="NUM">50</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_changepensizeby">
-            <value name="SIZE">
-                <shadow type="math_number">
-                    <field name="NUM">1</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_setpensizeto">
-            <value name="SIZE">
-                <shadow type="math_number">
-                    <field name="NUM">1</field>
-                </shadow>
-            </value>
-        </block>
-        <block type="pen_changepentransparencyby" id="pen_changepentransparencyby">
-            <value name="TRANSPARENCY">
-                <shadow type="math_number">
-                    <field name="NUM">10</field>
-            </shadow>
-          </value>
-        </block>
-        <block type="pen_setpentransparencyto" id="pen_setpentransparencyto">
-            <value name="TRANSPARENCY">
-                <shadow type="math_number">
-                    <field name="NUM">50</field>
-                </shadow>
-            </value>
-        </block>
+        <block id="tempo" type="sound_tempo"/>
+        ${categorySeparator}
     </category>
 `;
 
@@ -419,6 +346,7 @@ const events = `
         <block type="event_whenthisspriteclicked"/>
         <block type="event_whenbackdropswitchesto">
         </block>
+        ${blockSeparator}
         <block type="event_whengreaterthan">
             <value name="VALUE">
                 <shadow type="math_number">
@@ -426,6 +354,7 @@ const events = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="event_whenbroadcastreceived">
         </block>
         <block type="event_broadcast">
@@ -438,6 +367,7 @@ const events = `
                 <shadow type="event_broadcast_menu"/>
             </value>
         </block>
+        ${categorySeparator}
     </category>
 `;
 
@@ -450,6 +380,7 @@ const control = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="control_repeat">
             <value name="TIMES">
                 <shadow type="math_whole_number">
@@ -458,11 +389,14 @@ const control = `
             </value>
         </block>
         <block type="control_forever"/>
+        ${blockSeparator}
         <block type="control_if"/>
         <block type="control_if_else"/>
         <block type="control_wait_until"/>
         <block type="control_repeat_until"/>
+        ${blockSeparator}
         <block type="control_stop"/>
+        ${blockSeparator}
         <block type="control_start_as_clone"/>
         <block type="control_create_clone_of">
             <value name="CLONE_OPTION">
@@ -470,6 +404,7 @@ const control = `
             </value>
         </block>
         <block type="control_delete_this_clone"/>
+        ${categorySeparator}
     </category>
 `;
 
@@ -498,6 +433,7 @@ const sensing = `
                 <shadow type="sensing_distancetomenu"/>
             </value>
         </block>
+        ${blockSeparator}
         <block type="sensing_keypressed">
             <value name="KEY_OPTION">
                 <shadow type="sensing_keyoptions"/>
@@ -506,23 +442,28 @@ const sensing = `
         <block type="sensing_mousedown"/>
         <block type="sensing_mousex"/>
         <block type="sensing_mousey"/>
-        <block type="sensing_loudness"/>
-        <block type="sensing_timer"/>
+        ${blockSeparator}
+        <block id="loudness" type="sensing_loudness"/>
+        ${blockSeparator}
+        <block id="timer" type="sensing_timer"/>
         <block type="sensing_resettimer"/>
-        <block type="sensing_of">
+        ${blockSeparator}
+        <block id="of" type="sensing_of">
             <value name="PROPERTY">
-                <shadow type="sensing_of_property_menu"/>
+                <shadow id="sensing_of_property_menu" type="sensing_of_property_menu"/>
             </value>
             <value name="OBJECT">
-                <shadow type="sensing_of_object_menu"/>
+                <shadow id="sensing_of_object_menu" type="sensing_of_object_menu"/>
             </value>
         </block>
-        <block type="sensing_current">
+        ${blockSeparator}
+        <block id="current" type="sensing_current">
             <value name="CURRENTMENU">
-                <shadow type="sensing_currentmenu"/>
+                <shadow id="sensing_currentmenu" type="sensing_currentmenu"/>
             </value>
         </block>
         <block type="sensing_dayssince2000"/>
+        ${categorySeparator}
     </category>
 `;
 
@@ -576,6 +517,7 @@ const operators = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="operator_random">
             <value name="FROM">
                 <shadow type="math_number">
@@ -588,6 +530,7 @@ const operators = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="operator_lt">
             <value name="OPERAND1">
                 <shadow type="text">
@@ -624,9 +567,11 @@ const operators = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="operator_and"/>
         <block type="operator_or"/>
         <block type="operator_not"/>
+        ${blockSeparator}
         <block type="operator_join">
             <value name="STRING1">
                 <shadow type="text">
@@ -658,6 +603,19 @@ const operators = `
                 </shadow>
             </value>
         </block>
+        <block type="operator_contains" id="operator_contains">
+          <value name="STRING1">
+            <shadow type="text">
+              <field name="TEXT">hello</field>
+            </shadow>
+          </value>
+          <value name="STRING2">
+            <shadow type="text">
+              <field name="TEXT">world</field>
+            </shadow>
+          </value>
+        </block>
+        ${blockSeparator}
         <block type="operator_mod">
             <value name="NUM1">
                 <shadow type="math_number">
@@ -677,6 +635,7 @@ const operators = `
                 </shadow>
             </value>
         </block>
+        ${blockSeparator}
         <block type="operator_mathop">
             <value name="NUM">
                 <shadow type="math_number">
@@ -684,6 +643,7 @@ const operators = `
                 </shadow>
             </value>
         </block>
+        ${categorySeparator}
     </category>
 `;
 
@@ -700,18 +660,16 @@ const xmlClose = '</xml>';
  * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
  */
 const makeToolboxXML = function (categoriesXML) {
-    const gap = [separator];
+    const gap = [categorySeparator];
 
     const everything = [
         xmlOpen,
-        top, gap,
         motion, gap,
         looks, gap,
         sound, gap,
         events, gap,
         control, gap,
         sensing, gap,
-        pen, gap,
         operators, gap,
         data
     ];
@@ -721,7 +679,6 @@ const makeToolboxXML = function (categoriesXML) {
     }
 
     everything.push(xmlClose);
-
     return everything.join('\n');
 };
 
diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx
index 5468e7a09b54c292c9e107183fff80550fb0c29d..23ad4694ad0fbbe34340f7eda68698ae3405d07d 100644
--- a/src/lib/project-loader-hoc.jsx
+++ b/src/lib/project-loader-hoc.jsx
@@ -1,26 +1,7 @@
 import React from 'react';
-import xhr from 'xhr';
 
 import log from './log';
-import emptyProject from './empty-project.json';
-
-class ProjectLoaderConstructor {
-    get DEFAULT_PROJECT_DATA () {
-        return emptyProject;
-    }
-
-    load (id, callback) {
-        callback = callback || (err => log.error(err));
-        xhr({
-            uri: `https://projects.scratch.mit.edu/internalapi/project/${id}/get/`
-        }, (err, res, body) => {
-            if (err) return callback(err);
-            callback(null, body);
-        });
-    }
-}
-
-const ProjectLoader = new ProjectLoaderConstructor();
+import storage from './storage';
 
 /* Higher Order Component to provide behavior for loading projects by id from
  * the window's hash (#this part in the url)
@@ -35,13 +16,23 @@ const ProjectLoaderHOC = function (WrappedComponent) {
             this.updateProject = this.updateProject.bind(this);
             this.state = {
                 projectId: null,
-                projectData: this.fetchProjectId().length ? null : JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
+                projectData: null
             };
         }
         componentDidMount () {
             window.addEventListener('hashchange', this.updateProject);
             this.updateProject();
         }
+        componentDidUpdate (prevProps, prevState) {
+            if (this.state.projectId !== prevState.projectId) {
+                storage
+                    .load(storage.AssetType.Project, this.state.projectId, storage.DataFormat.JSON)
+                    .then(projectAsset => projectAsset && this.setState({
+                        projectData: projectAsset.data.toString()
+                    }))
+                    .catch(err => log.error(err));
+            }
+        }
         componentWillUnmount () {
             window.removeEventListener('hashchange', this.updateProject);
         }
@@ -49,18 +40,9 @@ const ProjectLoaderHOC = function (WrappedComponent) {
             return window.location.hash.substring(1);
         }
         updateProject () {
-            const projectId = this.fetchProjectId();
+            let projectId = this.fetchProjectId();
             if (projectId !== this.state.projectId) {
-                if (projectId.length < 1) {
-                    return this.setState({
-                        projectId: projectId,
-                        projectData: JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
-                    });
-                }
-                ProjectLoader.load(projectId, (err, body) => {
-                    if (err) return log.error(err);
-                    this.setState({projectData: body});
-                });
+                if (projectId.length < 1) projectId = 0;
                 this.setState({projectId: projectId});
             }
         }
@@ -80,6 +62,5 @@ const ProjectLoaderHOC = function (WrappedComponent) {
 
 
 export {
-    ProjectLoaderHOC as default,
-    ProjectLoader
+    ProjectLoaderHOC as default
 };
diff --git a/src/lib/storage.js b/src/lib/storage.js
index e1dc91bd6277148f3102a47aca8b7afd2a5401c3..6d858e6c20e57f919cbaa6dfc3eb2080b9f915ce 100644
--- a/src/lib/storage.js
+++ b/src/lib/storage.js
@@ -1,5 +1,7 @@
 import ScratchStorage from 'scratch-storage';
 
+import defaultProjectAssets from './default-project';
+
 const PROJECT_SERVER = 'https://cdn.projects.scratch.mit.edu';
 const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu';
 
@@ -23,7 +25,15 @@ class Storage extends ScratchStorage {
             [this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound],
             asset => `${ASSET_SERVER}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`
         );
+        defaultProjectAssets.forEach(asset => this.cache(
+            this.AssetType[asset.assetType],
+            this.DataFormat[asset.dataFormat],
+            asset.data,
+            asset.id
+        ));
     }
 }
 
-export default Storage;
+const storage = new Storage();
+
+export default storage;
diff --git a/src/locale.js b/src/locale.js
deleted file mode 100644
index ad3fcff79710fa84116934935a5953d9f83119f3..0000000000000000000000000000000000000000
--- a/src/locale.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import localeDataEn from 'react-intl/locale-data/en';
-import localeDataEs from 'react-intl/locale-data/es';
-import localeDataFr from 'react-intl/locale-data/fr';
-
-import messages from '../locale/messages.json'; // eslint-disable-line import/no-unresolved
-
-export default {
-    en: {
-        name: 'English',
-        localeData: localeDataEn,
-        messages: messages.en
-    },
-    es: {
-        name: 'Español',
-        localeData: localeDataEs,
-        messages: messages.es
-    },
-    fr: {
-        name: 'Français',
-        localeData: localeDataFr,
-        messages: messages.fr
-    }
-};
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index fef037d9231a94a541eeaf9b8e490898c4f14d0e..c9e5e41928931dca615abf8de677f693adc36dfa 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -6,6 +6,7 @@ import monitorReducer from './monitors';
 import targetReducer from './targets';
 import toolboxReducer from './toolbox';
 import vmReducer from './vm';
+import {ScratchPaintReducer} from 'scratch-paint';
 
 export default combineReducers({
     colorPicker: colorPickerReducer,
@@ -14,5 +15,6 @@ export default combineReducers({
     monitors: monitorReducer,
     targets: targetReducer,
     toolbox: toolboxReducer,
-    vm: vmReducer
+    vm: vmReducer,
+    scratchPaint: ScratchPaintReducer
 });
diff --git a/src/reducers/intl.js b/src/reducers/intl.js
index 51bce5e56350756287d12bf9273f2b2e1edf5ba8..12841c407e2bcea48ebe4f870cebfcf0b2688734 100644
--- a/src/reducers/intl.js
+++ b/src/reducers/intl.js
@@ -1,25 +1,30 @@
 import {addLocaleData} from 'react-intl';
 import {updateIntl as superUpdateIntl} from 'react-intl-redux';
 import {IntlProvider, intlReducer} from 'react-intl-redux';
+import defaultsDeep from 'lodash.defaultsdeep';
 
-import locales from '../locale.js';
+import localeData from 'scratch-l10n';
+import guiMessages from 'scratch-l10n/locales/gui-msgs';
+import paintMessages from 'scratch-l10n/locales/paint-msgs';
 
-Object.keys(locales).forEach(locale => {
+const combinedMessages = defaultsDeep({}, guiMessages.messages, paintMessages.messages);
+
+Object.keys(localeData).forEach(locale => {
     // TODO: will need to handle locales not in the default intl - see www/custom-locales
-    addLocaleData(locales[locale].localeData);
+    addLocaleData(localeData[locale].localeData);
 });
 
 const intlInitialState = {
     intl: {
         defaultLocale: 'en',
         locale: 'en',
-        messages: locales.en.messages
+        messages: combinedMessages.en.messages
     }
 };
 
 const updateIntl = locale => superUpdateIntl({
     locale: locale,
-    messages: locales[locale].messages || locales.en.messages
+    messages: combinedMessages[locale].messages || combinedMessages.en.messages
 });
 
 export {
diff --git a/src/reducers/vm.js b/src/reducers/vm.js
index 5d0e6f91f0cf707557316d63778e406c58f606c8..dd70f3f4fd756e17e76655ff149559cefd7ffbee 100644
--- a/src/reducers/vm.js
+++ b/src/reducers/vm.js
@@ -1,9 +1,9 @@
 import VM from 'scratch-vm';
-import Storage from '../lib/storage';
+import storage from '../lib/storage';
 
 const SET_VM = 'scratch-gui/vm/SET_VM';
 const defaultVM = new VM();
-defaultVM.attachStorage(new Storage());
+defaultVM.attachStorage(storage);
 const initialState = defaultVM;
 
 const reducer = function (state, action) {
diff --git a/test/helpers/enzyme-setup.js b/test/helpers/enzyme-setup.js
new file mode 100644
index 0000000000000000000000000000000000000000..5374f679d38ef65d773fdc12133032aae63c37b5
--- /dev/null
+++ b/test/helpers/enzyme-setup.js
@@ -0,0 +1,4 @@
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+Enzyme.configure({adapter: new Adapter()});
diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js
index b250bcbe5e82798a4d05c426de198a967cc7a180..c4f36ca05b712e36c30246c269d47c6c1afa78ce 100644
--- a/test/helpers/selenium-helper.js
+++ b/test/helpers/selenium-helper.js
@@ -1,6 +1,7 @@
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // eslint-disable-line no-undef
 
 import bindAll from 'lodash.bindall';
+import 'chromedriver'; // register path
 import webdriver from 'selenium-webdriver';
 
 const {By, until} = webdriver;
@@ -11,6 +12,7 @@ class SeleniumHelper {
             'clickText',
             'clickButton',
             'clickXpath',
+            'findByText',
             'findByXpath',
             'getDriver',
             'getLogs'
@@ -28,12 +30,16 @@ class SeleniumHelper {
         return this.driver.wait(until.elementLocated(By.xpath(xpath), 5 * 1000));
     }
 
+    findByText (text, scope) {
+        return this.findByXpath(`//body//${scope || '*'}//*[contains(text(), '${text}')]`);
+    }
+
     clickXpath (xpath) {
         return this.findByXpath(xpath).then(el => el.click());
     }
 
-    clickText (text) {
-        return this.clickXpath(`//body//*[contains(text(), '${text}')]`);
+    clickText (text, scope) {
+        return this.findByText(text, scope).then(el => el.click());
     }
 
     clickButton (text) {
diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js
index 3299fb27c532df3838a82dd5cfa04528aa3c287b..7fffa903a4bb6f99f08ab18285869ce7bbd05f17 100644
--- a/test/integration/examples.test.js
+++ b/test/integration/examples.test.js
@@ -67,12 +67,12 @@ describe('blocks example', () => {
         await driver.get(`file://${uri}`);
         await clickText('Looks');
         await clickText('Sound');
-        await clickText('Pen');
         await clickText('Events');
         await clickText('Control');
         await clickText('Sensing');
         await clickText('Operators');
         await clickText('Data');
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
         await clickText('Create variable...');
         let el = await findByXpath("//input[@placeholder='']");
         await el.sendKeys('score');
diff --git a/test/integration/test.js b/test/integration/test.js
index 39e8c6faec5a2451e08fb0133a0dd34462738fb4..86729cf277017b5cb99f9b858514633583564d47 100644
--- a/test/integration/test.js
+++ b/test/integration/test.js
@@ -5,6 +5,7 @@ const {
     clickText,
     clickButton,
     clickXpath,
+    findByText,
     findByXpath,
     getDriver,
     getLogs
@@ -18,6 +19,12 @@ const errorWhitelist = [
 
 let driver;
 
+const blocksTabScope = "*[@id='react-tabs-1']";
+const costumesTabScope = "*[@id='react-tabs-3']";
+const soundsTabScope = "*[@id='react-tabs-5']";
+const reportedValueScope = '*[@class="blocklyDropDownContent"]';
+const modalScope = '*[@class="ReactModalPortal"]';
+
 describe('costumes, sounds and variables', () => {
     beforeAll(() => {
         driver = getDriver();
@@ -27,6 +34,31 @@ describe('costumes, sounds and variables', () => {
         await driver.quit();
     });
 
+
+    test('Blocks report when clicked in the toolbox', async () => {
+        await driver.get(`file://${uri}`);
+        await clickText('Blocks');
+        await clickText('Operators', blocksTabScope);
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
+        await clickText('join', blocksTabScope); // Click "join <hello> <world>" block
+        await findByText('helloworld', reportedValueScope); // Tooltip with result
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
+    test('Switching sprites updates the block menus', async () => {
+        await driver.get(`file://${uri}`);
+        await clickText('Sound', blocksTabScope);
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
+        // "meow" sound block should be visible
+        await findByText('meow', blocksTabScope);
+        await clickText('Backdrops'); // Switch to the backdrop
+        // Now "pop" sound block should be visible
+        await findByText('pop', blocksTabScope);
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
     test('Adding a costume', async () => {
         await driver.get(`file://${uri}`);
         await clickText('Costumes');
@@ -34,8 +66,8 @@ describe('costumes, sounds and variables', () => {
         const el = await findByXpath("//input[@placeholder='what are you looking for?']");
         await el.sendKeys('abb');
         await clickText('abby-a'); // Should close the modal, then click the costumes in the selector
-        await clickText('costume1');
-        await clickText('abby-a');
+        await clickText('costume1', costumesTabScope);
+        await clickText('abby-a', costumesTabScope);
         const logs = await getLogs(errorWhitelist);
         await expect(logs).toEqual([]);
     });
@@ -47,10 +79,10 @@ describe('costumes, sounds and variables', () => {
         const el = await findByXpath("//input[@placeholder='what are you looking for?']");
         await el.sendKeys('chom');
         await clickText('chomp'); // Should close the modal, then click the sounds in the selector
-        await clickText('meow');
-        await clickText('chomp');
+        await clickText('meow', soundsTabScope);
+        await clickText('chomp', soundsTabScope);
         await clickXpath('//button[@title="Play"]');
-        await clickText('meow');
+        await clickText('meow', soundsTabScope);
         await clickXpath('//button[@title="Play"]');
 
         await clickText('Louder');
@@ -79,7 +111,8 @@ describe('costumes, sounds and variables', () => {
     test('Creating variables', async () => {
         await driver.get(`file://${uri}`);
         await clickText('Blocks');
-        await clickText('Data');
+        await clickText('Data', blocksTabScope);
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
         await clickText('Create variable...');
         let el = await findByXpath("//input[@placeholder='']");
         await el.sendKeys('score');
@@ -88,6 +121,33 @@ describe('costumes, sounds and variables', () => {
         el = await findByXpath("//input[@placeholder='']");
         await el.sendKeys('second variable');
         await clickButton('OK');
+
+        // Make sure reporting works on a new variable
+        await clickText('Data', blocksTabScope);
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
+        await clickText('score', blocksTabScope);
+        await findByText('0', reportedValueScope); // Tooltip with result
+
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
+    test('Importing extensions', async () => {
+        await driver.get(`file://${uri}`);
+        await clickText('Blocks');
+        await clickText('Extensions');
+        await clickText('Pen', modalScope); // Modal closes
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
+        await clickText('stamp', blocksTabScope); // Click the "stamp" block
+
+        // Make sure trying to load the extension again scrolls back down
+        await clickText('Motion', blocksTabScope); // To scroll the list back to the top
+        await clickText('Extensions');
+        await clickText('Pen', modalScope); // Modal closes
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
+        await clickText('stamp', blocksTabScope); // Would fail if didn't scroll back
+
+
         const logs = await getLogs(errorWhitelist);
         await expect(logs).toEqual([]);
     });
diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx
index 325488a250249f9a426926fb56ab367b519b4cae..63199ef01e6f6fffebd49ea2cb0397d1dbb47887 100644
--- a/test/unit/containers/sound-editor.test.jsx
+++ b/test/unit/containers/sound-editor.test.jsx
@@ -63,7 +63,7 @@ describe('Sound Editor Container', () => {
                 store={store}
             />
         );
-        const component = wrapper.find(SoundEditorComponent);
+        let component = wrapper.find(SoundEditorComponent);
         // Ensure rendering doesn't start playing any sounds
         expect(mockAudioBufferPlayer.instance.play.mock.calls).toEqual([]);
         expect(mockAudioBufferPlayer.instance.stop.mock.calls).toEqual([]);
@@ -73,9 +73,13 @@ describe('Sound Editor Container', () => {
 
         // Mock the audio buffer player calling onUpdate
         mockAudioBufferPlayer.instance.onUpdate(0.5);
+        wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.props().playhead).toEqual(0.5);
 
         component.props().onStop();
+        wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(mockAudioBufferPlayer.instance.stop).toHaveBeenCalled();
         expect(component.props().playhead).toEqual(null);
     });
@@ -87,13 +91,17 @@ describe('Sound Editor Container', () => {
                 store={store}
             />
         );
-        const component = wrapper.find(SoundEditorComponent);
+        let component = wrapper.find(SoundEditorComponent);
 
         component.props().onActivateTrim();
+        wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.props().trimStart).not.toEqual(null);
         expect(component.props().trimEnd).not.toEqual(null);
 
         component.props().onActivateTrim();
+        wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(vm.updateSoundBuffer).toHaveBeenCalled();
         expect(component.props().trimStart).toEqual(null);
         expect(component.props().trimEnd).toEqual(null);
@@ -223,7 +231,7 @@ describe('Sound Editor Container', () => {
                 store={store}
             />
         );
-        const component = wrapper.find(SoundEditorComponent);
+        let component = wrapper.find(SoundEditorComponent);
         // Undo and redo should be disabled initially
         expect(component.prop('canUndo')).toEqual(false);
         expect(component.prop('canRedo')).toEqual(false);
@@ -232,27 +240,34 @@ describe('Sound Editor Container', () => {
         component.props().onActivateTrim(); // Activate trimming
         component.props().onActivateTrim(); // Submit new samples by calling again
         wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(true);
         expect(component.prop('canRedo')).toEqual(false);
 
         // Undoing should make it possible to redo and not possible to undo again
         component.props().onUndo();
         wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(false);
         expect(component.prop('canRedo')).toEqual(true);
 
         // Redoing should make it possible to undo and not possible to redo again
         component.props().onRedo();
         wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(true);
         expect(component.prop('canRedo')).toEqual(false);
 
         // New submission should clear the redo stack
         component.props().onUndo(); // Undo to go back to a state where redo is enabled
         wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canRedo')).toEqual(true);
         component.props().onActivateTrim(); // Activate trimming
         component.props().onActivateTrim(); // Submit new samples by calling again
+
+        wrapper.update();
+        component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canRedo')).toEqual(false);
     });
 });
diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx
index 964894c4369441e328e6e365f97631d3736f15b6..3771f37692563ebef894a719138ece5fdc4813b0 100644
--- a/test/unit/util/project-loader-hoc.test.jsx
+++ b/test/unit/util/project-loader-hoc.test.jsx
@@ -1,5 +1,6 @@
 import React from 'react';
-import ProjectLoaderHOC, {ProjectLoader} from '../../../src/lib/project-loader-hoc.jsx';
+import ProjectLoaderHOC from '../../../src/lib/project-loader-hoc.jsx';
+import storage from '../../../src/lib/storage';
 import {mount} from 'enzyme';
 
 describe('ProjectLoaderHOC', () => {
@@ -7,34 +8,41 @@ describe('ProjectLoaderHOC', () => {
         const Component = ({projectData}) => <div>{projectData}</div>;
         const WrappedComponent = ProjectLoaderHOC(Component);
         window.location.hash = '#winning';
-        ProjectLoader.load = jest.fn((id, cb) => cb(null, null));
+        const originalLoad = storage.load;
+        storage.load = jest.fn(() => Promise.resolve(null));
         const mounted = mount(<WrappedComponent />);
-        ProjectLoader.load.mockRestore();
+        storage.load = originalLoad;
         window.location.hash = '';
-        expect(mounted.find('div').exists()).toEqual(false);
+        const mountedDiv = mounted.find('div');
+        expect(mountedDiv.exists()).toEqual(false);
     });
 
     test('when there is no hash, it loads the default project', () => {
         const Component = ({projectData}) => <div>{projectData}</div>;
         const WrappedComponent = ProjectLoaderHOC(Component);
         window.location.hash = '';
+        const originalLoad = storage.load;
+        storage.load = jest.fn((type, id) => Promise.resolve(id));
         const mounted = mount(<WrappedComponent />);
-        expect(mounted.find('div').text()).toEqual(JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA));
+        expect(mounted.state().projectId).toEqual(0);
+        expect(storage.load).toHaveBeenCalledWith(
+            storage.AssetType.Project, 0, storage.DataFormat.JSON
+        );
+        storage.load = originalLoad;
     });
 
     test('when there is a hash, it tries to load that project', () => {
         const Component = ({projectData}) => <div>{projectData}</div>;
         const WrappedComponent = ProjectLoaderHOC(Component);
         window.location.hash = '#winning';
-        ProjectLoader.load = jest.fn((id, cb) => cb(null, id));
+        const originalLoad = storage.load;
+        storage.load = jest.fn((type, id) => Promise.resolve({data: id}));
         const mounted = mount(<WrappedComponent />);
-        mounted.update();
-        ProjectLoader.load.mockRestore();
-        window.location.hash = '';
-        expect(mounted
-            .find('div')
-            .text()
-        ).toEqual('winning');
+        expect(mounted.state().projectId).toEqual('winning');
+        expect(storage.load).toHaveBeenLastCalledWith(
+            storage.AssetType.Project, 'winning', storage.DataFormat.JSON
+        );
+        storage.load = originalLoad;
     });
 
     test('when hash change happens, the project data state is changed', () => {
@@ -42,10 +50,12 @@ describe('ProjectLoaderHOC', () => {
         const WrappedComponent = ProjectLoaderHOC(Component);
         window.location.hash = '';
         const mounted = mount(<WrappedComponent />);
-        const before = mounted.find('div').text();
-        ProjectLoader.load = jest.fn((id, cb) => cb(null, id));
-        window.location.hash = `#winning`;
-        mounted.node.updateProject();
-        expect(mounted.find('div').text()).not.toEqual(before);
+        expect(mounted.state().projectId).toEqual(0);
+        const originalLoad = storage.load;
+        storage.load = jest.fn((type, id) => Promise.resolve({data: id}));
+        window.location.hash = '#winning';
+        mounted.instance().updateProject();
+        expect(mounted.state().projectId).toEqual('winning');
+        storage.load = originalLoad;
     });
 });
diff --git a/webpack.config.js b/webpack.config.js
index 6c486cc8aaacdf7336c262e602ceaadb47c5fdff..e9b97f9efa740c41a3abdbd0128f7260ad77bf48 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -21,6 +21,7 @@ module.exports = {
         lib: ['react', 'react-dom'],
         gui: './src/index.jsx',
         blocksonly: './src/examples/blocks-only.jsx',
+        compatibilitytesting: './src/examples/compatibility-testing.jsx',
         player: './src/examples/player.jsx'
     },
     output: {
@@ -66,8 +67,8 @@ module.exports = {
             }]
         },
         {
-            test: /\.svg$/,
-            loader: 'svg-url-loader?noquotes'
+            test: /\.(svg|png|wav)$/,
+            loader: 'file-loader'
         }]
     },
     plugins: [
@@ -90,6 +91,12 @@ module.exports = {
             filename: 'blocks-only.html',
             title: 'Scratch 3.0 GUI: Blocks Only Example'
         }),
+        new HtmlWebpackPlugin({
+            chunks: ['lib', 'compatibilitytesting'],
+            template: 'src/index.ejs',
+            filename: 'compatibility-testing.html',
+            title: 'Scratch 3.0 GUI: Compatibility Testing'
+        }),
         new HtmlWebpackPlugin({
             chunks: ['lib', 'player'],
             template: 'src/index.ejs',