diff --git a/.babelrc b/.babelrc
index 2e236249..3e9bd38e 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,4 +1,9 @@
{
- "plugins": ["transform-object-rest-spread"],
- "presets": ["es2015", "react"]
+ "plugins": [
+ "transform-object-rest-spread",
+ ["react-intl", {
+ "messagesDir": "./translations/messages/"
+ }]
+ ],
+ "presets": ["es2015", "react"],
}
diff --git a/.eslintignore b/.eslintignore
index fe1b4898..217e2872 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
node_modules/*
dist/*
playground/
+scripts/*
diff --git a/.gitignore b/.gitignore
index 1b3b3bf7..c6c5feec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,7 @@ dist/*
# Editors
/#*
*~
+
+# generated translation files
+/translations
+/locale
diff --git a/package.json b/package.json
index ba1ba054..1d7fd76a 100644
--- a/package.json
+++ b/package.json
@@ -4,11 +4,13 @@
"description": "Graphical User Interface for the Scratch 3.0 paint editor, which is used to make and edit sprites for use in projects.",
"main": "./dist/scratch-paint.js",
"scripts": {
- "build": "npm run clean && webpack --progress --colors --bail",
+ "build": "npm run clean && npm run i18n:msgs && webpack --progress --colors --bail",
"clean": "rimraf ./dist && mkdirp dist && rimraf ./playground && mkdirp playground",
"deploy": "touch playground/.nojekyll && gh-pages -t -d playground -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": "webpack-dev-server",
+ "start": "npm run i18n:msgs && webpack-dev-server",
"test": "npm run lint && npm run build && npm run unit",
"unit": "jest",
"watch": "webpack --progress --colors --watch"
@@ -26,10 +28,12 @@
},
"devDependencies": {
"autoprefixer": "7.1.1",
+ "babel-cli": "6.24.1",
"babel-core": "^6.23.1",
"babel-eslint": "^7.1.1",
"babel-jest": "^20.0.3",
"babel-loader": "^7.0.0",
+ "babel-plugin-react-intl": "2.3.1",
"babel-plugin-transform-object-rest-spread": "^6.22.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.22.0",
@@ -57,6 +61,7 @@
"react": "15.6.1",
"react-dom": "15.5.4",
"react-intl": "2.3.0",
+ "react-intl-redux": "0.6.0",
"react-redux": "5.0.5",
"react-test-renderer": "^15.5.4",
"redux": "3.6.0",
diff --git a/scripts/build-i18n-source.js b/scripts/build-i18n-source.js
new file mode 100755
index 00000000..9f6571c1
--- /dev/null
+++ b/scripts/build-i18n-source.js
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const glob = require('glob');
+const path = require('path');
+const mkdirp = require('mkdirp');
+
+var args = process.argv.slice(2);
+
+if (!args.length) {
+ process.stdout.write('You must specify the messages dir generated by babel-plugin-react-intl.\n');
+ process.exit(1);
+}
+
+const MESSAGES_PATTERN = args.shift() + '/**/*.json';
+
+if (!args.length) {
+ process.stdout.write('A destination directory must be specified.\n');
+ process.exit(1);
+}
+
+const LANG_DIR = args.shift();
+
+// Aggregates the default messages that were extracted from the example app's
+// React components via the React Intl Babel plugin. An error will be thrown if
+// there are messages in different components that use the same `id`. The result
+// is a chromei18n format collection of `id: {message: defaultMessage,
+// description: description}` pairs for the app's default locale.
+let defaultMessages = glob.sync(MESSAGES_PATTERN)
+ .map((filename) => fs.readFileSync(filename, 'utf8'))
+ .map((file) => JSON.parse(file))
+ .reduce((collection, descriptors) => {
+ descriptors.forEach(({id, defaultMessage, description}) => {
+ if (collection.hasOwnProperty(id)) {
+ throw new Error(`Duplicate message id: ${id}`);
+ }
+
+ collection[id] = {message: defaultMessage, description: description};
+ });
+
+ return collection;
+ }, {});
+
+mkdirp.sync(LANG_DIR);
+fs.writeFileSync(path.join(LANG_DIR, 'en.json'), JSON.stringify(defaultMessages, null, 2));
diff --git a/scripts/generate-locale-messages.js b/scripts/generate-locale-messages.js
new file mode 100755
index 00000000..44a1c581
--- /dev/null
+++ b/scripts/generate-locale-messages.js
@@ -0,0 +1,67 @@
+#!/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'];
+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/brush-mode.jsx b/src/components/brush-mode.jsx
index 865c04f4..84a831dd 100644
--- a/src/components/brush-mode.jsx
+++ b/src/components/brush-mode.jsx
@@ -1,8 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
const BrushModeComponent = props => (
-
+
);
BrushModeComponent.propTypes = {
diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx
index 44c6a90e..e751030f 100644
--- a/src/containers/brush-mode.jsx
+++ b/src/containers/brush-mode.jsx
@@ -74,8 +74,8 @@ BrushMode.propTypes = {
};
const mapStateToProps = state => ({
- brushModeState: state.brushMode,
- isBrushModeActive: state.mode === Modes.BRUSH
+ brushModeState: state.scratchPaint.brushMode,
+ isBrushModeActive: state.scratchPaint.mode === Modes.BRUSH
});
const mapDispatchToProps = dispatch => ({
changeBrushSize: brushSize => {
diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx
index c8ad4a3b..af932329 100644
--- a/src/containers/eraser-mode.jsx
+++ b/src/containers/eraser-mode.jsx
@@ -70,8 +70,8 @@ EraserMode.propTypes = {
};
const mapStateToProps = state => ({
- eraserModeState: state.eraserMode,
- isEraserModeActive: state.mode === Modes.ERASER
+ eraserModeState: state.scratchPaint.eraserMode,
+ isEraserModeActive: state.scratchPaint.mode === Modes.ERASER
});
const mapDispatchToProps = dispatch => ({
changeBrushSize: brushSize => {
diff --git a/src/index.js b/src/index.js
index 6611b719..b7d91a70 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,7 @@
import PaintEditor from './containers/paint-editor.jsx';
+import ScratchPaintReducer from './reducers/scratch-paint-reducer';
-export default PaintEditor;
+export {
+ PaintEditor as default,
+ ScratchPaintReducer
+};
diff --git a/src/locale.js b/src/locale.js
new file mode 100644
index 00000000..fb1e72e9
--- /dev/null
+++ b/src/locale.js
@@ -0,0 +1,11 @@
+import localeDataEn from 'react-intl/locale-data/en';
+
+import messages from '../locale/messages.json'; // eslint-disable-line import/no-unresolved
+
+export default {
+ en: {
+ name: 'English',
+ localeData: localeDataEn,
+ messages: messages.en
+ }
+};
diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx
index 8a27161e..9f68704a 100644
--- a/src/playground/playground.jsx
+++ b/src/playground/playground.jsx
@@ -3,16 +3,20 @@ import ReactDOM from 'react-dom';
import PaintEditor from '..';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
-import reducer from '../reducers/combine-reducers';
+import reducer from './reducers/combine-reducers';
+import {intlInitialState, IntlProvider} from './reducers/intl.js';
const appTarget = document.createElement('div');
document.body.appendChild(appTarget);
const store = createStore(
reducer,
+ intlInitialState,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render((
-
+
+
+
), appTarget);
diff --git a/src/playground/reducers/combine-reducers.js b/src/playground/reducers/combine-reducers.js
new file mode 100644
index 00000000..e3182b5e
--- /dev/null
+++ b/src/playground/reducers/combine-reducers.js
@@ -0,0 +1,8 @@
+import {combineReducers} from 'redux';
+import intlReducer from './intl';
+import {ScratchPaintReducer} from '../..';
+
+export default combineReducers({
+ intl: intlReducer,
+ scratchPaint: ScratchPaintReducer
+});
diff --git a/src/playground/reducers/intl.js b/src/playground/reducers/intl.js
new file mode 100644
index 00000000..a9d43eda
--- /dev/null
+++ b/src/playground/reducers/intl.js
@@ -0,0 +1,30 @@
+import {addLocaleData} from 'react-intl';
+import {updateIntl as superUpdateIntl} from 'react-intl-redux';
+import {IntlProvider, intlReducer} from 'react-intl-redux';
+
+import locales from '../../locale.js';
+
+Object.keys(locales).forEach(locale => {
+ // TODO: will need to handle locales not in the default intl - see www/custom-locales
+ addLocaleData(locales[locale].localeData);
+});
+
+const intlInitialState = {
+ intl: {
+ defaultLocale: 'en',
+ locale: 'en',
+ messages: locales.en.messages
+ }
+};
+
+const updateIntl = locale => superUpdateIntl({
+ locale: locale,
+ messages: locales[locale].messages || locales.en.messages
+});
+
+export {
+ intlReducer as default,
+ IntlProvider,
+ intlInitialState,
+ updateIntl
+};
diff --git a/src/reducers/combine-reducers.js b/src/reducers/scratch-paint-reducer.js
similarity index 100%
rename from src/reducers/combine-reducers.js
rename to src/reducers/scratch-paint-reducer.js