diff --git a/Makefile b/Makefile
index 98b2e0527..2f9111b8e 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,7 @@ GIT_MESSAGE=$(shell git log -1 --pretty=%s 2> /dev/null)
 
 build:
 	@make clean
+	@make translations
 	@make webpack
 	@make tag
 
@@ -54,6 +55,10 @@ test:
 	@make lint
 	@make build
 	@echo ""
+	@make unit
+	@echo ""
+	@make functional
+	@echo ""
 
 lint:
 	$(ESLINT) ./*.js
diff --git a/bin/build-locales b/bin/build-locales
new file mode 100755
index 000000000..1c32f86ac
--- /dev/null
+++ b/bin/build-locales
@@ -0,0 +1,157 @@
+#!/usr/bin/env node
+
+/*
+    Converts the existing .po translation files in the module to JSON files.
+    Requires po2json in order to work. Takes as input a directory
+    in which to store the resulting json translation files.
+
+    Takes in as an argument an output directory to put translation files.
+    Searches for files named `l10n.json` in the `src/views/` directory to get
+    template english strings (as well as the general template at `src/l10n.json`).
+
+    It compiles the template strings into a flat object that is compared against the
+    translations in the .po files from the `scratchr2_translations` dependency, using
+    an md5 of the template string without whitespace, and an md5 of the .po msgid string
+    without whitespace.
+
+    The output files are javascript files that declare objects by locale. Each locale
+    has a sub-object with FormattedMessage ids as keys, and translated strings as
+    values. If no translation was found for a string, the default english will be the
+    value.
+
+    Output Example:
+    '''
+    var message = {
+        en: {
+            'general.inAWorld': 'In a world, where bears are invisible...',
+            'general.question': 'Are there bears here?',
+            'general.answer': 'I dunno, but there could be...'
+        },
+        es: {
+            'general.inAWorld': 'En un mundo, donde hay osos invisibles',
+            'general.question': 'Are there bears here?',
+            'general.answer': 'No sé, pero es posible...'
+        }
+    }
+    '''
+*/
+var fs = require('fs');
+var glob = require('glob');
+var merge = require('lodash.merge');
+var path = require('path');
+var po2icu = require('po2icu');
+
+var localeCompare = require('./lib/locale-compare');
+
+// -----------------------------------------------------------------------------
+// Main script
+// -----------------------------------------------------------------------------
+
+
+var args = process.argv.slice(2);
+
+if (!args.length) {
+    process.stdout.write('A destination directory must be specified.');
+    process.exit(1);
+}
+
+var poUiDir = path.resolve(__dirname, '../node_modules/scratchr2_translations/ui');
+var outputDir = path.resolve(__dirname, '../', args[0]);
+try {
+    fs.accessSync(outputDir, fs.F_OK);
+} catch (err) {
+    // Doesn't exist - create it.
+    fs.mkdirSync(outputDir);
+}
+
+// get global locale strings first.
+var globalTemplateFile = path.resolve(__dirname, '../src/l10n.json');
+// message key with english string values (i.e. default values)
+var generalIds = JSON.parse(fs.readFileSync(globalTemplateFile, 'utf8'));
+var viewLocales = {};
+var generalLocales = {
+    en: generalIds
+};
+
+// FormattedMessage id with english string as value. Use for default values in translations
+// Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' }
+var idsWithICU = {};
+
+// reverse (i.e. english string with message key as the value) object for searching po files.
+// Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' }
+var icuWithIds = {};
+
+for (var id in generalIds) {
+    idsWithICU['general-' + id] = generalIds[id];
+    icuWithIds[generalIds[id]] = 'general-' + id;
+}
+
+// get view-specific locale strings.
+var files = glob.sync(path.resolve(__dirname, '../src/views/**/l10n.json'));
+files.forEach(function (file) {
+    var dirPath = file.split('/');
+    dirPath.pop();
+    var view = dirPath.pop();
+
+    var viewIds = JSON.parse(fs.readFileSync(file, 'utf8'));
+    viewLocales[view] = {
+        en: viewIds
+    };
+    for (var id in viewIds) {
+        idsWithICU[view + '-' + id] = viewIds[id];
+        icuWithIds[viewIds[id]] = view + '-' + id; // add viewName to identifier for later
+    }
+});
+
+// md5 of english strings with message key as the value for searching po files.
+// Sample structure: { 'sdfas43534sdfasdf': 'general-general.blah', 'lkjfasdf4t342asdfa': 'about-about.blah' }
+var md5WithIds = localeCompare.getMD5Map(icuWithIds);
+
+// Get ui localization strings first
+glob(poUiDir + '/*', function (err, files) {
+    if (err) throw new Error(err);
+    
+    files.forEach(function (file) {
+        var lang = file.split('/').pop();
+        var jsFile = path.resolve(file, 'LC_MESSAGES/djangojs.po');
+        var pyFile = path.resolve(file, 'LC_MESSAGES/django.po');
+
+        var translations = {};
+
+        try {
+            var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
+            translations = localeCompare.mergeNewTranslations(translations, jsTranslations, idsWithICU, md5WithIds);
+        } catch (err) {
+            process.stdout.write(lang + ': ' + err + '\n');
+        }
+
+        try {
+            var pyTranslations = po2icu.poFileToICUSync(lang, pyFile);
+            translations = localeCompare.mergeNewTranslations(translations, pyTranslations, idsWithICU, md5WithIds);
+        } catch (err) {
+            process.stdout.write(lang + ': ' + err + '\n');
+        }
+
+        // add new translations to locale object
+        for (var id in translations) {
+            var ids = id.split('-'); // [viewName, stringId]
+            var viewName = ids[0];
+            var stringId = ids[1];
+            if (viewLocales.hasOwnProperty(viewName)) {
+                if (!viewLocales[viewName].hasOwnProperty(lang)) viewLocales[viewName][lang] = {};
+                viewLocales[viewName][lang][stringId] = translations[id];
+            } else {
+                // default to general
+                if (!generalLocales.hasOwnProperty(lang)) generalLocales[lang] = {};
+                generalLocales[lang][stringId] = translations[id];
+            }
+        }
+    });
+    
+    for (var view in viewLocales) {
+        var viewTranslations = merge(viewLocales[view], generalLocales);
+        var objectString = JSON.stringify(viewTranslations);
+        var fileString = 'window._messages = ' + objectString + ';';
+        fs.writeFileSync(outputDir + '/' + view + '.js', fileString);
+    }
+});
diff --git a/bin/lib/locale-compare.js b/bin/lib/locale-compare.js
new file mode 100644
index 000000000..25ac94345
--- /dev/null
+++ b/bin/lib/locale-compare.js
@@ -0,0 +1,65 @@
+// -----------------------------------------------------------------------------
+// Helper Methods for build-locales node script.
+// -----------------------------------------------------------------------------
+
+var crypto = require('crypto');
+
+var Helpers = {};
+
+/**
+ * Get the md5 has of a string with whitespace removed.
+ *
+ * @param {string} string a string
+ * @return {string} an md5 hash of the string in hex.
+ */
+Helpers.getMD5 = function (string) {
+    var cleanedString = string.replace(/\s+/g, '');
+    return crypto.createHash('md5').update(cleanedString, 'utf8').digest('hex');
+};
+
+/*
+    Existing translations should be in the key value format specified by react-intl (i.e.
+    formatted message id, with icu string as the value). New Translations should be in the
+    format returned by po2icu (i.e. a source language icu string for key, and a localized
+    language icu string for value).
+
+    ICU Map is an object in the reverse react-intl formatting (icu string as key), which will
+    help determine if the translation belongs in www currently.
+*/
+Helpers.mergeNewTranslations = function (existingTranslations, newTranslations, icuTemplate, md5Map) {
+    for (var id in newTranslations) {
+        var md5 = Helpers.getMD5(id);
+        if (md5Map.hasOwnProperty(md5) && newTranslations[id].length > 0) {
+            existingTranslations[md5Map[md5]] = newTranslations[id];
+        }
+    }
+
+    //Fill in defaults
+    for (var id in icuTemplate) {
+        if (!existingTranslations.hasOwnProperty(id)) existingTranslations[id] = icuTemplate[id];
+    }
+    return existingTranslations;
+};
+
+/**
+ * Converts a map of icu strings with react-intl id values into a map
+ * with md5 hashes of the icu strings as keys and react-intl id values.
+ * This is done so as to eliminate potential po conversion misses that
+ * could be caused by different white space formatting between po and icu.
+ *
+ * The md5 is generated after all white space is removed from the string.
+ *
+ * @param   {object}    idICUMap    map where key=icuString, value=react-intl id
+ *
+ * @return  {object}
+ */
+Helpers.getMD5Map = function (ICUIdMap) {
+    var md5Map = {};
+    for (var icu in ICUIdMap) {
+        var md5 = Helpers.getMD5(icu);
+        md5Map[md5] = ICUIdMap[icu];
+    }
+    return md5Map;
+};
+
+module.exports = Helpers;
diff --git a/intl-loader.js b/intl-loader.js
deleted file mode 100644
index 6fa23473b..000000000
--- a/intl-loader.js
+++ /dev/null
@@ -1,45 +0,0 @@
-var glob = require('glob');
-var path = require('path');
-var po2icu = require('po2icu');
-
-var localeCompare = require('./bin/lib/locale-compare');
-
-module.exports = function (source) {
-    this.cacheable();
-
-    var poUiDir = path.resolve(__dirname, './node_modules/scratchr2_translations/ui');
-    var viewIds = JSON.parse(source);
-    var viewLocales = {
-        en: viewIds
-    };
-    var icuWithIds = {};
-    for (var id in viewIds) {
-        icuWithIds[viewIds[id]] = id;
-    }
-    var md5WithIds = localeCompare.getMD5Map(icuWithIds);
-    
-    var files = glob.sync(poUiDir + '/*');
-    files.forEach(function (file) {
-        var lang = file.split('/').pop();
-        var jsFile = path.resolve(file, 'LC_MESSAGES/djangojs.po');
-        var pyFile = path.resolve(file, 'LC_MESSAGES/django.po');
-
-        var translations = {};
-        try {
-            var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
-            translations = localeCompare.mergeNewTranslations(translations, jsTranslations, viewIds, md5WithIds);
-        } catch (err) {
-            process.stdout.write(lang + ': ' + err + '\n');
-        }
-        try {
-            var pyTranslations = po2icu.poFileToICUSync(lang, pyFile);
-            translations = localeCompare.mergeNewTranslations(translations, pyTranslations, viewIds, md5WithIds);
-        } catch (err) {
-            process.stdout.write(lang + ': ' + err + '\n');
-        }
-
-        viewLocales[lang] = translations;
-    });
-
-    return 'module.exports = ' + JSON.stringify(viewLocales, undefined, '\t') + ';';
-};
diff --git a/package.json b/package.json
index 257840c81..5ca221023 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
     "lodash.range": "3.0.1",
     "minilog": "2.0.8",
     "node-sass": "3.3.3",
+    "po2icu": "git://github.com/LLK/po2icu.git#develop",
     "react": "0.14.0",
     "react-addons-test-utils": "0.14.0",
     "react-dom": "0.14.0",
@@ -61,7 +62,7 @@
     "routes-to-nginx-conf": "0.0.4",
     "sass-lint": "1.3.2",
     "sass-loader": "2.0.1",
-    "scratch-www-intl-loader": "git+ssh://git@github.com/LLK/scratch-www-intl-loader.git#33a0d059308f4fd71c88bbf501e43ce682429ef0",
+    "scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
     "slick-carousel": "1.5.8",
     "source-map-support": "0.3.2",
     "style-loader": "0.12.3",
diff --git a/server/template.html b/server/template.html
index cd748fbb2..4741617f0 100644
--- a/server/template.html
+++ b/server/template.html
@@ -51,6 +51,7 @@
         <script src="/js/lib/react-intl-with-locales{{min}}.js"></script>
         <script src="/js/lib/raven.min.js"></script>
 
+        <script src="/js/intl/{{view}}.js"></script>
         <script src="/js/{{view}}.bundle.js"></script>
 
         <!-- Error logging (Sentry) -->
diff --git a/src/main.intl b/src/l10n.json
similarity index 100%
rename from src/main.intl
rename to src/l10n.json
diff --git a/src/lib/render.jsx b/src/lib/render.jsx
index 29e28015e..378997e70 100644
--- a/src/lib/render.jsx
+++ b/src/lib/render.jsx
@@ -3,21 +3,22 @@ var ReactDOM = require('react-dom');
 var ReactIntl = require('./intl.jsx');
 var IntlProvider = ReactIntl.IntlProvider;
 
-var render = function (jsx, element, messages) {
+var render = function (jsx, element) {
     // Get locale and messages from global namespace (see "init.js")
     var locale = window._locale || 'en';
-    if (typeof messages[locale] === 'undefined') {
+    if (typeof window._messages[locale] === 'undefined') {
         // Fall back on the split
         locale = locale.split('-')[0];
     }
-    if (typeof messages[locale] === 'undefined') {
+    if (typeof window._messages[locale] === 'undefined') {
         // Language appears to not be supported – fall back to 'en'
         locale = 'en';
     }
+    var messages = window._messages[locale];
 
     // Render component
     var component = ReactDOM.render(
-        <IntlProvider locale={locale} messages={messages[locale]}>
+        <IntlProvider locale={locale} messages={messages}>
             {jsx}
         </IntlProvider>,
         element
diff --git a/src/views/about/about.jsx b/src/views/about/about.jsx
index fcea2ae61..1f178fdb6 100644
--- a/src/views/about/about.jsx
+++ b/src/views/about/about.jsx
@@ -1,16 +1,11 @@
+var React = require('react');
 var FormattedHTMLMessage = require('react-intl').FormattedHTMLMessage;
 var FormattedMessage = require('react-intl').FormattedMessage;
-var merge = require('lodash.merge');
-var React = require('react');
-
 var render = require('../../lib/render.jsx');
 
 require('../../main.scss');
 require('./about.scss');
 
-var generalMessages = require('../../main.intl');
-var viewMessages = require('./about.intl');
-
 var Navigation = require('../../components/navigation/navigation.jsx');
 var Footer = require('../../components/footer/footer.jsx');
 
@@ -109,6 +104,6 @@ var About = React.createClass({
     }
 });
 
-render(<Navigation />, document.getElementById('navigation'), generalMessages);
-render(<Footer />, document.getElementById('footer'), generalMessages);
-render(<About />, document.getElementById('view'), merge(generalMessages, viewMessages));
+render(<Navigation />, document.getElementById('navigation'));
+render(<Footer />, document.getElementById('footer'));
+render(<About />, document.getElementById('view'));
diff --git a/src/views/about/about.intl b/src/views/about/l10n.json
similarity index 100%
rename from src/views/about/about.intl
rename to src/views/about/l10n.json
diff --git a/src/views/components/components.jsx b/src/views/components/components.jsx
index 59b06878b..59194eb25 100644
--- a/src/views/components/components.jsx
+++ b/src/views/components/components.jsx
@@ -11,8 +11,6 @@ var Spinner = require('../../components/spinner/spinner.jsx');
 require('../../main.scss');
 require('./components.scss');
 
-var generalMessages = require('../../main.intl');
-
 var Navigation = require('../../components/navigation/navigation.jsx');
 var Footer = require('../../components/footer/footer.jsx');
 
@@ -50,6 +48,6 @@ var Components = React.createClass({
     }
 });
 
-render(<Navigation />, document.getElementById('navigation'), generalMessages);
-render(<Footer />, document.getElementById('footer'), generalMessages);
-render(<Components />, document.getElementById('view'), generalMessages);
+render(<Navigation />, document.getElementById('navigation'));
+render(<Footer />, document.getElementById('footer'));
+render(<Components />, document.getElementById('view'));
diff --git a/src/views/credits/credits.jsx b/src/views/credits/credits.jsx
index b3e3a9eff..7a948acbd 100644
--- a/src/views/credits/credits.jsx
+++ b/src/views/credits/credits.jsx
@@ -4,8 +4,6 @@ var render = require('../../lib/render.jsx');
 require('../../main.scss');
 require('./credits.scss');
 
-var generalMessages = require('../../main.intl');
-
 var Navigation = require('../../components/navigation/navigation.jsx');
 var Footer = require('../../components/footer/footer.jsx');
 
@@ -300,6 +298,6 @@ var Credits = React.createClass({
     }
 });
 
-render(<Navigation />, document.getElementById('navigation'), generalMessages);
-render(<Footer />, document.getElementById('footer'), generalMessages);
-render(<Credits />, document.getElementById('view'), generalMessages);
+render(<Navigation />, document.getElementById('navigation'));
+render(<Footer />, document.getElementById('footer'));
+render(<Credits />, document.getElementById('view'));
diff --git a/src/views/hoc/hoc.jsx b/src/views/hoc/hoc.jsx
index e14beac9a..c800684c0 100644
--- a/src/views/hoc/hoc.jsx
+++ b/src/views/hoc/hoc.jsx
@@ -1,7 +1,6 @@
 var classNames = require('classnames');
 var FormattedHTMLMessage = require('react-intl').FormattedHTMLMessage;
 var FormattedMessage = require('react-intl').FormattedMessage;
-var merge = require('lodash.merge');
 var React = require('react');
 var render = require('../../lib/render.jsx');
 
@@ -12,9 +11,6 @@ var SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
 require('../../main.scss');
 require('./hoc.scss');
 
-var generalMessages = require('../../main.intl');
-var viewMessages = require('./hoc.intl');
-
 var Navigation = require('../../components/navigation/navigation.jsx');
 var Footer = require('../../components/footer/footer.jsx');
 
@@ -413,6 +409,6 @@ var Hoc = React.createClass({
     }
 });
 
-render(<Navigation />, document.getElementById('navigation'), generalMessages);
-render(<Footer />, document.getElementById('footer'), generalMessages);
-render(<Hoc />, document.getElementById('view'), merge(generalMessages, viewMessages));
+render(<Navigation />, document.getElementById('navigation'));
+render(<Footer />, document.getElementById('footer'));
+render(<Hoc />, document.getElementById('view'));
diff --git a/src/views/hoc/hoc.intl b/src/views/hoc/l10n.json
similarity index 100%
rename from src/views/hoc/hoc.intl
rename to src/views/hoc/l10n.json
diff --git a/src/views/splash/splash.intl b/src/views/splash/l10n.json
similarity index 100%
rename from src/views/splash/splash.intl
rename to src/views/splash/l10n.json
diff --git a/src/views/splash/splash.jsx b/src/views/splash/splash.jsx
index 430a34c73..f2d2b04d7 100644
--- a/src/views/splash/splash.jsx
+++ b/src/views/splash/splash.jsx
@@ -1,5 +1,4 @@
 var injectIntl = require('react-intl').injectIntl;
-var merge = require('lodash.merge');
 var omit = require('lodash.omit');
 var React = require('react');
 var render = require('../../lib/render.jsx');
@@ -21,9 +20,6 @@ var Welcome = require('../../components/welcome/welcome.jsx');
 require('../../main.scss');
 require('./splash.scss');
 
-var generalMessages = require('../../main.intl');
-var viewMessages = require('./splash.intl');
-
 var Navigation = require('../../components/navigation/navigation.jsx');
 var Footer = require('../../components/footer/footer.jsx');
 
@@ -419,6 +415,6 @@ var Splash = injectIntl(React.createClass({
     }
 }));
 
-render(<Navigation />, document.getElementById('navigation'), generalMessages);
-render(<Footer />, document.getElementById('footer'), generalMessages);
-render(<Splash />, document.getElementById('view'), merge(generalMessages, viewMessages));
+render(<Navigation />, document.getElementById('navigation'));
+render(<Footer />, document.getElementById('footer'));
+render(<Splash />, document.getElementById('view'));
diff --git a/test/fixtures/build_locales.json b/test/fixtures/build_locales.json
new file mode 100644
index 000000000..762540250
--- /dev/null
+++ b/test/fixtures/build_locales.json
@@ -0,0 +1,4 @@
+{
+    "<p> <img src=\"{STATIC_URL}/images/help/ask-and-wait.png\" /> asks a question and stores the keyboard input in  <img src=\"{STATIC_URL}/images/help/answer.png\" />. The answer is shared by all sprites. </p><p>If you want to save the current answer, you can store it in a variable or list. For example, <img src=\"{STATIC_URL}/images/help/answer-ex2.png\"/> </p><p>To view the value of answer, click the checkbox next to the answer block.<br><img src=\"{STATIC_URL}/images/help/answer-checkbox.png\" /></p>": "test.id1",
+    "<p> <img src=\"{STATIC_URL}/images/help/ask-and-wait.png\" /> asks a question and stores the keyboard input in  <img src=\"{STATIC_URL}/images/help/answer.png\" />. The question appears in a voice balloon on the screen. The program waits as the user types in a response, until the Enter key is pressed or the check mark is clicked. </p>": "test.id2"
+}
diff --git a/test/fixtures/test_es.po b/test/fixtures/test_es.po
new file mode 100644
index 000000000..754051c43
--- /dev/null
+++ b/test/fixtures/test_es.po
@@ -0,0 +1,72 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-03-20 14:16+0000\n"
+"PO-Revision-Date: 2015-09-21 17:37+0000\n"
+"Last-Translator: Anonymous Pootle User\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Pootle 2.5.1.1\n"
+"X-POOTLE-MTIME: 1442857052.000000\n"
+
+#: test.html:15
+#, python-format
+msgid ""
+"\n"
+"<p> <img src=\"%(STATIC_URL)s/images/help/ask-and-wait.png\" /> asks a "
+"question and stores the keyboard input in  <img src=\"%(STATIC_URL)s/images/"
+"help/answer.png\" />. The answer is shared by all sprites. </p>\n"
+"<p>If you want to save the current answer, you can store it in a variable or "
+"list. For example, <img src=\"%(STATIC_URL)s/images/help/answer-ex2.png\"/"
+"> \n"
+"</p>\n"
+"\n"
+"<p>\n"
+"To view the value of answer, click the checkbox next to the answer block."
+"<br>\n"
+"<img src=\"%(STATIC_URL)s/images/help/answer-checkbox.png\" />\n"
+"</p>"
+msgstr ""
+"\n"
+"<p><img src=\"%(STATIC_URL)s/images/help/es/ask-and-wait.png\" /> hace una "
+"pregunta y almacena la entrada de teclado en <img src=\"%(STATIC_URL)s/"
+"images/help/es/answer.png\" />. La respuesta se comparte para todos los "
+"objetos. </p>\n"
+"<p>Si deseas guardar la respuesta actual, debes almacenarla en una variable "
+"o una lista. Por ejemplo, <img src=\"%(STATIC_URL)s/images/help/es/answer-"
+"ex2.png\"/> \n"
+"</p>\n"
+"\n"
+"<p>\n"
+"Si deseas ver el valor de una respuesta, haz clic en la casilla que aparece "
+"junto al bloque de respuesta.<br>\n"
+"<img src=\"%(STATIC_URL)s/images/help/es/answer-checkbox.png\" />\n"
+"</p>"
+
+#: test.html:18
+#, python-format
+msgid ""
+"\n"
+"<p> <img src=\"%(STATIC_URL)s/images/help/ask-and-wait.png\" /> asks a "
+"question and stores the keyboard input in  <img src=\"%(STATIC_URL)s/images/"
+"help/answer.png\" />. The question appears in a voice balloon on the screen. "
+"The program waits as the user types in a response, until the Enter key is "
+"pressed or the check mark is clicked. \n"
+"</p>"
+msgstr ""
+"\n"
+"<p> <img src=\"%(STATIC_URL)s/images/help/es/ask-and-wait.png\" /> hace una "
+"pregunta y almacena la entrada de teclado en <img src=\"%(STATIC_URL)s/"
+"images/help/es/answer.png\" />. La pregunta aparece en un globo de voz en la "
+"pantalla. El programa espera hasta que el usuario escriba una respuesta y "
+"presione Enter o haga clic en la casilla de aprobación.\n"
+"</p>"
diff --git a/test/fixtures/test_es_md5map.json b/test/fixtures/test_es_md5map.json
new file mode 100644
index 000000000..b19fcf3ab
--- /dev/null
+++ b/test/fixtures/test_es_md5map.json
@@ -0,0 +1,4 @@
+{
+    "2ec20d41b181e1a41c071e13f414a74d": "test.id1",
+    "37ba6d5ef524504215f478912155f9ba": "test.id2"
+}
diff --git a/test/functional/build_locales_complex_strings.js b/test/functional/build_locales_complex_strings.js
new file mode 100644
index 000000000..e9b17c546
--- /dev/null
+++ b/test/functional/build_locales_complex_strings.js
@@ -0,0 +1,16 @@
+var fs = require('fs');
+var path = require('path');
+var po2icu = require('po2icu');
+var tap = require('tap');
+
+var buildLocales = require('../../bin/lib/locale-compare');
+
+tap.test('buildLocalesFile', function (t) {
+    var md5map = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../fixtures/test_es_md5map.json'), 'utf8'));
+    var newTranslations = po2icu.poFileToICUSync('es', path.resolve(__dirname, '../fixtures/test_es.po'));
+    var translations = buildLocales.mergeNewTranslations({}, newTranslations, {}, md5map);
+
+    t.ok(translations['test.id1'] !== undefined);
+    t.ok(translations['test.id2'] !== undefined);
+    t.end();
+});
diff --git a/test/functional/build_locales_md5map.js b/test/functional/build_locales_md5map.js
new file mode 100644
index 000000000..98d99dc6f
--- /dev/null
+++ b/test/functional/build_locales_md5map.js
@@ -0,0 +1,16 @@
+var fs = require('fs');
+var path = require('path');
+var tap = require('tap');
+
+var buildLocales = require('../../bin/lib/locale-compare');
+
+tap.test('buildLocalesFile', function (t) {
+    var actualMd5map = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../fixtures/test_es_md5map.json'), 'utf8'));
+    var templates = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../fixtures/build_locales.json'), 'utf8'));
+    var testMd5map = buildLocales.getMD5Map(templates);
+
+    for (var key in actualMd5map) {
+        t.ok(testMd5map[key] !== undefined);
+    }
+    t.end();
+});
diff --git a/test/functional/build_locales_mergeTranslations.js b/test/functional/build_locales_mergeTranslations.js
new file mode 100644
index 000000000..47d6e512d
--- /dev/null
+++ b/test/functional/build_locales_mergeTranslations.js
@@ -0,0 +1,23 @@
+var tap = require('tap');
+
+var buildLocales = require('../../bin/lib/locale-compare');
+
+tap.test('buildLocalesMergeTranslations', function (t) {
+    var existingTranslations = {
+        'test.test1': 'It\'s like raaayaaain, on your wedding day',
+        'test.test2': 'Free to flyyy, when you already paid'
+    };
+    var newTranslations = {
+        'Isn\'t it ironic? No.': 'Es irónico? No.'
+    };
+    var md5map = {
+        'c21ce5ceefe167028182032d4255a384': 'test.test1',
+        '9c40648034e467e16f8d6ae24bd610ab': 'test.test2',
+        '6885a345adafb3a9dd43d9f549430c88': 'test.test3'
+    };
+
+    var mergedTranslations = buildLocales.mergeNewTranslations(existingTranslations, newTranslations, {}, md5map);
+    t.ok(mergedTranslations['test.test3'] !== undefined);
+    t.ok(mergedTranslations['test.test2'] !== undefined);
+    t.end();
+});
diff --git a/test/unit/build_locales_getmd5.js b/test/unit/build_locales_getmd5.js
new file mode 100644
index 000000000..fd9b7ff06
--- /dev/null
+++ b/test/unit/build_locales_getmd5.js
@@ -0,0 +1,11 @@
+var tap = require('tap');
+
+var buildLocales = require('../../bin/lib/locale-compare');
+
+tap.test('buildLocalesGetMD5', function (t) {
+    var testString1 = 'are there bears here?';
+    var testString2 = 'are\nthere\tbears here?';
+    
+    t.equal(buildLocales.getMD5(testString1), buildLocales.getMD5(testString2));
+    t.end();
+});
diff --git a/webpack.config.js b/webpack.config.js
index 367309dc3..d793e2c9b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -44,10 +44,6 @@ module.exports = {
             {
                 test: /\.(png|jpg|gif|eot|svg|ttf|woff)$/,
                 loader: 'url-loader'
-            },
-            {
-                test: /\.intl$/,
-                loader: 'scratch-www-intl-loader'
             }
         ]
     },
@@ -56,7 +52,8 @@ module.exports = {
     },
     plugins: [
         new CopyWebpackPlugin([
-            {from: 'static'}
+            {from: 'static'},
+            {from: 'intl', to: 'js/intl'}
         ]),
         new webpack.optimize.UglifyJsPlugin({
             compress: {