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/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx
index c161d2bb..1007ca97 100644
--- a/client/modules/IDE/components/CollectionList/CollectionList.jsx
+++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx
@@ -143,9 +143,9 @@ class CollectionList extends React.Component {
{this._renderFieldHeader('name', 'Name')}
- {(!mobile) && this._renderFieldHeader('createdAt', 'Date Created')}
- {this._renderFieldHeader('updatedAt', 'Date Updated')}
- {this._renderFieldHeader('numItems', '# sketches')}
+ {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
+ {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
+ {this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')}
diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx
index f3119632..de20cea9 100644
--- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx
+++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx
@@ -213,7 +213,7 @@ class CollectionListRowBase extends React.Component {
{this.renderCollectionName()}
- {(!mobile) &&
{format(new Date(collection.createdAt), 'MMM D, YYYY')} }
+ {mobile && 'Created: '}{format(new Date(collection.createdAt), 'MMM D, YYYY')}
{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}
{mobile && '# sketches: '}{(collection.items || []).length}
diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx
index 4aacd8c2..d5f9d71d 100644
--- a/client/modules/IDE/components/SketchList.jsx
+++ b/client/modules/IDE/components/SketchList.jsx
@@ -437,8 +437,8 @@ class SketchList extends React.Component {
{this._renderFieldHeader('name', 'Sketch')}
- {this._renderFieldHeader('createdAt', 'Date Created')}
- {this._renderFieldHeader('updatedAt', 'Date Updated')}
+ {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)}
+ {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)}
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index 9b832d42..1dcae539 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 (
@@ -389,6 +388,7 @@ class IDEView extends React.Component {
expandConsole={this.props.expandConsole}
clearConsole={this.props.clearConsole}
cmController={this.cmController}
+ language={this.props.preferences.language}
/>
@@ -538,7 +538,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/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx
index 7bae38f4..270faee7 100644
--- a/client/modules/Mobile/MobileDashboardView.jsx
+++ b/client/modules/Mobile/MobileDashboardView.jsx
@@ -24,9 +24,11 @@ import Loader from '../App/components/loader';
const EXAMPLE_USERNAME = 'p5';
+// @ghalestrilo 08/13/2020: I'm sorry
const ContentWrapper = styled(Content)`
table {
table-layout: fixed;
+ margin-bottom: ${remSize(120)}
}
td ,thead button {
@@ -55,14 +57,18 @@ const ContentWrapper = styled(Content)`
tbody td { justify-self: start; text-align: start; padding: 0 }
tbody td:nth-child(2) { justify-self: start; text-align: start; padding-left: ${remSize(12)}};
- tbody td:last-child { justify-self: end; text-align: end; };
+ tbody td:last-child {
+ justify-self: end;
+ text-align: end;
+ grid-row-start: 1;
+ grid-column-start: 3;
+ };
.sketch-list__dropdown-column { width: auto; };
tbody { height: ${remSize(48)}; }
.sketches-table-container {
- padding-bottom: ${remSize(160)};
background: ${prop('SketchList.background')};
}
.sketches-table__row {
@@ -79,18 +85,33 @@ const ContentWrapper = styled(Content)`
};
thead tr {
- grid-template-columns: 1fr 1fr 1fr 0fr;
+ grid-template-columns: repeat(${props => props.fieldcount}, 1fr) 0fr;
+ ${props => props.noheader && 'display: none;'}
}
tbody tr {
padding: ${remSize(8)};
border-radius: ${remSize(4)};
- grid-template-columns: 5fr 5fr 1fr;
+ grid-template-columns: repeat(${props => props.fieldcount - 1}) 1fr;
grid-template-areas: "name name name" "content content content";
+ grid-row-gap: ${remSize(12)}
}
.loader-container { position: fixed ; padding-bottom: 32% }
+ .sketches-table thead th {
+ background-color: transparent;
+ }
+
+ .asset-table thead th {
+ height: initial;
+ align-self: center;
+ }
+
+ .asset-table thead tr {
+ height: ${remSize(32)}
+ }
+
`;
const Subheader = styled.div`
@@ -168,7 +189,7 @@ const MobileDashboard = ({ params, location }) => {
-
+
{panel === Tabs[0] && }
{panel === Tabs[1] && }
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/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx
index b3a1a4b4..e5c7577f 100644
--- a/client/modules/User/components/APIKeyForm.jsx
+++ b/client/modules/User/components/APIKeyForm.jsx
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
-
import Button from '../../../common/Button';
import { PlusIcon } from '../../../common/icons';
import CopyableInput from '../../IDE/components/CopyableInput';
@@ -12,7 +11,7 @@ export const APIKeyPropType = PropTypes.shape({
token: PropTypes.object, // eslint-disable-line
label: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
- lastUsedAt: PropTypes.string,
+ lastUsedAt: PropTypes.string
});
class APIKeyForm extends React.Component {
@@ -39,7 +38,7 @@ class APIKeyForm extends React.Component {
}
removeKey(key) {
- const message = `Are you sure you want to delete "${key.label}"?`;
+ const message = this.props.t('APIKeyForm.ConfirmDelete', { key_label: key.label });
if (window.confirm(message)) {
this.props.removeApiKey(key.id);
@@ -51,10 +50,10 @@ class APIKeyForm extends React.Component {
if (hasApiKeys) {
return (
-
+
);
}
- return You have no exsiting tokens.
;
+ return {this.props.t('APIKeyForm.NoTokens')}
;
}
render() {
@@ -63,27 +62,18 @@ class APIKeyForm extends React.Component {
return (
- Personal Access Tokens act like your password to allow automated
- scripts to access the Editor API. Create a token for each script that
- needs access.
+ {this.props.t('APIKeyForm.Summary')}
-
Create new token
+
{this.props.t('APIKeyForm.CreateToken')}
- {keyWithToken && (
-
-
- Your new access token
-
-
- Make sure to copy your new personal access token now. You won’t
- be able to see it again!
-
-
-
- )}
+ {
+ keyWithToken && (
+
+
{this.props.t('APIKeyForm.NewTokenTitle')}
+
+ {this.props.t('APIKeyForm.NewTokenInfo')}
+
+
+
+ )
+ }
-
Existing tokens
+ {this.props.t('APIKeyForm.ExistingTokensTitle')}
{this.renderApiKeys()}
@@ -127,6 +113,7 @@ APIKeyForm.propTypes = {
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
createApiKey: PropTypes.func.isRequired,
removeApiKey: PropTypes.func.isRequired,
+ t: PropTypes.func.isRequired
};
export default APIKeyForm;
diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.jsx
index 9201aa4b..2c2e39ac 100644
--- a/client/modules/User/components/APIKeyList.jsx
+++ b/client/modules/User/components/APIKeyList.jsx
@@ -8,22 +8,22 @@ import { APIKeyPropType } from './APIKeyForm';
import TrashCanIcon from '../../../images/trash-can.svg';
-function APIKeyList({ apiKeys, onRemove }) {
+function APIKeyList({ apiKeys, onRemove, t }) {
return (
- Name
- Created on
- Last used
- Actions
+ {t('APIKeyList.Name')}
+ {t('APIKeyList.Created')}
+ {t('APIKeyList.LastUsed')}
+ {t('APIKeyList.Actions')}
{orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => {
const lastUsed = key.lastUsedAt ?
distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) :
- 'Never';
+ t('APIKeyList.Never');
return (
@@ -34,7 +34,7 @@ function APIKeyList({ apiKeys, onRemove }) {
onRemove(key)}
- aria-label="Delete API Key"
+ aria-label={t('APIKeyList.DeleteARIA')}
>
@@ -50,6 +50,7 @@ function APIKeyList({ apiKeys, onRemove }) {
APIKeyList.propTypes = {
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
onRemove: PropTypes.func.isRequired,
+ t: PropTypes.func.isRequired
};
export default APIKeyList;
diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx
index 90d5fc18..c1bfe6db 100644
--- a/client/modules/User/components/AccountForm.jsx
+++ b/client/modules/User/components/AccountForm.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';
@@ -14,6 +15,7 @@ function AccountForm(props) {
submitting,
invalid,
pristine,
+ t
} = props;
const handleInitiateVerification = (evt) => {
@@ -24,12 +26,10 @@ function AccountForm(props) {
return (
);
@@ -123,6 +124,7 @@ AccountForm.propTypes = {
submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool,
+ t: PropTypes.func.isRequired
};
AccountForm.defaultProps = {
@@ -131,4 +133,4 @@ AccountForm.defaultProps = {
invalid: false,
};
-export default AccountForm;
+export default withTranslation()(AccountForm);
diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx
index c971eb49..cf8b3ad9 100644
--- a/client/modules/User/components/NewPasswordForm.jsx
+++ b/client/modules/User/components/NewPasswordForm.jsx
@@ -1,16 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
-
+import { withTranslation } from 'react-i18next';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function NewPasswordForm(props) {
const {
- fields: { password, confirmPassword },
- handleSubmit,
- submitting,
- invalid,
- pristine,
+ fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine,
+ t
} = props;
return (
);
}
@@ -67,6 +58,7 @@ NewPasswordForm.propTypes = {
params: PropTypes.shape({
reset_password_token: PropTypes.string,
}).isRequired,
+ t: PropTypes.func.isRequired
};
NewPasswordForm.defaultProps = {
@@ -75,4 +67,4 @@ NewPasswordForm.defaultProps = {
submitting: false,
};
-export default NewPasswordForm;
+export default withTranslation()(NewPasswordForm);
diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx
index dc48fe73..ccd05ecc 100644
--- a/client/modules/User/components/ResetPasswordForm.jsx
+++ b/client/modules/User/components/ResetPasswordForm.jsx
@@ -1,16 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
-
+import { withTranslation } from 'react-i18next';
import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function ResetPasswordForm(props) {
const {
- fields: { email },
- handleSubmit,
- submitting,
- invalid,
- pristine,
+ fields: { email }, handleSubmit, submitting, invalid, pristine, t
} = props;
return (
);
@@ -54,8 +45,9 @@ ResetPasswordForm.propTypes = {
invalid: PropTypes.bool,
pristine: PropTypes.bool,
user: PropTypes.shape({
- resetPasswordInitiate: PropTypes.bool,
+ resetPasswordInitiate: PropTypes.bool
}).isRequired,
+ t: PropTypes.func.isRequired
};
ResetPasswordForm.defaultProps = {
@@ -64,4 +56,4 @@ ResetPasswordForm.defaultProps = {
invalid: false,
};
-export default ResetPasswordForm;
+export default withTranslation()(ResetPasswordForm);
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))}
>
-
- User Name
-
+ {props.t('SignupForm.Title')}
-
- Email
-
+ {props.t('SignupForm.Email')}
-
- Password
-
+ {props.t('SignupForm.Password')}
-
- Confirm Password
-
+ {props.t('SignupForm.ConfirmPassword')}
@@ -79,8 +72,10 @@ function SignupForm(props) {
{confirmPassword.error}
)}
-
- Sign Up
+ {props.t('SignupForm.SubmitSignup')}
);
@@ -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/AccountView.jsx b/client/modules/User/pages/AccountView.jsx
index fff8d331..f9f46cc8 100644
--- a/client/modules/User/pages/AccountView.jsx
+++ b/client/modules/User/pages/AccountView.jsx
@@ -4,6 +4,7 @@ import { reduxForm } from 'redux-form';
import { bindActionCreators } from 'redux';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { Helmet } from 'react-helmet';
+import { withTranslation } from 'react-i18next';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import AccountForm from '../components/AccountForm';
import apiClient from '../../../utils/apiClient';
@@ -16,9 +17,11 @@ function SocialLoginPanel(props) {
return (
- Social Login
+ {/* eslint-disable-next-line react/prop-types */}
+ {props.t('AccountView.SocialLogin')}
- Use your GitHub or Google account to log into the p5.js Web Editor.
+ {/* eslint-disable-next-line react/prop-types */}
+ {props.t('AccountView.SocialLoginDescription')}
@@ -39,21 +42,21 @@ class AccountView extends React.Component {
return (
- p5.js Web Editor | Account Settings
+ {this.props.t('AccountView.Title')}
- Account Settings
+ {this.props.t('AccountView.Settings')}
{accessTokensUIEnabled &&
- Account
- {accessTokensUIEnabled && Access Tokens }
+ {this.props.t('AccountView.AccountTab')}
+ {accessTokensUIEnabled && {this.props.t('AccountView.AccessTokensTab')} }
@@ -107,13 +110,14 @@ function asyncValidate(formProps, dispatch, props) {
AccountView.propTypes = {
previousPath: PropTypes.string.isRequired,
- theme: PropTypes.string.isRequired
+ theme: PropTypes.string.isRequired,
+ t: PropTypes.func.isRequired
};
-export default reduxForm({
+export default withTranslation()(reduxForm({
form: 'updateAllSettings',
fields: ['username', 'email', 'currentPassword', 'newPassword'],
validate: validateSettings,
asyncValidate,
asyncBlurFields: ['username', 'email', 'currentPassword']
-}, mapStateToProps, mapDispatchToProps)(AccountView);
+}, mapStateToProps, mapDispatchToProps)(AccountView));
diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx
index 1760cc32..d1b27106 100644
--- a/client/modules/User/pages/NewPasswordView.jsx
+++ b/client/modules/User/pages/NewPasswordView.jsx
@@ -4,6 +4,8 @@ import { reduxForm } from 'redux-form';
import classNames from 'classnames';
import { bindActionCreators } from 'redux';
import { Helmet } from 'react-helmet';
+import { withTranslation } from 'react-i18next';
+import i18next from 'i18next';
import NewPasswordForm from '../components/NewPasswordForm';
import * as UserActions from '../actions';
import Nav from '../../../components/Nav';
@@ -20,13 +22,13 @@ function NewPasswordView(props) {
- p5.js Web Editor | New Password
+ {props.t('NewPasswordView.Title')}
-
Set a New Password
+
{props.t('NewPasswordView.Description')}
- The password reset token is invalid or has expired.
+ {props.t('NewPasswordView.TokenInvalidOrExpired')}
@@ -41,21 +43,22 @@ NewPasswordView.propTypes = {
validateResetPasswordToken: PropTypes.func.isRequired,
user: PropTypes.shape({
resetPasswordInvalid: PropTypes.bool
- }).isRequired
+ }).isRequired,
+ t: PropTypes.func.isRequired
};
function validate(formProps) {
const errors = {};
if (!formProps.password) {
- errors.password = 'Please enter a password';
+ errors.password = i18next.t('NewPasswordView.EmptyPassword');
}
if (!formProps.confirmPassword) {
- errors.confirmPassword = 'Please enter a password confirmation';
+ errors.confirmPassword = i18next.t('NewPasswordView.PasswordConfirmation');
}
if (formProps.password !== formProps.confirmPassword) {
- errors.password = 'Passwords must match';
+ errors.password = i18next.t('NewPasswordView.PasswordMismatch');
}
return errors;
@@ -71,8 +74,8 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(UserActions, dispatch);
}
-export default reduxForm({
+export default withTranslation()(reduxForm({
form: 'new-password',
fields: ['password', 'confirmPassword'],
validate
-}, mapStateToProps, mapDispatchToProps)(NewPasswordView);
+}, mapStateToProps, mapDispatchToProps)(NewPasswordView));
diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.jsx
index 8ff7dac1..46d0b517 100644
--- a/client/modules/User/pages/ResetPasswordView.jsx
+++ b/client/modules/User/pages/ResetPasswordView.jsx
@@ -6,6 +6,7 @@ import classNames from 'classnames';
import { bindActionCreators } from 'redux';
import { reduxForm } from 'redux-form';
import { Helmet } from 'react-helmet';
+import { withTranslation } from 'react-i18next';
import * as UserActions from '../actions';
import ResetPasswordForm from '../components/ResetPasswordForm';
import { validateResetPassword } from '../../../utils/reduxFormUtils';
@@ -23,19 +24,18 @@ function ResetPasswordView(props) {
- p5.js Web Editor | Reset Password
+ {props.t('ResetPasswordView.Title')}
-
Reset Your Password
+
{props.t('ResetPasswordView.Reset')}
- Your password reset email should arrive shortly. If you don't see it, check
- in your spam folder as sometimes it can end up there.
+ {props.t('ResetPasswordView.Submitted')}
- Log In
- or
- Sign Up
+ {props.t('ResetPasswordView.Login')}
+ {props.t('ResetPasswordView.LoginOr')}
+ {props.t('ResetPasswordView.SignUp')}
@@ -48,6 +48,7 @@ ResetPasswordView.propTypes = {
user: PropTypes.shape({
resetPasswordInitiate: PropTypes.bool
}).isRequired,
+ t: PropTypes.func.isRequired
};
function mapStateToProps(state) {
@@ -60,8 +61,8 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(UserActions, dispatch);
}
-export default reduxForm({
+export default withTranslation()(reduxForm({
form: 'reset-password',
fields: ['email'],
validate: validateResetPassword
-}, mapStateToProps, mapDispatchToProps)(ResetPasswordView);
+}, mapStateToProps, mapDispatchToProps)(ResetPasswordView));
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 {
- p5.js Web Editor | Signup
+ {this.props.t('SignupView.Title')}
-
Sign Up
+
{this.props.t('SignupView.Description')}
-
Or
+
{this.props.t('SignupView.Or')}
- Already have an account?
- Log In
+ {this.props.t('SignupView.AlreadyHave')}
+ {this.props.t('SignupView.Login')}
@@ -108,7 +109,8 @@ SignupView.propTypes = {
previousPath: PropTypes.string.isRequired,
user: PropTypes.shape({
authenticated: PropTypes.bool
- })
+ }),
+ t: PropTypes.func.isRequired
};
SignupView.defaultProps = {
@@ -117,11 +119,11 @@ SignupView.defaultProps = {
}
};
-export default reduxForm({
+export default withTranslation()(reduxForm({
form: 'signup',
fields: ['username', 'email', 'password', 'confirmPassword'],
onSubmitFail,
validate: validateSignup,
asyncValidate,
asyncBlurFields: ['username', 'email']
-}, mapStateToProps, mapDispatchToProps)(SignupView);
+}, mapStateToProps, mapDispatchToProps)(SignupView));
diff --git a/client/utils/reduxFormUtils.js b/client/utils/reduxFormUtils.js
index 8ff4b0a9..27d23a9d 100644
--- a/client/utils/reduxFormUtils.js
+++ b/client/utils/reduxFormUtils.js
@@ -1,4 +1,5 @@
/* eslint-disable */
+import i18n from 'i18next';
export const domOnlyProps = ({
initialValue,
autofill,
@@ -20,19 +21,19 @@ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))
function validateNameEmail(formProps, errors) {
if (!formProps.username) {
- errors.username = 'Please enter a username.';
+ errors.username = i18n.t('ReduxFormUtils.errorEmptyUsername');
} else if (!formProps.username.match(/^.{1,20}$/)) {
- errors.username = 'Username must be less than 20 characters.';
+ errors.username = i18n.t('ReduxFormUtils.errorLongUsername');
} else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) {
- errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.';
+ errors.username = i18n.t('ReduxFormUtils.errorValidUsername');
}
if (!formProps.email) {
- errors.email = 'Please enter an email.';
+ errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail');
} else if (
// eslint-disable-next-line max-len
!formProps.email.match(EMAIL_REGEX)) {
- errors.email = 'Please enter a valid email address.';
+ errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail');
}
}
@@ -42,10 +43,10 @@ export function validateSettings(formProps) {
validateNameEmail(formProps, errors);
if (formProps.currentPassword && !formProps.newPassword) {
- errors.newPassword = 'Please enter a new password or leave the current password empty.';
+ errors.newPassword = i18n.t('ReduxFormUtils.errorNewPassword');
}
if (formProps.newPassword && formProps.newPassword.length < 6) {
- errors.newPassword = 'Password must be at least 6 characters';
+ errors.newPassword = i18n.t('ReduxFormUtils.errorShortPassword');
}
return errors;
}
@@ -53,10 +54,10 @@ export function validateSettings(formProps) {
export function validateLogin(formProps) {
const errors = {};
if (!formProps.email) {
- errors.email = 'Please enter an email';
+ errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail');
}
if (!formProps.password) {
- errors.password = 'Please enter a password';
+ errors.password = i18n.t('ReduxFormUtils.errorEmptyPassword');
}
return errors;
}
@@ -67,17 +68,17 @@ export function validateSignup(formProps) {
validateNameEmail(formProps, errors);
if (!formProps.password) {
- errors.password = 'Please enter a password';
+ errors.password = i18n.t('ReduxFormUtils.errorEmptyPassword');
}
if (formProps.password && formProps.password.length < 6) {
- errors.password = 'Password must be at least 6 characters';
+ errors.password = i18n.t('ReduxFormUtils.errorShortPassword');
}
if (!formProps.confirmPassword) {
- errors.confirmPassword = 'Please enter a password confirmation';
+ errors.confirmPassword = i18n.t('ReduxFormUtils.errorConfirmPassword');
}
if (formProps.password !== formProps.confirmPassword && formProps.confirmPassword) {
- errors.confirmPassword = 'Passwords must match';
+ errors.confirmPassword = i18n.t('ReduxFormUtils.errorPasswordMismatch');
}
return errors;
@@ -85,11 +86,11 @@ export function validateSignup(formProps) {
export function validateResetPassword(formProps) {
const errors = {};
if (!formProps.email) {
- errors.email = 'Please enter an email.';
+ errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail');
} else if (
// eslint-disable-next-line max-len
!formProps.email.match(EMAIL_REGEX)) {
- errors.email = 'Please enter a valid email address.';
+ errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail');
}
return errors;
}
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 });
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 652eece4..44be5d87 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -183,21 +183,20 @@
"Error": "Error",
"Save": "Save",
"p5logoARIA": "p5.js Logo"
-
},
"IDEView": {
"SubmitFeedback": "Submit Feedback"
},
"NewFileModal": {
"Title": "Create File",
- "CloseButtonARIA": "Close New File Modal",
+ "CloseButtonARIA": "Close New File Modal",
"EnterName": "Please enter a name",
"InvalidType": "Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert."
},
"NewFileForm": {
- "AddFileSubmit": "Add File",
+ "AddFileSubmit": "Add File",
"Placeholder": "Name"
-},
+ },
"NewFolderModal": {
"Title": "Create Folder",
"CloseButtonARIA": "Close New Folder Modal",
@@ -208,5 +207,105 @@
"NewFolderForm": {
"AddFolderSubmit": "Add Folder",
"Placeholder": "Name"
+ },
+ "ResetPasswordForm": {
+ "Email": "Email used for registration",
+ "EmailARIA": "email",
+ "Submit": "Send Password Reset Email"
+ },
+ "ResetPasswordView": {
+ "Title": "p5.js Web Editor | Reset Password",
+ "Reset": "Reset Your Password",
+ "Submitted": "Your password reset email should arrive shortly. If you don't see it, check\n in your spam folder as sometimes it can end up there.",
+ "Login": "Log In",
+ "LoginOr": "or",
+ "SignUp": "Sign Up"
+ },
+ "ReduxFormUtils": {
+ "errorInvalidEmail": "Please enter a valid email address",
+ "errorEmptyEmail": "Please enter an email",
+ "errorPasswordMismatch": "Passwords must match",
+ "errorEmptyPassword": "Please enter a password",
+ "errorShortPassword": "Password must be at least 6 characters",
+ "errorConfirmPassword": "Please enter a password confirmation",
+ "errorNewPassword": "Please enter a new password or leave the current password empty.",
+ "errorEmptyUsername": "Please enter a username.",
+ "errorLongUsername": "Username must be less than 20 characters.",
+ "errorValidUsername": "Username must only consist of numbers, letters, periods, dashes, and underscores."
+ },
+ "NewPasswordView": {
+ "Title": "p5.js Web Editor | New Password",
+ "Description": "Set a New Password",
+ "TokenInvalidOrExpired": "The password reset token is invalid or has expired.",
+ "EmptyPassword": "Please enter a password",
+ "PasswordConfirmation": "Please enter a password confirmation",
+ "PasswordMismatch": "Passwords must match"
+ },
+ "AccountForm": {
+ "Email": "Email",
+ "EmailARIA": "email",
+ "Unconfirmed": "Unconfirmed.",
+ "EmailSent": "Confirmation sent, check your email.",
+ "Resend": "Resend confirmation email",
+ "UserName": "User Name",
+ "UserNameARIA": "Username",
+ "CurrentPassword": "Current Password",
+ "CurrentPasswordARIA": "Current Password",
+ "NewPassword": "New Password",
+ "NewPasswordARIA": "New Password",
+ "SubmitSaveAllSettings": "Save All Settings"
+ },
+ "AccountView": {
+ "SocialLogin": "Social Login",
+ "SocialLoginDescription": "Use your GitHub or Google account to log into the p5.js Web Editor.",
+ "Title": "p5.js Web Editor | Account Settings",
+ "Settings": "Account Settings",
+ "AccountTab": "Account",
+ "AccessTokensTab": "Access Tokens"
+ },
+ "APIKeyForm": {
+ "ConfirmDelete": "Are you sure you want to delete {{key_label}}?",
+ "Summary": "Personal Access Tokens act like your password to allow automated\n scripts to access the Editor API. Create a token for each script\n that needs access.",
+ "CreateToken": "Create new token",
+ "TokenLabel": "What is this token for?",
+ "TokenPlaceholder": "What is this token for? e.g. Example import script",
+ "CreateTokenSubmit": "Create",
+ "NoTokens": "You have no existing tokens.",
+ "NewTokenTitle": "Your new access token",
+ "NewTokenInfo": "Make sure to copy your new personal access token now.\n You won’t be able to see it again!",
+ "ExistingTokensTitle": "Existing tokens"
+ },
+ "APIKeyList": {
+ "Name": "Name",
+ "Created": "Created on",
+ "LastUsed": "Last used",
+ "Actions": "Actions",
+ "Never": "Never",
+ "DeleteARIA": "Delete API Key"
+ },
+ "NewPasswordForm": {
+ "Title": "Password",
+ "TitleARIA": "Password",
+ "ConfirmPassword": "Confirm Password",
+ "ConfirmPasswordARIA": "Confirm Password",
+ "SubmitSetNewPassword": "Set New Password"
+ },
+ "SignupForm": {
+ "Title": "User Name",
+ "TitleARIA": "username",
+ "Email": "Email",
+ "EmailARIA": "email",
+ "Password": "Password",
+ "PasswordARIA": "password",
+ "ConfirmPassword": "Confirm Password",
+ "ConfirmPasswordARIA": "Confirm password",
+ "SubmitSignup": "Sign Up"
+ },
+ "SignupView": {
+ "Title": "p5.js Web Editor | Signup",
+ "Description": "Sign Up",
+ "Or": "Or",
+ "AlreadyHave": "Already have an account?",
+ "Login": "Log In"
}
}
diff --git a/translations/locales/es-419/translations.json b/translations/locales/es-419/translations.json
index 5f3efaf7..2072032a 100644
--- a/translations/locales/es-419/translations.json
+++ b/translations/locales/es-419/translations.json
@@ -188,8 +188,8 @@
"SubmitFeedback": "Enviar retroalimentación"
},
"NewFileModal": {
- "Title": "Crear Archivo",
- "CloseButtonARIA": "Cerrar diálogo de crear archivo",
+ "Title": "Crear Archivo",
+ "CloseButtonARIA": "Cerrar diálogo de crear archivo",
"EnterName": "Por favor introduce un nombre",
"InvalidType": "Tipo de archivo inválido. Las extensiones válidas son .js, .css, .json, .txt, .csv, .tsv, .frag y .vert."
},
@@ -198,18 +198,114 @@
"Placeholder": "Nombre"
},
"NewFolderModal": {
- "Title": "Crear Directorio",
- "CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio",
- "EnterName": "Por favor introduce un nombre",
- "EmptyName": " El nombre del directorio no debe contener solo espacios vacíos",
- "InvalidExtension": "El nombre del directorio no debe contener una extensión"
-},
+ "Title": "Crear Directorio",
+ "CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio",
+ "EnterName": "Por favor introduce un nombre",
+ "EmptyName": " El nombre del directorio no debe contener solo espacios vacíos",
+ "InvalidExtension": "El nombre del directorio no debe contener una extensión"
+ },
"NewFolderForm": {
"AddFolderSubmit": "Agregar Directorio",
"Placeholder": "Nombre"
+ },
+ "ResetPasswordForm": {
+ "Email": "Correo electrónico usado al registrarse",
+ "EmailARIA": "correo electrónico",
+ "Submit": "Enviar correo para regenerar contraseña"
+ },
+ "ResetPasswordView": {
+ "Title": "Editor Web p5.js | Regenerar Contraseña",
+ "Reset": "Regenerar Contraseña",
+ "Submitted": "Your password reset email should arrive shortly. If you don't see it, check\n in your spam folder as sometimes it can end up there.",
+ "Login": "Ingresa",
+ "LoginOr": "o",
+ "SignUp": "Registráte"
+ },
+ "ReduxFormUtils": {
+ "errorInvalidEmail": "Por favor introduce un correo electrónico válido",
+ "errorEmptyEmail": "Por favor introduce un correo electrónico",
+ "errorPasswordMismatch": "Las contraseñas deben coincidir",
+ "errorEmptyPassword": "Por favor introduce una contraseña",
+ "errorShortPassword": "La contraseña debe tener al menos 6 caracteres",
+ "errorConfirmPassword": "Por favor confirma una contraseña",
+ "errorNewPassword": "Por favor introduce una nueva contraseña o deja la actual contraseña vacía",
+ "errorEmptyUsername": "Por favor introduce tu identificación",
+ "errorLongUsername": "La identificación debe ser menor a 20 caracteres.",
+ "errorValidUsername": "La identificación debe consistir solamente de números, letras, puntos, guiones y guiones bajos."
+ },
+ "NewPasswordView": {
+ "Title": "Editor Web p5.js | Nueva Contraseña",
+ "Description": "Define una nueva contraseña",
+ "TokenInvalidOrExpired": "El token para regenerar la contraseña es inválido o ha expirado.",
+ "EmptyPassword": "Por favor introduce una contraseña",
+ "PasswordConfirmation": "Por favor confirma la contraseña",
+ "PasswordMismatch": "Las contraseña deben coincidir"
+ },
+ "AccountForm": {
+ "Email": "Correo Electrónico",
+ "EmailARIA": "correo electrónico",
+ "Unconfirmed": "Sin confirmar.",
+ "EmailSent": "Confirmación enviada, revisa tu correo electrónico.",
+ "Resend": "Reenviar correo de confirmación",
+ "UserName": "Nombre de Identificación",
+ "UserNameARIA": "Nombre de identificación",
+ "CurrentPassword": "Contraseña Actual",
+ "CurrentPasswordARIA": "Contraseña Actual",
+ "NewPassword": "Nueva Contraseña",
+ "NewPasswordARIA": "Nueva Contraseña",
+ "SubmitSaveAllSettings": "Guardar Todas Las Configuraciones"
+ },
+ "AccountView": {
+ "SocialLogin": "Identificacion usando redes sociales",
+ "SocialLoginDescription": "Usa tu cuenta de GitHub o Google para acceder al Editor Web de p5.js .",
+ "Title": "Editor Web p5.js | Configuración Cuenta",
+ "Settings": "Configuración de la Cuenta",
+ "AccountTab": "Cuenta",
+ "AccessTokensTab": "Tokens de acceso"
+ },
+ "APIKeyForm": {
+ "ConfirmDelete": "¿Estas seguro que quieres borrar {{key_label}}?",
+ "Summary": " Los Tokens de acceso personal actuan como tu contraseña para permitir\n a scripts automáticos acceder al API del Editor. Crea un token por cada script \n que necesite acceso.",
+ "CreateToken": "Crear nuevo token",
+ "TokenLabel": "¿Para que es este token?",
+ "TokenPlaceholder": "¿Para que es este token? p.e. Ejemplo para Importar un Archivo",
+ "CreateTokenSubmit": "Crear",
+ "NoTokens": "No tienes tokens.",
+ "NewTokenTitle": "Tu nuevo token de acceso",
+ "NewTokenInfo": "Asegurate de copiar tu token ahora mismo.\n ¡No podras verlo de nuevo!",
+ "ExistingTokensTitle": "Tokens existentes"
+ },
+ "APIKeyList": {
+ "Name": "Nombre",
+ "Created": "Creado en",
+ "LastUsed": "Usado por última vez",
+ "Actions": "Acciones",
+ "Never": "Nunca",
+ "DeleteARIA": "Borrar clave de API"
+ },
+ "NewPasswordForm": {
+ "Title": "Contraseña",
+ "TitleARIA": "Contraseña",
+ "ConfirmPassword": "Confirmar Contraseña",
+ "ConfirmPasswordARIA": "Confirmar contraseña",
+ "SubmitSetNewPassword": "Crear Nueva Contraseña"
+ },
+ "SignupForm": {
+ "Title": "Identificación",
+ "TitleARIA": "Identificación",
+ "Email": "Correo electrónico",
+ "EmailARIA": "correo electrónico",
+ "Password": "Contraseña",
+ "PasswordARIA": "contraseña",
+ "ConfirmPassword": "Confirma tu contraseña",
+ "ConfirmPasswordARIA": "Confirma tu contraseña",
+ "SubmitSignup": "Registráte"
+ },
+ "SignupView": {
+ "Title": " Editor Web p5.js | Registráte",
+ "Description": "Registráte",
+ "Or": "o",
+ "AlreadyHave": "¿Ya tienes cuenta? ",
+ "Login": "Ingresa"
}
}
-
-
-
-