diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 4f315bb3..4056607f 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -9,7 +9,7 @@ import i18next from 'i18next'; import * as IDEActions from '../modules/IDE/actions/ide'; import * as toastActions from '../modules/IDE/actions/toast'; import * as projectActions from '../modules/IDE/actions/project'; -import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences'; +import { setAllAccessibleOutput, setLanguage } from '../modules/IDE/actions/preferences'; import { logoutUser } from '../modules/User/actions'; import getConfig from '../utils/getConfig'; @@ -72,7 +72,6 @@ class Nav extends React.PureComponent { document.removeEventListener('mousedown', this.handleClick, false); document.removeEventListener('keydown', this.closeDropDown, false); } - setDropdown(dropdown) { this.setState({ dropdownOpen: dropdown @@ -170,7 +169,7 @@ class Nav extends React.PureComponent { } handleLangSelection(event) { - i18next.changeLanguage(event.target.value); + this.props.setLanguage(event.target.value); this.props.showToast(1500); this.props.setToastText('Toast.LangChange'); this.setDropdown('none'); @@ -808,8 +807,8 @@ Nav.propTypes = { params: PropTypes.shape({ username: PropTypes.string }), - t: PropTypes.func.isRequired - + t: PropTypes.func.isRequired, + setLanguage: PropTypes.func.isRequired, }; Nav.defaultProps = { @@ -839,7 +838,8 @@ const mapDispatchToProps = { ...projectActions, ...toastActions, logoutUser, - setAllAccessibleOutput + setAllAccessibleOutput, + setLanguage }; export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav))); diff --git a/client/components/__test__/Nav.test.jsx b/client/components/__test__/Nav.test.jsx index ba7d3fd6..ee175fe7 100644 --- a/client/components/__test__/Nav.test.jsx +++ b/client/components/__test__/Nav.test.jsx @@ -45,7 +45,8 @@ describe('Nav', () => { rootFile: { id: 'root-file' }, - t: jest.fn() + t: jest.fn(), + setLanguage: jest.fn() }; it('renders correctly', () => { diff --git a/client/constants.js b/client/constants.js index 477409fc..a23badd8 100644 --- a/client/constants.js +++ b/client/constants.js @@ -93,6 +93,7 @@ export const SHOW_TOAST = 'SHOW_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_THEME = 'SET_THEME'; +export const SET_LANGUAGE = 'SET_LANGUAGE'; export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; export const SET_AUTOREFRESH = 'SET_AUTOREFRESH'; diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index a4ec3d7a..3b74e1fc 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import getConfig from '../../utils/getConfig'; import DevTools from './components/DevTools'; import { setPreviousPath } from '../IDE/actions/ide'; +import { setLanguage } from '../IDE/actions/preferences'; class App extends React.Component { constructor(props, context) { @@ -25,6 +26,10 @@ class App extends React.Component { if (locationWillChange && !shouldSkipRemembering) { this.props.setPreviousPath(this.props.location.pathname); } + + if (this.props.language !== nextProps.language) { + this.props.setLanguage(nextProps.language, { persistPreference: false }); + } } componentDidUpdate(prevProps) { @@ -52,18 +57,22 @@ App.propTypes = { }), }).isRequired, setPreviousPath: PropTypes.func.isRequired, + setLanguage: PropTypes.func.isRequired, + language: PropTypes.string, theme: PropTypes.string, }; App.defaultProps = { children: null, - theme: 'light', + language: null, + theme: 'light' }; const mapStateToProps = state => ({ theme: state.preferences.theme, + language: state.preferences.language, }); -const mapDispatchToProps = { setPreviousPath }; +const mapDispatchToProps = { setPreviousPath, setLanguage }; export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index a182da76..af9a8d88 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; @@ -210,3 +211,22 @@ export function setAllAccessibleOutput(value) { }; } +export function setLanguage(value, { persistPreference = true } = {}) { + return (dispatch, getState) => { + i18next.changeLanguage(value); + dispatch({ + type: ActionTypes.SET_LANGUAGE, + language: value + }); + const state = getState(); + if (persistPreference && state.user.authenticated) { + const formParams = { + preferences: { + language: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 4979eaee..9f010c16 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -35,6 +35,7 @@ import AddToCollectionList from '../components/AddToCollectionList'; import Feedback from '../components/Feedback'; import { CollectionSearchbar } from '../components/Searchbar'; + function getTitle(props) { const { id } = props.project; return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor'; @@ -167,13 +168,11 @@ class IDEView extends React.Component { warnIfUnsavedChanges(this.props)); } } - componentWillUnmount() { document.removeEventListener('keydown', this.handleGlobalKeydown, false); clearTimeout(this.autosaveInterval); this.autosaveInterval = null; } - handleGlobalKeydown(e) { // 83 === s if ( @@ -428,6 +427,7 @@ class IDEView extends React.Component { expandConsole={this.props.expandConsole} clearConsole={this.props.clearConsole} cmController={this.cmController} + language={this.props.preferences.language} /> @@ -585,6 +585,7 @@ IDEView.propTypes = { soundOutput: PropTypes.bool.isRequired, theme: PropTypes.string.isRequired, autorefresh: PropTypes.bool.isRequired, + language: PropTypes.string.isRequired }).isRequired, closePreferences: PropTypes.func.isRequired, setFontSize: PropTypes.func.isRequired, diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index 080a7343..3ed39bef 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -1,5 +1,7 @@ +import i18next from 'i18next'; import * as ActionTypes from '../../../constants'; + const initialState = { fontSize: 18, autosave: true, @@ -10,7 +12,8 @@ const initialState = { gridOutput: false, soundOutput: false, theme: 'light', - autorefresh: false + autorefresh: false, + language: 'en-US' }; const preferences = (state = initialState, action) => { @@ -37,6 +40,8 @@ const preferences = (state = initialState, action) => { return Object.assign({}, state, { autorefresh: action.value }); case ActionTypes.SET_LINE_NUMBERS: return Object.assign({}, state, { lineNumbers: action.value }); + case ActionTypes.SET_LANGUAGE: + return Object.assign({}, state, { language: action.language }); default: return state; } diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index dd54224d..d0648be3 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -2,6 +2,7 @@ import { browserHistory } from 'react-router'; import * as ActionTypes from '../../constants'; import apiClient from '../../utils/apiClient'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; +import { setLanguage } from '../IDE/actions/preferences'; import { showToast, setToastText } from '../IDE/actions/toast'; export function authError(error) { @@ -59,6 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) { type: ActionTypes.SET_PREFERENCES, preferences: response.data.preferences }); + setLanguage(response.data.preferences.language, { persistPreference: false }); dispatch(justOpenedProject()); browserHistory.push(previousPath); resolve(); @@ -80,8 +82,8 @@ export function getUser() { type: ActionTypes.SET_PREFERENCES, preferences: response.data.preferences }); - }) - .catch((error) => { + setLanguage(response.data.preferences.language, { persistPreference: false }); + }).catch((error) => { const { response } = error; const message = response.message || response.data.error; dispatch(authError(message)); diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index a2d2bd56..918e7cb3 100644 --- a/client/modules/User/components/SignupForm.jsx +++ b/client/modules/User/components/SignupForm.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { withTranslation } from 'react-i18next'; import { domOnlyProps } from '../../../utils/reduxFormUtils'; import Button from '../../../common/Button'; @@ -20,12 +21,10 @@ function SignupForm(props) { onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))} >

- +

- +

- +

- + @@ -79,8 +72,10 @@ function SignupForm(props) { {confirmPassword.error} )}

- ); @@ -99,6 +94,7 @@ SignupForm.propTypes = { invalid: PropTypes.bool, pristine: PropTypes.bool, previousPath: PropTypes.string.isRequired, + t: PropTypes.func.isRequired }; SignupForm.defaultProps = { @@ -107,4 +103,4 @@ SignupForm.defaultProps = { invalid: false, }; -export default SignupForm; +export default withTranslation()(SignupForm); diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index 225bb8e4..b646a405 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux'; import { Link, browserHistory } from 'react-router'; import { Helmet } from 'react-helmet'; import { reduxForm } from 'redux-form'; +import { withTranslation } from 'react-i18next'; import * as UserActions from '../actions'; import SignupForm from '../components/SignupForm'; import apiClient from '../../../utils/apiClient'; @@ -26,19 +27,19 @@ class SignupView extends React.Component {