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 af441a9d..61fed8ce 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) { @@ -23,6 +24,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) { @@ -50,18 +55,22 @@ App.propTypes = { }), }).isRequired, setPreviousPath: PropTypes.func.isRequired, + setLanguage: PropTypes.func.isRequired, + language: PropTypes.string, theme: PropTypes.string, }; App.defaultProps = { children: null, + 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 252006b3..107f874f 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'; @@ -144,13 +145,11 @@ class IDEView extends React.Component { this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props)); } } - componentWillUnmount() { document.removeEventListener('keydown', this.handleGlobalKeydown, false); clearTimeout(this.autosaveInterval); this.autosaveInterval = null; } - handleGlobalKeydown(e) { // 83 === s if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { @@ -367,6 +366,7 @@ class IDEView extends React.Component { expandConsole={this.props.expandConsole} clearConsole={this.props.clearConsole} cmController={this.cmController} + language={this.props.preferences.language} /> @@ -527,7 +527,8 @@ IDEView.propTypes = { gridOutput: PropTypes.bool.isRequired, soundOutput: PropTypes.bool.isRequired, theme: PropTypes.string.isRequired, - autorefresh: PropTypes.bool.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/server/models/user.js b/server/models/user.js index c4097e87..ae019c63 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -65,7 +65,8 @@ const userSchema = new Schema({ gridOutput: { type: Boolean, default: false }, soundOutput: { type: Boolean, default: false }, theme: { type: String, default: 'light' }, - autorefresh: { type: Boolean, default: false } + autorefresh: { type: Boolean, default: false }, + language: { type: String, default: 'en-US' } }, totalSize: { type: Number, default: 0 } }, { timestamps: true, usePushEach: true });