diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index 48bed0cb..0cd29574 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -36,9 +36,7 @@ const DropdownWrapper = styled.ul` background-color: ${prop('Button.hover.background')}; color: ${prop('Button.hover.foreground')}; - & button, & a { - color: ${prop('Button.hover.foreground')}; - } + * { color: ${prop('Button.hover.foreground')}; } } li { @@ -48,12 +46,21 @@ const DropdownWrapper = styled.ul` align-items: center; & button, + & button span, & a { - color: ${prop('primaryTextColor')}; - width: 100%; - text-align: left; padding: ${remSize(8)} ${remSize(16)}; } + + * { + text-align: left; + justify-content: left; + + color: ${prop('primaryTextColor')}; + width: 100%; + justify-content: flex-start; + } + + & button span { padding: 0px } } `; @@ -63,24 +70,29 @@ const DropdownWrapper = styled.ul` const Dropdown = ({ items, align }) => ( {/* className="nav__items-left" */} - {items && items.map(({ title, icon, href }) => ( + {items && items.map(({ + title, icon, href, action + }) => (
  • - - {/* {MaybeIcon(icon, `Navigate to ${title}`)} */} - {title} - + {/* {MaybeIcon(icon, `Navigate to ${title}`)} */} + {href + ? {title} + : action()}>{title}} +
  • )) }
    ); + Dropdown.propTypes = { align: PropTypes.oneOf(['left', 'right']), items: PropTypes.arrayOf(PropTypes.shape({ action: PropTypes.func, icon: PropTypes.func, - href: PropTypes.string + href: PropTypes.string, + title: PropTypes.string })), }; diff --git a/client/components/mobile/IconButton.jsx b/client/components/mobile/IconButton.jsx index 08f05311..7085f8a1 100644 --- a/client/components/mobile/IconButton.jsx +++ b/client/components/mobile/IconButton.jsx @@ -17,7 +17,7 @@ const IconButton = (props) => { const Icon = icon; return (} + iconBefore={icon && } kind={Button.kinds.inline} focusable="false" {...otherProps} @@ -25,7 +25,11 @@ const IconButton = (props) => { }; IconButton.propTypes = { - icon: PropTypes.func.isRequired + icon: PropTypes.func +}; + +IconButton.defaultProps = { + icon: null }; export default IconButton; diff --git a/client/constants.js b/client/constants.js index a23badd8..0a5188a1 100644 --- a/client/constants.js +++ b/client/constants.js @@ -62,6 +62,7 @@ export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE'; export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE'; export const CLEAR_LINT_MESSAGE = 'CLEAR_LINT_MESSAGE'; +export const TOGGLE_FORCE_DESKTOP = 'TOGGLE_FORCE_DESKTOP'; export const UPDATE_FILE_NAME = 'UPDATE_FILE_NAME'; export const DELETE_FILE = 'DELETE_FILE'; diff --git a/client/jest.setup.js b/client/jest.setup.js index 79652c74..233c76db 100644 --- a/client/jest.setup.js +++ b/client/jest.setup.js @@ -3,3 +3,21 @@ import '@babel/polyfill'; // See: https://github.com/testing-library/jest-dom // eslint-disable-next-line import/no-extraneous-dependencies import '@testing-library/jest-dom'; + +import lodash from 'lodash'; + +// For testing, we use en-US and provide a mock implementation +// of t() that finds the correct translation +import translations from '../translations/locales/en-US/translations.json'; + +// This function name needs to be prefixed with "mock" so that Jest doesn't +// complain that it's out-of-scope in the mock below +const mockTranslate = key => lodash.get(translations, key); + +jest.mock('react-i18next', () => ({ + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => (Component) => { + Component.defaultProps = { ...Component.defaultProps, t: mockTranslate }; + return Component; + }, +})); diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 4e54b615..3ebeca45 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { browserHistory } from 'react-router'; +import { withTranslation } from 'react-i18next'; import ExitIcon from '../../../images/exit.svg'; @@ -80,7 +81,7 @@ class Overlay extends React.Component {

    {title}

    {actions} -
    @@ -101,6 +102,7 @@ Overlay.propTypes = { ariaLabel: PropTypes.string, previousPath: PropTypes.string, isFixedHeight: PropTypes.bool, + t: PropTypes.func.isRequired }; Overlay.defaultProps = { @@ -113,4 +115,4 @@ Overlay.defaultProps = { isFixedHeight: false, }; -export default Overlay; +export default withTranslation()(Overlay); diff --git a/client/modules/IDE/actions/editorAccessibility.js b/client/modules/IDE/actions/editorAccessibility.js index ffb88db4..1014e6ad 100644 --- a/client/modules/IDE/actions/editorAccessibility.js +++ b/client/modules/IDE/actions/editorAccessibility.js @@ -14,3 +14,9 @@ export function clearLintMessage() { type: ActionTypes.CLEAR_LINT_MESSAGE }; } + +export function toggleForceDesktop() { + return { + type: ActionTypes.TOGGLE_FORCE_DESKTOP + }; +} diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index e9417d3b..f4369124 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { withTranslation } from 'react-i18next'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; @@ -14,7 +15,7 @@ import Loader from '../../App/components/loader'; import QuickAddList from './QuickAddList'; const projectInCollection = (project, collection) => - collection.items.find(item => item.project.id === project.id) != null; + collection.items.find(item => item.projectId === project.id) != null; class CollectionList extends React.Component { constructor(props) { @@ -42,9 +43,9 @@ class CollectionList extends React.Component { getTitle() { if (this.props.username === this.props.user.username) { - return 'p5.js Web Editor | My collections'; + return this.props.t('AddToCollectionList.Title'); } - return `p5.js Web Editor | ${this.props.username}'s collections`; + return this.props.t('AddToCollectionList.AnothersTitle', { anotheruser: this.props.username }); } handleCollectionAdd = (collection) => { @@ -74,19 +75,22 @@ class CollectionList extends React.Component { items={collectionWithSketchStatus} onAdd={this.handleCollectionAdd} onRemove={this.handleCollectionRemove} + t={this.props.t} /> ); } else { - content = 'No collections'; + content = this.props.t('AddToCollectionList.Empty'); } return ( -
    - - {this.getTitle()} - +
    +
    + + {this.getTitle()} + - {content} + {content} +
    ); } @@ -133,7 +137,8 @@ CollectionList.propTypes = { owner: PropTypes.shape({ id: PropTypes.string }) - }) + }), + t: PropTypes.func.isRequired }; CollectionList.defaultProps = { @@ -162,4 +167,4 @@ function mapDispatchToProps(dispatch) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionList)); diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index d4173b4b..339ead18 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { withTranslation } from 'react-i18next'; // import find from 'lodash/find'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; @@ -32,9 +33,9 @@ class SketchList extends React.Component { getSketchesTitle() { if (this.props.username === this.props.user.username) { - return 'p5.js Web Editor | My sketches'; + return this.props.t('AddToCollectionSketchList.Title'); } - return `p5.js Web Editor | ${this.props.username}'s sketches`; + return this.props.t('AddToCollectionSketchList.AnothersTitle', { anotheruser: this.props.username }); } handleCollectionAdd = (sketch) => { @@ -68,15 +69,17 @@ class SketchList extends React.Component { /> ); } else { - content = 'No collections'; + content = this.props.t('AddToCollectionSketchList.NoCollections'); } return ( -
    - - {this.getSketchesTitle()} - - {content} +
    +
    + + {this.getSketchesTitle()} + + {content} +
    ); } @@ -111,6 +114,7 @@ SketchList.propTypes = { }).isRequired, addToCollection: PropTypes.func.isRequired, removeFromCollection: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; SketchList.defaultProps = { @@ -134,4 +138,4 @@ function mapDispatchToProps(dispatch) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(SketchList); +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(SketchList)); diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 1007ca97..2f556d36 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Helmet } from 'react-helmet'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; @@ -50,9 +51,9 @@ class CollectionList extends React.Component { getTitle() { if (this.props.username === this.props.user.username) { - return 'p5.js Web Editor | My collections'; + return this.props.t('CollectionList.Title'); } - return `p5.js Web Editor | ${this.props.username}'s collections`; + return this.props.t('CollectionList.AnothersTitle', { anotheruser: this.props.username }); } showAddSketches = (collectionId) => { @@ -78,7 +79,7 @@ class CollectionList extends React.Component { _renderEmptyTable() { if (!this.props.loading && this.props.collections.length === 0) { - return (

    No collections.

    ); + return (

    {this.props.t('CollectionList.NoCollections')}

    ); } return null; } @@ -88,14 +89,14 @@ class CollectionList extends React.Component { let buttonLabel; if (field !== fieldName) { if (field === 'name') { - buttonLabel = `Sort by ${displayName} ascending.`; + buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { displayName }); } else { - buttonLabel = `Sort by ${displayName} descending.`; + buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { displayName }); } } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = `Sort by ${displayName} descending.`; + buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { displayName }); } else { - buttonLabel = `Sort by ${displayName} ascending.`; + buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { displayName }); } return buttonLabel; } @@ -116,10 +117,10 @@ class CollectionList extends React.Component { > {displayName} {field === fieldName && direction === SortingActions.DIRECTION.ASC && - + } {field === fieldName && direction === SortingActions.DIRECTION.DESC && - + } @@ -142,10 +143,10 @@ class CollectionList extends React.Component { - {this._renderFieldHeader('name', 'Name')} - {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)} - {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)} - {this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')} + {this._renderFieldHeader('name', this.props.t('CollectionList.HeaderName'))} + {this._renderFieldHeader('createdAt', this.props.t('CollectionList.HeaderCreatedAt', { context: mobile ? 'mobile' : '' }))} + {this._renderFieldHeader('updatedAt', this.props.t('CollectionList.HeaderUpdatedAt', { context: mobile ? 'mobile' : '' }))} + {this._renderFieldHeader('numItems', this.props.t('CollectionList.HeaderNumItems', { context: mobile ? 'mobile' : '' }))} @@ -165,17 +166,15 @@ class CollectionList extends React.Component { { this.state.addingSketchesToCollectionId && ( } closeOverlay={this.hideAddSketches} isFixedHeight > -
    - -
    +
    ) } @@ -213,6 +212,7 @@ CollectionList.propTypes = { id: PropTypes.string }) }), + t: PropTypes.func.isRequired, mobile: PropTypes.bool, }; @@ -244,4 +244,4 @@ function mapDispatchToProps(dispatch) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionList)); diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index de20cea9..4775639c 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -4,6 +4,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; +import { withTranslation } from 'react-i18next'; import * as ProjectActions from '../../actions/project'; import * as CollectionsActions from '../../actions/collections'; import * as IdeActions from '../../actions/ide'; @@ -81,7 +82,7 @@ class CollectionListRowBase extends React.Component { handleCollectionDelete = () => { this.closeAll(); - if (window.confirm(`Are you sure you want to delete "${this.props.collection.name}"?`)) { + if (window.confirm(this.props.t('Common.DeleteConfirmation', { name: this.props.collection.name }))) { this.props.deleteCollection(this.props.collection.id); } } @@ -130,7 +131,7 @@ class CollectionListRowBase extends React.Component { onClick={this.toggleOptions} onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} - aria-label="Toggle Open/Close collection options" + aria-label={this.props.t('CollectionListRow.ToggleCollectionOptionsARIA')} > @@ -145,7 +146,7 @@ class CollectionListRowBase extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Add sketch + {this.props.t('CollectionListRow.AddSketch')} {userIsOwner && @@ -156,7 +157,7 @@ class CollectionListRowBase extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Delete + {this.props.t('CollectionListRow.Delete')} } {userIsOwner && @@ -167,7 +168,7 @@ class CollectionListRowBase extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Rename + {this.props.t('CollectionListRow.Rename')} } @@ -248,6 +249,7 @@ CollectionListRowBase.propTypes = { editCollection: PropTypes.func.isRequired, onAddSketches: PropTypes.func.isRequired, mobile: PropTypes.bool, + t: PropTypes.func.isRequired }; CollectionListRowBase.defaultProps = { @@ -258,4 +260,4 @@ function mapDispatchToPropsSketchListRow(dispatch) { return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions, ToastActions), dispatch); } -export default connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase); +export default withTranslation()(connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase)); diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index ddb6d55e..eaee9492 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -1,4 +1,6 @@ import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; import { bindActionCreators } from 'redux'; @@ -72,7 +74,7 @@ const getConsoleFeedStyle = (theme, times, fontSize) => { } }; -const Console = () => { +const Console = ({ t }) => { const consoleEvents = useSelector(state => state.console); const isExpanded = useSelector(state => state.ide.consoleIsExpanded); const { theme, fontSize } = useSelector(state => state.preferences); @@ -98,19 +100,19 @@ const Console = () => { return (
    -

    Console

    +

    {t('Console.Title')}

    - -
    @@ -140,5 +142,9 @@ const Console = () => { ); }; +Console.propTypes = { + t: PropTypes.func.isRequired, +}; -export default Console; + +export default withTranslation()(Console); diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index b1ff6b10..099065a5 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -1,15 +1,16 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Link } from 'react-router'; +import { withTranslation } from 'react-i18next'; class ErrorModal extends React.Component { forceAuthentication() { return (

    - In order to save sketches, you must be logged in. Please  - Login -  or  - Sign Up. + {this.props.t('ErrorModal.MessageLogin')} + {this.props.t('ErrorModal.Login')} + {this.props.t('ErrorModal.LoginOr')} + {this.props.t('ErrorModal.SignUp')}.

    ); } @@ -17,8 +18,8 @@ class ErrorModal extends React.Component { staleSession() { return (

    - It looks like you've been logged out. Please  - log in. + {this.props.t('ErrorModal.MessageLoggedOut')} + {this.props.t('ErrorModal.LogIn')}.

    ); } @@ -26,8 +27,7 @@ class ErrorModal extends React.Component { staleProject() { return (

    - The project you have attempted to save has been saved from another window. - Please refresh the page to see the latest version. + {this.props.t('ErrorModal.SavedDifferentWindow')}

    ); } @@ -51,7 +51,8 @@ class ErrorModal extends React.Component { ErrorModal.propTypes = { type: PropTypes.string.isRequired, - closeModal: PropTypes.func.isRequired + closeModal: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; -export default ErrorModal; +export default withTranslation()(ErrorModal); diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index d0a10ef8..0533091a 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -3,6 +3,8 @@ import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import classNames from 'classnames'; +import { withTranslation } from 'react-i18next'; + import * as IDEActions from '../actions/ide'; import * as FileActions from '../actions/files'; import DownArrowIcon from '../../../images/down-filled-triangle.svg'; @@ -152,7 +154,9 @@ export class FileNode extends React.Component { } handleClickDelete = () => { - if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) { + const prompt = this.props.t('Common.DeleteConfirmation', { name: this.props.name }); + + if (window.confirm(prompt)) { this.setState({ isDeleting: true }); this.props.resetSelectedFile(this.props.id); setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100); @@ -237,6 +241,8 @@ export class FileNode extends React.Component { const isFolder = this.props.fileType === 'folder'; const isRoot = this.props.name === 'root'; + const { t } = this.props; + return (
    { !isRoot && @@ -252,14 +258,14 @@ export class FileNode extends React.Component { @@ -286,7 +292,7 @@ export class FileNode extends React.Component { />
  • { this.props.authenticated &&
  • } @@ -342,7 +348,7 @@ export class FileNode extends React.Component { onFocus={this.onFocusComponent} className="sidebar__file-item-option" > - Rename + {t('FileNode.Rename')}
  • @@ -352,7 +358,7 @@ export class FileNode extends React.Component { onFocus={this.onFocusComponent} className="sidebar__file-item-option" > - Delete + {t('FileNode.Delete')}
  • @@ -388,6 +394,7 @@ FileNode.propTypes = { canEdit: PropTypes.bool.isRequired, openUploadFileModal: PropTypes.func.isRequired, authenticated: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, onClickFile: PropTypes.func }; @@ -408,5 +415,8 @@ function mapDispatchToProps(dispatch) { return bindActionCreators(Object.assign(FileActions, IDEActions), dispatch); } -const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(FileNode); +const TranslatedFileNode = withTranslation()(FileNode); + +const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(TranslatedFileNode); + export default ConnectedFileNode; diff --git a/client/modules/IDE/components/FileUploader.jsx b/client/modules/IDE/components/FileUploader.jsx index e2e6e509..f33b59ed 100644 --- a/client/modules/IDE/components/FileUploader.jsx +++ b/client/modules/IDE/components/FileUploader.jsx @@ -3,6 +3,7 @@ import React from 'react'; import Dropzone from 'dropzone'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; import * as UploaderActions from '../actions/uploader'; import getConfig from '../../../utils/getConfig'; import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils'; @@ -30,7 +31,7 @@ class FileUploader extends React.Component { thumbnailWidth: 200, thumbnailHeight: 200, acceptedFiles: fileExtensionsAndMimeTypes, - dictDefaultMessage: 'Drop files here or click to use the file browser', + dictDefaultMessage: this.props.t('FileUploader.DictDefaultMessage'), accept: this.props.dropzoneAcceptCallback.bind(this, userId), sending: this.props.dropzoneSendingCallback, complete: this.props.dropzoneCompleteCallback @@ -59,7 +60,8 @@ FileUploader.propTypes = { }), user: PropTypes.shape({ id: PropTypes.string - }) + }), + t: PropTypes.func.isRequired }; FileUploader.defaultProps = { @@ -84,4 +86,4 @@ function mapDispatchToProps(dispatch) { return bindActionCreators(UploaderActions, dispatch); } -export default connect(mapStateToProps, mapDispatchToProps)(FileUploader); +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(FileUploader)); diff --git a/client/modules/IDE/components/QuickAddList/QuickAddList.jsx b/client/modules/IDE/components/QuickAddList/QuickAddList.jsx index be7a5ac3..32264230 100644 --- a/client/modules/IDE/components/QuickAddList/QuickAddList.jsx +++ b/client/modules/IDE/components/QuickAddList/QuickAddList.jsx @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router'; +import { withTranslation } from 'react-i18next'; import Icons from './Icons'; const Item = ({ - isAdded, onSelect, name, url + isAdded, onSelect, name, url, t }) => { const buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection'; return ( @@ -20,7 +21,7 @@ const Item = ({ target="_blank" onClick={e => e.stopPropogation()} > - View + {t('QuickAddList.View')} ); @@ -37,10 +38,11 @@ Item.propTypes = { url: PropTypes.string.isRequired, isAdded: PropTypes.bool.isRequired, onSelect: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; const QuickAddList = ({ - items, onAdd, onRemove + items, onAdd, onRemove, t }) => { const handleAction = (item) => { if (item.isAdded) { @@ -53,6 +55,7 @@ const QuickAddList = ({ return (
      {items.map(item => ( { @@ -70,6 +73,7 @@ QuickAddList.propTypes = { items: PropTypes.arrayOf(ItemType).isRequired, onAdd: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; -export default QuickAddList; +export default withTranslation()(QuickAddList); diff --git a/client/modules/IDE/components/ShareModal.jsx b/client/modules/IDE/components/ShareModal.jsx index e0962624..bea4c42a 100644 --- a/client/modules/IDE/components/ShareModal.jsx +++ b/client/modules/IDE/components/ShareModal.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { withTranslation } from 'react-i18next'; import CopyableInput from './CopyableInput'; class ShareModal extends React.PureComponent { @@ -16,21 +17,21 @@ class ShareModal extends React.PureComponent { {projectName} `} /> @@ -42,7 +43,8 @@ class ShareModal extends React.PureComponent { ShareModal.propTypes = { projectId: PropTypes.string.isRequired, ownerUsername: PropTypes.string.isRequired, - projectName: PropTypes.string.isRequired + projectName: PropTypes.string.isRequired, + t: PropTypes.func.isRequired }; -export default ShareModal; +export default withTranslation()(ShareModal); diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 97c8c0ec..5d480278 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -1,6 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; +import { withTranslation } from 'react-i18next'; + import ConnectedFileNode from './FileNode'; import DownArrowIcon from '../../../images/down-filled-triangle.svg'; @@ -71,11 +73,11 @@ class Sidebar extends React.Component {

      - Sketch Files + {this.props.t('Sidebar.Title')}

    • { this.props.user.authenticated &&
    • } @@ -159,11 +161,12 @@ Sidebar.propTypes = { user: PropTypes.shape({ id: PropTypes.string, authenticated: PropTypes.bool.isRequired - }).isRequired + }).isRequired, + t: PropTypes.func.isRequired, }; Sidebar.defaultProps = { owner: undefined }; -export default Sidebar; +export default withTranslation()(Sidebar); diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index d5f9d71d..17aa1d41 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -2,6 +2,7 @@ import format from 'date-fns/format'; import PropTypes from 'prop-types'; import React from 'react'; import { Helmet } from 'react-helmet'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; @@ -148,14 +149,14 @@ class SketchListRowBase extends React.Component { handleSketchDelete = () => { this.closeAll(); - if (window.confirm(`Are you sure you want to delete "${this.props.sketch.name}"?`)) { + if (window.confirm(this.props.t('Common.DeleteConfirmation', { name: this.props.sketch.name }))) { this.props.deleteProject(this.props.sketch.id); } } renderViewButton = sketchURL => (
    ) @@ -170,7 +171,7 @@ class SketchListRowBase extends React.Component { onClick={this.toggleOptions} onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} - aria-label="Toggle Open/Close Sketch Options" + aria-label={this.props.t('SketchList.ToggleLabelARIA')} >
    - View + {this.props.t('SketchList.View')}
    - {this._renderFieldHeader('name', 'Sketch')} - {this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)} - {this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)} + {this._renderFieldHeader('name', this.props.t('SketchList.HeaderName'))} + {this._renderFieldHeader('createdAt', this.props.t('SketchList.HeaderCreatedAt', { context: mobile ? 'mobile' : '' }))} + {this._renderFieldHeader('updatedAt', this.props.t('SketchList.HeaderUpdatedAt', { context: mobile ? 'mobile' : '' }))} @@ -453,6 +453,7 @@ class SketchList extends React.Component { onAddToCollection={() => { this.setState({ sketchToAddToCollection: sketch }); }} + t={this.props.t} />))}
    } @@ -460,7 +461,7 @@ class SketchList extends React.Component { this.state.sketchToAddToCollection && this.setState({ sketchToAddToCollection: null })} > { this.modal = element; }}>
    -

    Upload File

    +

    {this.props.t('UploadFileModal.Title')}

    { this.props.reachedTotalSizeLimit &&

    - { - `Error: You cannot upload any more files. You have reached the total size limit of ${limitText}. - If you would like to upload more, please remove the ones you aren't using anymore by - in your ` - } + {this.props.t('UploadFileModal.SizeLimitError', { sizeLimit: limitText })} assets .

    @@ -68,4 +66,4 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(UploadFileModal); +export default withTranslation()(connect(mapStateToProps)(UploadFileModal)); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 1dcae539..c5b358f1 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -418,15 +418,15 @@ class IDEView extends React.Component { )} {this.props.location.pathname.match(/add-to-collection$/) && ( } isFixedHeight @@ -440,8 +440,8 @@ class IDEView extends React.Component { )} {this.props.ide.shareModalVisible && ( +const getNavOptions = (username = undefined, logoutUser = () => {}, toggleForceDesktop = () => {}) => (username ? [ - { icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', }, - { icon: PreferencesIcon, title: 'My Stuff', href: `/mobile/${username}/sketches` }, - { icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' }, - { icon: PreferencesIcon, title: 'Original Editor', href: '/', }, + { icon: PreferencesIcon, title: 'Preferences', href: '/preferences', }, + { icon: PreferencesIcon, title: 'My Stuff', href: `/${username}/sketches` }, + { icon: PreferencesIcon, title: 'Examples', href: '/p5/sketches' }, + { icon: PreferencesIcon, title: 'Original Editor', action: toggleForceDesktop, }, + { icon: PreferencesIcon, title: 'Logout', action: logoutUser, }, ] : [ - { icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', }, - { icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' }, - { icon: PreferencesIcon, title: 'Original Editor', href: '/', }, + { icon: PreferencesIcon, title: 'Preferences', href: '/preferences', }, + { icon: PreferencesIcon, title: 'Examples', href: '/p5/sketches' }, + { icon: PreferencesIcon, title: 'Original Editor', action: toggleForceDesktop, }, + { icon: PreferencesIcon, title: 'Login', href: '/login', }, ] ); @@ -144,7 +146,7 @@ const handleGlobalKeydown = (props, cmController) => (e) => { const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) => { const { - autosaveProject, preferences, ide, selectedFile: file, project, user + autosaveProject, preferences, ide, selectedFile: file, project } = props; const { selectedFile: oldFile } = prevProps; @@ -170,10 +172,23 @@ const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) = } }; +// ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, +// stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files + const MobileIDEView = (props) => { + // const { + // preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage, + // selectedFile, updateFileContent, files, user, params, + // closeEditorOptions, showEditorOptions, logoutUser, + // startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console, + // showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges, + // toggleForceDesktop + // } = props; + const { ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, - stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files + stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files, + toggleForceDesktop, logoutUser } = props; @@ -204,7 +219,7 @@ const MobileIDEView = (props) => { // Screen Modals const [toggleNavDropdown, NavDropDown] = useAsModal(); @@ -239,6 +254,7 @@ const MobileIDEView = (props) => { subtitle={filename} > + {
  • - { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" /> + { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
  • @@ -307,12 +323,22 @@ MobileIDEView.propTypes = { name: PropTypes.string.isRequired, }).isRequired, + files: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + })).isRequired, + + toggleForceDesktop: PropTypes.func.isRequired, + user: PropTypes.shape({ authenticated: PropTypes.bool.isRequired, id: PropTypes.string, username: PropTypes.string, }).isRequired, + logoutUser: PropTypes.func.isRequired, + getProject: PropTypes.func.isRequired, clearPersistedState: PropTypes.func.isRequired, params: PropTypes.shape({ @@ -320,10 +346,9 @@ MobileIDEView.propTypes = { username: PropTypes.string }).isRequired, - unsavedChanges: PropTypes.bool.isRequired, - startSketch: PropTypes.func.isRequired, - stopSketch: PropTypes.func.isRequired, + + unsavedChanges: PropTypes.bool.isRequired, autosaveProject: PropTypes.func.isRequired, @@ -351,7 +376,8 @@ const mapDispatchToProps = dispatch => bindActionCreators({ ...ProjectActions, ...IDEActions, ...ConsoleActions, - ...PreferencesActions + ...PreferencesActions, + ...EditorAccessibilityActions }, dispatch); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView)); diff --git a/client/modules/IDE/reducers/editorAccessibility.js b/client/modules/IDE/reducers/editorAccessibility.js index d86dbede..73d6cd75 100644 --- a/client/modules/IDE/reducers/editorAccessibility.js +++ b/client/modules/IDE/reducers/editorAccessibility.js @@ -1,7 +1,8 @@ import * as ActionTypes from '../../../constants'; const initialState = { - lintMessages: [] + lintMessages: [], + forceDesktop: false }; let messageId = 0; @@ -16,6 +17,8 @@ const editorAccessibility = (state = initialState, action) => { }); case ActionTypes.CLEAR_LINT_MESSAGE: return Object.assign({}, state, { lintMessages: [] }); + case ActionTypes.TOGGLE_FORCE_DESKTOP: + return Object.assign({}, state, { forceDesktop: !(state.forceDesktop) }); default: return state; } diff --git a/client/modules/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx index 270faee7..0fd7cfe4 100644 --- a/client/modules/Mobile/MobileDashboardView.jsx +++ b/client/modules/Mobile/MobileDashboardView.jsx @@ -138,8 +138,8 @@ const Panels = { const navOptions = username => [ - { title: 'Create Sketch', href: '/mobile' }, - { title: 'Create Collection', href: `/mobile/${username}/collections/create` } + { title: 'Create Sketch', href: '/' }, + { title: 'Create Collection', href: `/${username}/collections/create` } ]; @@ -185,7 +185,7 @@ const MobileDashboard = ({ params, location }) => { - + diff --git a/client/modules/Mobile/MobilePreferences.jsx b/client/modules/Mobile/MobilePreferences.jsx index c7991c2f..69ad737b 100644 --- a/client/modules/Mobile/MobilePreferences.jsx +++ b/client/modules/Mobile/MobilePreferences.jsx @@ -69,7 +69,7 @@ const MobilePreferences = () => {
    - +
    diff --git a/client/modules/Mobile/MobileSketchView.jsx b/client/modules/Mobile/MobileSketchView.jsx index 4d31c172..a4238fec 100644 --- a/client/modules/Mobile/MobileSketchView.jsx +++ b/client/modules/Mobile/MobileSketchView.jsx @@ -39,7 +39,7 @@ const MobileSketchView = () => { return (
    } + leftButton={} title={projectName} /> diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index d0648be3..a5b22edb 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -229,7 +229,7 @@ export function updateSettings(formValues) { dispatch(updateSettingsSuccess(response.data)); browserHistory.push('/'); dispatch(showToast(5500)); - dispatch(setToastText('Settings saved.')); + dispatch(setToastText('Toast.SettingsSaved')); }) .catch((error) => { const { response } = error; diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 54e87518..7e9a8e38 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; +import { withTranslation } from 'react-i18next'; import classNames from 'classnames'; import Button from '../../../common/Button'; @@ -27,7 +28,7 @@ import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import RemoveIcon from '../../../images/close.svg'; -const ShareURL = ({ value }) => { +const ShareURL = ({ value, t }) => { const [showURL, setShowURL] = useState(false); const node = useRef(); @@ -56,11 +57,11 @@ const ShareURL = ({ value }) => { onClick={() => setShowURL(!showURL)} iconAfter={} > - Share + {t('Collection.Share')} { showURL &&
    - +
    }
    @@ -69,22 +70,23 @@ const ShareURL = ({ value }) => { ShareURL.propTypes = { value: PropTypes.string.isRequired, + t: PropTypes.func.isRequired }; const CollectionItemRowBase = ({ - collection, item, isOwner, removeFromCollection + collection, item, isOwner, removeFromCollection, t }) => { const projectIsDeleted = item.isDeleted; const handleSketchRemove = () => { const name = projectIsDeleted ? 'deleted sketch' : item.project.name; - if (window.confirm(`Are you sure you want to remove "${name}" from this collection?`)) { + if (window.confirm(t('Collection.DeleteFromCollection', { name_sketch: name }))) { removeFromCollection(collection.id, item.projectId); } }; - const name = projectIsDeleted ? Sketch was deleted : ( + const name = projectIsDeleted ? {t('Collection.SketchDeleted')} : ( {item.project.name} @@ -106,7 +108,7 @@ const CollectionItemRowBase = ({ @@ -138,7 +140,8 @@ CollectionItemRowBase.propTypes = { username: PropTypes.string, authenticated: PropTypes.bool.isRequired }).isRequired, - removeFromCollection: PropTypes.func.isRequired + removeFromCollection: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; function mapDispatchToPropsSketchListRow(dispatch) { @@ -163,9 +166,9 @@ class Collection extends React.Component { getTitle() { if (this.props.username === this.props.user.username) { - return 'p5.js Web Editor | My collections'; + return this.props.t('Collection.Title'); } - return `p5.js Web Editor | ${this.props.username}'s collections`; + return this.props.t('Collection.AnothersTitle', { anotheruser: this.props.username }); } getUsername() { @@ -257,27 +260,27 @@ class Collection extends React.Component { InputComponent="textarea" value={description} onChange={handleEditCollectionDescription} - emptyPlaceholder="Add description" + emptyPlaceholder={this.props.t('Collection.DescriptionPlaceholder')} /> : description }

    -

    Collection by{' '} +

    {this.props.t('Collection.By')} {owner.username}

    -

    {items.length} sketch{items.length === 1 ? '' : 'es'}

    +

    {this.props.t('Collection.NumSketches', { count: items.length }) }

    - +

    { this.isOwner() && }
    @@ -304,7 +307,7 @@ class Collection extends React.Component { this.props.collection.items.length > 0; if (!isLoading && !hasCollectionItems) { - return (

    No sketches in collection

    ); + return (

    {this.props.t('Collection.NoSketches')}

    ); } return null; } @@ -314,14 +317,14 @@ class Collection extends React.Component { let buttonLabel; if (field !== fieldName) { if (field === 'name') { - buttonLabel = `Sort by ${displayName} ascending.`; + buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { displayName }); } else { - buttonLabel = `Sort by ${displayName} descending.`; + buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { displayName }); } } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = `Sort by ${displayName} descending.`; + buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { displayName }); } else { - buttonLabel = `Sort by ${displayName} ascending.`; + buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { displayName }); } return buttonLabel; } @@ -342,10 +345,10 @@ class Collection extends React.Component { > {displayName} {field === fieldName && direction === SortingActions.DIRECTION.ASC && - + } {field === fieldName && direction === SortingActions.DIRECTION.DESC && - + } @@ -371,9 +374,9 @@ class Collection extends React.Component { - {this._renderFieldHeader('name', 'Name')} - {this._renderFieldHeader('createdAt', 'Date Added')} - {this._renderFieldHeader('user', 'Owner')} + {this._renderFieldHeader('name', this.props.t('Collection.HeaderName'))} + {this._renderFieldHeader('createdAt', this.props.t('Collection.HeaderCreatedAt'))} + {this._renderFieldHeader('user', this.props.t('Collection.HeaderUser'))} @@ -386,6 +389,7 @@ class Collection extends React.Component { username={this.getUsername()} collection={this.props.collection} isOwner={isOwner} + t={this.props.t} />))}
    @@ -393,14 +397,15 @@ class Collection extends React.Component { { this.state.isAddingSketches && ( } closeOverlay={this.hideAddSketches} isFixedHeight > -
    - -
    +
    ) } @@ -436,7 +441,8 @@ Collection.propTypes = { sorting: PropTypes.shape({ field: PropTypes.string.isRequired, direction: PropTypes.string.isRequired - }).isRequired + }).isRequired, + t: PropTypes.func.isRequired }; Collection.defaultProps = { @@ -467,4 +473,4 @@ function mapDispatchToProps(dispatch) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(Collection); +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Collection)); diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx index cf679ccc..9a7b6315 100644 --- a/client/modules/User/components/CollectionCreate.jsx +++ b/client/modules/User/components/CollectionCreate.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Helmet } from 'react-helmet'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as CollectionsActions from '../../IDE/actions/collections'; @@ -24,7 +25,7 @@ class CollectionCreate extends React.Component { } getTitle() { - return 'p5.js Web Editor | Create collection'; + return this.props.t('CollectionCreate.Title'); } handleTextChange = field => (evt) => { @@ -55,34 +56,34 @@ class CollectionCreate extends React.Component {
    - {creationError && Couldn't create collection} + {creationError && {this.props.t('CollectionCreate.FormError')}}

    - + - {invalid && Collection name is required} + {invalid && {this.props.t('CollectionCreate.NameRequired')}}

    - +