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: {