diff --git a/package.json b/package.json index c51758645a007e87d64233a070982d4bfdcd8b55..c41835048ff4df542c9e64af7ce2b788eb455ca1 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "react-draggable": "3.0.5", "react-ga": "2.5.3", "react-intl": "2.4.0", - "react-intl-redux": "0.7.0", "react-modal": "3.4.4", "react-popover": "0.5.7", "react-redux": "5.0.7", @@ -94,7 +93,7 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "0.1.0-prerelease.20180618171838", - "scratch-blocks": "0.1.0-prerelease.1529499405", + "scratch-blocks": "0.1.0-prerelease.1529616842", "scratch-l10n": "3.0.20180611175036", "scratch-paint": "0.2.0-prerelease.20180619182645", "scratch-render": "0.1.0-prerelease.20180618173030", diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 03639242144a9c67369d98b9dc04831e291e3bd1..1b411d6336e9291efabdbca2959ef0f85a7f1d96 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -52,7 +52,8 @@ class Blocks extends React.Component { 'onVisualReport', 'onWorkspaceUpdate', 'onWorkspaceMetricsChange', - 'setBlocks' + 'setBlocks', + 'setLocale' ]); this.ScratchBlocks.prompt = this.handlePromptStart; this.state = { @@ -86,7 +87,7 @@ class Blocks extends React.Component { addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); this.attachVM(); - this.props.vm.setLocale(this.props.locale, this.props.messages); + this.setLocale(); analytics.pageview('/editors/blocks'); } @@ -109,7 +110,7 @@ class Blocks extends React.Component { } if (prevProps.locale !== this.props.locale) { - this.props.vm.setLocale(this.props.locale, this.props.messages); + this.setLocale(); } if (prevProps.toolboxXML !== this.props.toolboxXML) { @@ -144,6 +145,16 @@ class Blocks extends React.Component { clearTimeout(this.toolboxUpdateTimeout); } + setLocale () { + this.workspace.getFlyout().setRecyclingEnabled(false); + this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); + this.props.vm.setLocale(this.props.locale, this.props.messages); + + this.workspace.updateToolbox(this.props.toolboxXML); + this.props.vm.refreshWorkspace(); + this.workspace.getFlyout().setRecyclingEnabled(true); + } + updateToolbox () { this.toolboxUpdateTimeout = false; @@ -461,8 +472,8 @@ const mapStateToProps = state => ({ state.scratchGui.mode.isFullScreen ), extensionLibraryVisible: state.scratchGui.modals.extensionLibrary, - locale: state.intl.locale, - messages: state.intl.messages, + locale: state.locales.locale, + messages: state.locales.messages, toolboxXML: state.scratchGui.toolbox.toolboxXML, customProceduresVisible: state.scratchGui.customProcedures.active }); diff --git a/src/containers/language-selector.jsx b/src/containers/language-selector.jsx index 49fb60943ada6bdefa77f95548dc40e2b3080d79..c3f108efbe3315dd732a3b450479efddc7bec00d 100644 --- a/src/containers/language-selector.jsx +++ b/src/containers/language-selector.jsx @@ -2,7 +2,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; -import {updateIntl} from 'react-intl-redux'; +import {selectLocale} from '../reducers/locales'; import {closeLanguageMenu} from '../reducers/menus'; import LanguageSelectorComponent from '../components/language-selector/language-selector.jsx'; @@ -15,11 +15,15 @@ class LanguageSelector extends React.Component { ]); } handleChange (e) { - this.props.onChangeLanguage(e.target.value); + const newLocale = e.target.value; + if (this.props.supportedLocales.includes(newLocale)) { + this.props.onChangeLanguage(newLocale); + } } render () { const { onChangeLanguage, // eslint-disable-line no-unused-vars + supportedLocales, // eslint-disable-line no-unused-vars children, ...props } = this.props; @@ -36,16 +40,19 @@ class LanguageSelector extends React.Component { LanguageSelector.propTypes = { children: PropTypes.node, - onChangeLanguage: PropTypes.func.isRequired + currentLocale: PropTypes.string.isRequired, + onChangeLanguage: PropTypes.func.isRequired, + supportedLocales: PropTypes.arrayOf(PropTypes.string) }; const mapStateToProps = state => ({ - currentLocale: state.intl.locale + currentLocale: state.locales.locale, + supportedLocales: Object.keys(state.locales.messagesByLocale) }); const mapDispatchToProps = dispatch => ({ onChangeLanguage: locale => { - dispatch(updateIntl({locale: locale, messages: {}})); + dispatch(selectLocale(locale)); dispatch(closeLanguageMenu()); } }); diff --git a/src/containers/menu.jsx b/src/containers/menu.jsx index 865ad19d3d1181b62c0125a7efbc7ca8c7302b57..e9a51cd4ced969134675d81635388e16c0d5507a 100644 --- a/src/containers/menu.jsx +++ b/src/containers/menu.jsx @@ -13,12 +13,17 @@ class Menu extends React.Component { 'handleClick', 'ref' ]); - if (props.open) this.addListeners(); + } + componentDidMount () { + if (this.props.open) this.addListeners(); } componentDidUpdate (prevProps) { if (this.props.open && !prevProps.open) this.addListeners(); if (!this.props.open && prevProps.open) this.removeListeners(); } + componentWillUnmount () { + this.removeListeners(); + } addListeners () { document.addEventListener('mouseup', this.handleClick); } diff --git a/src/index.js b/src/index.js index 3bfa737f52db778b0196009bcfecb83c96f94795..dfd32a399de9c132135e2cb0f0596bd8eecc74a2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,12 @@ import GUI from './containers/gui.jsx'; import GuiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from './reducers/gui'; +import LocalesReducer, {localesInitialState, initLocale} from './reducers/locales'; import {ScratchPaintReducer} from 'scratch-paint'; -import IntlReducer from './reducers/intl'; import {setFullScreen, setPlayer} from './reducers/mode'; import {setAppElement} from 'react-modal'; const guiReducers = { - intl: IntlReducer, + locales: LocalesReducer, scratchGui: GuiReducer, scratchPaint: ScratchPaintReducer }; @@ -19,6 +19,8 @@ export { guiMiddleware, initPlayer, initFullScreen, + initLocale, + localesInitialState, setFullScreen, setPlayer }; diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx index dc62aa31bca70f8a05ea90cc698d8050c811ea7c..20e2852a131c5ad70a878d4d0d4b3038df6c8548 100644 --- a/src/lib/app-state-hoc.jsx +++ b/src/lib/app-state-hoc.jsx @@ -2,12 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Provider} from 'react-redux'; import {createStore, combineReducers, compose} from 'redux'; - -import {intlShape} from 'react-intl'; -import {IntlProvider, updateIntl} from 'react-intl-redux'; -import intlReducer from '../reducers/intl.js'; +import ConnectedIntlProvider from './connected-intl-provider.jsx'; import guiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from '../reducers/gui'; +import localesReducer, {initLocale, localesInitialState} from '../reducers/locales'; import {setPlayer, setFullScreen} from '../reducers/mode.js'; @@ -33,21 +31,28 @@ const AppStateHOC = function (WrappedComponent) { if (props.isPlayerOnly) { initializedGui = initPlayer(initializedGui); } + + let initializedLocales = localesInitialState; + if (window.location.search.indexOf('locale=') !== -1 || + window.location.search.indexOf('lang=') !== -1) { + const locale = window.location.search.match(/(?:locale|lang)=([\w]+)/)[1]; + initializedLocales = initLocale(initializedLocales, locale); + } + const reducer = combineReducers({ - intl: intlReducer, + locales: localesReducer, scratchGui: guiReducer, scratchPaint: ScratchPaintReducer }); - this.store = createStore( reducer, - {scratchGui: initializedGui}, + { + locales: initializedLocales, + scratchGui: initializedGui + }, enhancer); } componentDidUpdate (prevProps) { - if (prevProps.intl !== this.props.intl) { - this.store.dispatch(updateIntl(this.props.intl)); - } if (prevProps.isPlayerOnly !== this.props.isPlayerOnly) { this.store.dispatch(setPlayer(this.props.isPlayerOnly)); } @@ -57,22 +62,20 @@ const AppStateHOC = function (WrappedComponent) { } render () { const { - intl, // eslint-disable-line no-unused-vars isFullScreen, // eslint-disable-line no-unused-vars isPlayerOnly, // eslint-disable-line no-unused-vars ...componentProps } = this.props; return ( <Provider store={this.store}> - <IntlProvider> + <ConnectedIntlProvider> <WrappedComponent {...componentProps} /> - </IntlProvider> + </ConnectedIntlProvider> </Provider> ); } } AppStateWrapper.propTypes = { - intl: intlShape, isFullScreen: PropTypes.bool, isPlayerOnly: PropTypes.bool }; diff --git a/src/lib/connected-intl-provider.jsx b/src/lib/connected-intl-provider.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e4150ef1b39fe73dd1e5db92c1ab427cb16298d5 --- /dev/null +++ b/src/lib/connected-intl-provider.jsx @@ -0,0 +1,10 @@ +import {IntlProvider as ReactIntlProvider} from 'react-intl'; +import {connect} from 'react-redux'; + +const mapStateToProps = state => ({ + key: state.locales.locale, + locale: state.locales.locale, + messages: state.locales.messages +}); + +export default connect(mapStateToProps)(ReactIntlProvider); diff --git a/src/lib/hash-parser-hoc.jsx b/src/lib/hash-parser-hoc.jsx index 04250aa6e9f1297f662574d649469e3bb610c333..f3ac92542808f2e6e7ee1f9acbdf3978c3b4e7c8 100644 --- a/src/lib/hash-parser-hoc.jsx +++ b/src/lib/hash-parser-hoc.jsx @@ -24,9 +24,9 @@ const HashParserHOC = function (WrappedComponent) { window.removeEventListener('hashchange', this.handleHashChange); } handleHashChange () { - let projectId = window.location.hash.substring(1); + const hashMatch = window.location.hash.match(/#(\d+)/); + const projectId = hashMatch === null ? 0 : hashMatch[1]; if (projectId !== this.state.projectId) { - if (projectId.length < 1) projectId = 0; this.setState({projectId: projectId}); } } diff --git a/src/playground/intl.js b/src/playground/intl.js deleted file mode 100644 index 42b72b003e59fed430ea5ae7d1a73f50e9f40bad..0000000000000000000000000000000000000000 --- a/src/playground/intl.js +++ /dev/null @@ -1,19 +0,0 @@ -import {addLocaleData} from 'react-intl'; - -import localeData from 'scratch-l10n'; -import editorMessages from 'scratch-l10n/locales/editor-msgs'; - -Object.keys(localeData).forEach(locale => { - addLocaleData(localeData[locale].localeData); -}); - -const intlDefault = { - defaultLocale: 'en', - locale: 'en', - messages: editorMessages.en -}; - -export { - intlDefault as default, - editorMessages -}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 829f0b0446171e56ca2c3157ea316c5611727d2a..7d98ae2f3f74a757bc101910bbabe6fd491fc192 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -58,6 +58,7 @@ const initFullScreen = function (currentState) { }} ); }; + const guiReducer = combineReducers({ assetDrag: assetDragReducer, blockDrag: blockDragReducer, diff --git a/src/reducers/intl.js b/src/reducers/intl.js deleted file mode 100644 index 8c49ee10475c5a960c35d6eba723203dbb772091..0000000000000000000000000000000000000000 --- a/src/reducers/intl.js +++ /dev/null @@ -1,14 +0,0 @@ -import {intlReducer} from 'react-intl-redux'; - -const intlInitialState = { - intl: { - defaultLocale: 'en', - locale: 'en', - messages: {} - } -}; - -export { - intlReducer as default, - intlInitialState -}; diff --git a/src/reducers/locales.js b/src/reducers/locales.js new file mode 100644 index 0000000000000000000000000000000000000000..fda8f9dc36d14dac09f72fad62f704617b87e0cf --- /dev/null +++ b/src/reducers/locales.js @@ -0,0 +1,71 @@ +import {addLocaleData} from 'react-intl'; + +import {localeData} from 'scratch-l10n'; +import editorMessages from 'scratch-l10n/locales/editor-msgs'; + +addLocaleData(localeData); + +const UPDATE_LOCALES = 'scratch-gui/locales/UPDATE_LOCALES'; +const SELECT_LOCALE = 'scratch-gui/locales/SELECT_LOCALE'; + +const initialState = { + locale: 'en', + messagesByLocale: editorMessages, + messages: editorMessages.en +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SELECT_LOCALE: + return Object.assign({}, state, { + locale: action.locale, + messagesByLocale: state.messagesByLocale, + messages: state.messagesByLocale[action.locale] + }); + case UPDATE_LOCALES: + return Object.assign({}, state, { + locale: state.locale, + messagesByLocale: action.messagesByLocale, + messages: action.messagesByLocale[state.locale] + }); + default: + return state; + } +}; + +const selectLocale = function (locale) { + return { + type: SELECT_LOCALE, + locale: locale + }; +}; + +const setLocales = function (localesMessages) { + return { + type: UPDATE_LOCALES, + messagesByLocale: localesMessages + }; +}; +const initLocale = function (currentState, locale) { + if (currentState.messagesByLocale.hasOwnProperty(locale)) { + return Object.assign( + {}, + currentState, + { + locale: locale, + messagesByLocale: currentState.messagesByLocale, + messages: currentState.messagesByLocale[locale] + } + ); + } + // don't change locale if it's not in the current messages + return currentState; +}; +export { + reducer as default, + initialState as localesInitialState, + initLocale, + selectLocale, + setLocales +}; diff --git a/test/unit/util/hash-project-loader-hoc.test.jsx b/test/unit/util/hash-project-loader-hoc.test.jsx index 5cf78747ad9b580c2aa506f1e949daa57f7f26e1..1242385d62241588f60b89ea9be57f169f1516fd 100644 --- a/test/unit/util/hash-project-loader-hoc.test.jsx +++ b/test/unit/util/hash-project-loader-hoc.test.jsx @@ -8,9 +8,9 @@ describe('HashParserHOC', () => { test('when there is a hash, it passes the hash as projectId', () => { const Component = ({projectId}) => <div>{projectId}</div>; const WrappedComponent = HashParserHOC(Component); - window.location.hash = '#winning'; + window.location.hash = '#1234567'; const mounted = mount(<WrappedComponent />); - expect(mounted.state().projectId).toEqual('winning'); + expect(mounted.state().projectId).toEqual('1234567'); }); test('when there is no hash, it passes 0 as the projectId', () => { @@ -21,14 +21,22 @@ describe('HashParserHOC', () => { expect(mounted.state().projectId).toEqual(0); }); + test('when the hash is not a number, it passes 0 as projectId', () => { + const Component = ({projectId}) => <div>{projectId}</div>; + const WrappedComponent = HashParserHOC(Component); + window.location.hash = '#winning'; + const mounted = mount(<WrappedComponent />); + expect(mounted.state().projectId).toEqual(0); + }); + test('when hash change happens, the projectId state is changed', () => { const Component = ({projectId}) => <div>{projectId}</div>; const WrappedComponent = HashParserHOC(Component); window.location.hash = ''; const mounted = mount(<WrappedComponent />); expect(mounted.state().projectId).toEqual(0); - window.location.hash = '#winning'; + window.location.hash = '#1234567'; mounted.instance().handleHashChange(); - expect(mounted.state().projectId).toEqual('winning'); + expect(mounted.state().projectId).toEqual('1234567'); }); });