From 3333dd41fabe768fa466615177f434be8c435b8a Mon Sep 17 00:00:00 2001 From: ov Date: Mon, 17 Aug 2020 10:23:58 +0100 Subject: [PATCH 1/2] Persistence Language Functionality to Store Language in User Preferences (#1536) * Entry points to introduce persistence in language selection * setLanguage action changes both the state and the i18next language * Ensure language change applies to all pages on load Co-authored-by: Andrew Nicolaou --- client/components/Nav.jsx | 12 ++++++------ client/components/__test__/Nav.test.jsx | 3 ++- client/constants.js | 1 + client/modules/App/App.jsx | 11 ++++++++++- client/modules/IDE/actions/preferences.js | 20 ++++++++++++++++++++ client/modules/IDE/pages/IDEView.jsx | 7 ++++--- client/modules/IDE/reducers/preferences.js | 7 ++++++- client/modules/User/actions.js | 6 ++++-- server/models/user.js | 3 ++- 9 files changed, 55 insertions(+), 15 deletions(-) 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 }); From 1eddeef528ab1163189452899d924016f29cba26 Mon Sep 17 00:00:00 2001 From: ov Date: Mon, 17 Aug 2020 10:51:59 +0100 Subject: [PATCH 2/2] Signup form: Spanish Translation (#1550) * SignupForms and view Co-authored-by: Andrew Nicolaou --- client/modules/User/components/SignupForm.jsx | 24 ++++++++++--------- client/modules/User/pages/SignupView.jsx | 18 +++++++------- translations/locales/en-US/translations.json | 18 ++++++++++++++ translations/locales/es-419/translations.json | 18 ++++++++++++++ 4 files changed, 59 insertions(+), 19 deletions(-) diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index 7d18062b..24202d74 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'; @@ -13,10 +14,10 @@ function SignupForm(props) { return (

- + {username.error}}

- + {email.error}}

- + {password.error}}

- + @@ -63,7 +64,7 @@ function SignupForm(props) {

); @@ -81,7 +82,8 @@ SignupForm.propTypes = { submitting: PropTypes.bool, invalid: PropTypes.bool, pristine: PropTypes.bool, - previousPath: PropTypes.string.isRequired + previousPath: PropTypes.string.isRequired, + t: PropTypes.func.isRequired }; SignupForm.defaultProps = { @@ -90,4 +92,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 {