Merge pull request #6 from fsih/addI18n

Add i18n support to paint editor
This commit is contained in:
DD Liu 2017-08-25 11:09:16 -04:00 committed by GitHub
commit 5b756c9e3a
15 changed files with 203 additions and 12 deletions

View file

@ -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"],
}

View file

@ -1,3 +1,4 @@
node_modules/*
dist/*
playground/
scripts/*

4
.gitignore vendored
View file

@ -12,3 +12,7 @@ dist/*
# Editors
/#*
*~
# generated translation files
/translations
/locale

View file

@ -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",

45
scripts/build-i18n-source.js Executable file
View file

@ -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));

View file

@ -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));

View file

@ -1,8 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
const BrushModeComponent = props => (
<button onClick={props.onMouseDown}>Brush</button>
<button onClick={props.onMouseDown}>
<FormattedMessage
defaultMessage="Brush"
description="Label for the brush tool"
id="paint.brushMode.brush"
/>
</button>
);
BrushModeComponent.propTypes = {

View file

@ -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 => {

View file

@ -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 => {

View file

@ -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
};

11
src/locale.js Normal file
View file

@ -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
}
};

View file

@ -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((
<Provider store={store}>
<IntlProvider>
<PaintEditor />
</IntlProvider>
</Provider>
), appTarget);

View file

@ -0,0 +1,8 @@
import {combineReducers} from 'redux';
import intlReducer from './intl';
import {ScratchPaintReducer} from '../..';
export default combineReducers({
intl: intlReducer,
scratchPaint: ScratchPaintReducer
});

View file

@ -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
};