🔀 merge from develop

This commit is contained in:
ghalestrilo 2020-08-24 10:03:56 -03:00
commit 384a185028
44 changed files with 842 additions and 292 deletions

View file

@ -36,9 +36,7 @@ const DropdownWrapper = styled.ul`
background-color: ${prop('Button.hover.background')}; background-color: ${prop('Button.hover.background')};
color: ${prop('Button.hover.foreground')}; color: ${prop('Button.hover.foreground')};
& button, & a { * { color: ${prop('Button.hover.foreground')}; }
color: ${prop('Button.hover.foreground')};
}
} }
li { li {
@ -48,12 +46,21 @@ const DropdownWrapper = styled.ul`
align-items: center; align-items: center;
& button, & button,
& button span,
& a { & a {
color: ${prop('primaryTextColor')};
width: 100%;
text-align: left;
padding: ${remSize(8)} ${remSize(16)}; 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 }) => ( const Dropdown = ({ items, align }) => (
<DropdownWrapper align={align} > <DropdownWrapper align={align} >
{/* className="nav__items-left" */} {/* className="nav__items-left" */}
{items && items.map(({ title, icon, href }) => ( {items && items.map(({
title, icon, href, action
}) => (
<li key={`nav-${title && title.toLowerCase()}`}> <li key={`nav-${title && title.toLowerCase()}`}>
<Link to={href}>
{/* {MaybeIcon(icon, `Navigate to ${title}`)} */} {/* {MaybeIcon(icon, `Navigate to ${title}`)} */}
{title} {href
</Link> ? <IconButton to={href}>{title}</IconButton>
: <IconButton onClick={() => action()}>{title}</IconButton>}
</li> </li>
)) ))
} }
</DropdownWrapper> </DropdownWrapper>
); );
Dropdown.propTypes = { Dropdown.propTypes = {
align: PropTypes.oneOf(['left', 'right']), align: PropTypes.oneOf(['left', 'right']),
items: PropTypes.arrayOf(PropTypes.shape({ items: PropTypes.arrayOf(PropTypes.shape({
action: PropTypes.func, action: PropTypes.func,
icon: PropTypes.func, icon: PropTypes.func,
href: PropTypes.string href: PropTypes.string,
title: PropTypes.string
})), })),
}; };

View file

@ -17,7 +17,7 @@ const IconButton = (props) => {
const Icon = icon; const Icon = icon;
return (<ButtonWrapper return (<ButtonWrapper
iconBefore={<Icon />} iconBefore={icon && <Icon />}
kind={Button.kinds.inline} kind={Button.kinds.inline}
focusable="false" focusable="false"
{...otherProps} {...otherProps}
@ -25,7 +25,11 @@ const IconButton = (props) => {
}; };
IconButton.propTypes = { IconButton.propTypes = {
icon: PropTypes.func.isRequired icon: PropTypes.func
};
IconButton.defaultProps = {
icon: null
}; };
export default IconButton; export default IconButton;

View file

@ -62,6 +62,7 @@ export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE';
export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE'; export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE';
export const CLEAR_LINT_MESSAGE = 'CLEAR_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 UPDATE_FILE_NAME = 'UPDATE_FILE_NAME';
export const DELETE_FILE = 'DELETE_FILE'; export const DELETE_FILE = 'DELETE_FILE';

View file

@ -3,3 +3,21 @@ import '@babel/polyfill';
// See: https://github.com/testing-library/jest-dom // See: https://github.com/testing-library/jest-dom
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom'; 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;
},
}));

View file

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import { withTranslation } from 'react-i18next';
import ExitIcon from '../../../images/exit.svg'; import ExitIcon from '../../../images/exit.svg';
@ -80,7 +81,7 @@ class Overlay extends React.Component {
<h2 className="overlay__title">{title}</h2> <h2 className="overlay__title">{title}</h2>
<div className="overlay__actions"> <div className="overlay__actions">
{actions} {actions}
<button className="overlay__close-button" onClick={this.close} aria-label={`Close ${title} overlay`} > <button className="overlay__close-button" onClick={this.close} aria-label={this.props.t('Overlay.AriaLabel', { title })}>
<ExitIcon focusable="false" aria-hidden="true" /> <ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
@ -101,6 +102,7 @@ Overlay.propTypes = {
ariaLabel: PropTypes.string, ariaLabel: PropTypes.string,
previousPath: PropTypes.string, previousPath: PropTypes.string,
isFixedHeight: PropTypes.bool, isFixedHeight: PropTypes.bool,
t: PropTypes.func.isRequired
}; };
Overlay.defaultProps = { Overlay.defaultProps = {
@ -113,4 +115,4 @@ Overlay.defaultProps = {
isFixedHeight: false, isFixedHeight: false,
}; };
export default Overlay; export default withTranslation()(Overlay);

View file

@ -14,3 +14,9 @@ export function clearLintMessage() {
type: ActionTypes.CLEAR_LINT_MESSAGE type: ActionTypes.CLEAR_LINT_MESSAGE
}; };
} }
export function toggleForceDesktop() {
return {
type: ActionTypes.TOGGLE_FORCE_DESKTOP
};
}

View file

@ -3,6 +3,7 @@ import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { withTranslation } from 'react-i18next';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import * as ProjectsActions from '../actions/projects'; import * as ProjectsActions from '../actions/projects';
@ -14,7 +15,7 @@ import Loader from '../../App/components/loader';
import QuickAddList from './QuickAddList'; import QuickAddList from './QuickAddList';
const projectInCollection = (project, collection) => 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 { class CollectionList extends React.Component {
constructor(props) { constructor(props) {
@ -42,9 +43,9 @@ class CollectionList extends React.Component {
getTitle() { getTitle() {
if (this.props.username === this.props.user.username) { 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) => { handleCollectionAdd = (collection) => {
@ -74,13 +75,15 @@ class CollectionList extends React.Component {
items={collectionWithSketchStatus} items={collectionWithSketchStatus}
onAdd={this.handleCollectionAdd} onAdd={this.handleCollectionAdd}
onRemove={this.handleCollectionRemove} onRemove={this.handleCollectionRemove}
t={this.props.t}
/> />
); );
} else { } else {
content = 'No collections'; content = this.props.t('AddToCollectionList.Empty');
} }
return ( return (
<div className="collection-add-sketch">
<div className="quick-add-wrapper"> <div className="quick-add-wrapper">
<Helmet> <Helmet>
<title>{this.getTitle()}</title> <title>{this.getTitle()}</title>
@ -88,6 +91,7 @@ class CollectionList extends React.Component {
{content} {content}
</div> </div>
</div>
); );
} }
} }
@ -133,7 +137,8 @@ CollectionList.propTypes = {
owner: PropTypes.shape({ owner: PropTypes.shape({
id: PropTypes.string id: PropTypes.string
}) })
}) }),
t: PropTypes.func.isRequired
}; };
CollectionList.defaultProps = { CollectionList.defaultProps = {
@ -162,4 +167,4 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionList));

View file

@ -3,6 +3,7 @@ import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { withTranslation } from 'react-i18next';
// import find from 'lodash/find'; // import find from 'lodash/find';
import * as ProjectsActions from '../actions/projects'; import * as ProjectsActions from '../actions/projects';
import * as CollectionsActions from '../actions/collections'; import * as CollectionsActions from '../actions/collections';
@ -32,9 +33,9 @@ class SketchList extends React.Component {
getSketchesTitle() { getSketchesTitle() {
if (this.props.username === this.props.user.username) { 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) => { handleCollectionAdd = (sketch) => {
@ -68,16 +69,18 @@ class SketchList extends React.Component {
/> />
); );
} else { } else {
content = 'No collections'; content = this.props.t('AddToCollectionSketchList.NoCollections');
} }
return ( return (
<div className="collection-add-sketch">
<div className="quick-add-wrapper"> <div className="quick-add-wrapper">
<Helmet> <Helmet>
<title>{this.getSketchesTitle()}</title> <title>{this.getSketchesTitle()}</title>
</Helmet> </Helmet>
{content} {content}
</div> </div>
</div>
); );
} }
} }
@ -111,6 +114,7 @@ SketchList.propTypes = {
}).isRequired, }).isRequired,
addToCollection: PropTypes.func.isRequired, addToCollection: PropTypes.func.isRequired,
removeFromCollection: PropTypes.func.isRequired, removeFromCollection: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
SketchList.defaultProps = { SketchList.defaultProps = {
@ -134,4 +138,4 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(SketchList); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(SketchList));

View file

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import classNames from 'classnames'; import classNames from 'classnames';
@ -50,9 +51,9 @@ class CollectionList extends React.Component {
getTitle() { getTitle() {
if (this.props.username === this.props.user.username) { 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) => { showAddSketches = (collectionId) => {
@ -78,7 +79,7 @@ class CollectionList extends React.Component {
_renderEmptyTable() { _renderEmptyTable() {
if (!this.props.loading && this.props.collections.length === 0) { if (!this.props.loading && this.props.collections.length === 0) {
return (<p className="sketches-table__empty">No collections.</p>); return (<p className="sketches-table__empty">{this.props.t('CollectionList.NoCollections')}</p>);
} }
return null; return null;
} }
@ -88,14 +89,14 @@ class CollectionList extends React.Component {
let buttonLabel; let buttonLabel;
if (field !== fieldName) { if (field !== fieldName) {
if (field === 'name') { if (field === 'name') {
buttonLabel = `Sort by ${displayName} ascending.`; buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { displayName });
} else { } else {
buttonLabel = `Sort by ${displayName} descending.`; buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { displayName });
} }
} else if (direction === SortingActions.DIRECTION.ASC) { } else if (direction === SortingActions.DIRECTION.ASC) {
buttonLabel = `Sort by ${displayName} descending.`; buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { displayName });
} else { } else {
buttonLabel = `Sort by ${displayName} ascending.`; buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { displayName });
} }
return buttonLabel; return buttonLabel;
} }
@ -116,10 +117,10 @@ class CollectionList extends React.Component {
> >
<span className={headerClass}>{displayName}</span> <span className={headerClass}>{displayName}</span>
{field === fieldName && direction === SortingActions.DIRECTION.ASC && {field === fieldName && direction === SortingActions.DIRECTION.ASC &&
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" /> <ArrowUpIcon role="img" aria-label={this.props.t('CollectionList.DirectionAscendingARIA')} focusable="false" />
} }
{field === fieldName && direction === SortingActions.DIRECTION.DESC && {field === fieldName && direction === SortingActions.DIRECTION.DESC &&
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" /> <ArrowDownIcon role="img" aria-label={this.props.t('CollectionList.DirectionDescendingARIA')} focusable="false" />
} }
</button> </button>
</th> </th>
@ -142,10 +143,10 @@ class CollectionList extends React.Component {
<table className="sketches-table" summary="table containing all collections"> <table className="sketches-table" summary="table containing all collections">
<thead> <thead>
<tr> <tr>
{this._renderFieldHeader('name', 'Name')} {this._renderFieldHeader('name', this.props.t('CollectionList.HeaderName'))}
{this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)} {this._renderFieldHeader('createdAt', this.props.t('CollectionList.HeaderCreatedAt', { context: mobile ? 'mobile' : '' }))}
{this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)} {this._renderFieldHeader('updatedAt', this.props.t('CollectionList.HeaderUpdatedAt', { context: mobile ? 'mobile' : '' }))}
{this._renderFieldHeader('numItems', mobile ? 'Sketches' : '# sketches')} {this._renderFieldHeader('numItems', this.props.t('CollectionList.HeaderNumItems', { context: mobile ? 'mobile' : '' }))}
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
@ -165,17 +166,15 @@ class CollectionList extends React.Component {
{ {
this.state.addingSketchesToCollectionId && ( this.state.addingSketchesToCollectionId && (
<Overlay <Overlay
title="Add sketch" title={this.props.t('CollectionList.AddSketch')}
actions={<SketchSearchbar />} actions={<SketchSearchbar />}
closeOverlay={this.hideAddSketches} closeOverlay={this.hideAddSketches}
isFixedHeight isFixedHeight
> >
<div className="collection-add-sketch">
<AddToCollectionSketchList <AddToCollectionSketchList
username={this.props.username} username={this.props.username}
collection={find(this.props.collections, { id: this.state.addingSketchesToCollectionId })} collection={find(this.props.collections, { id: this.state.addingSketchesToCollectionId })}
/> />
</div>
</Overlay> </Overlay>
) )
} }
@ -213,6 +212,7 @@ CollectionList.propTypes = {
id: PropTypes.string id: PropTypes.string
}) })
}), }),
t: PropTypes.func.isRequired,
mobile: PropTypes.bool, mobile: PropTypes.bool,
}; };
@ -244,4 +244,4 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionList));

View file

@ -4,6 +4,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { withTranslation } from 'react-i18next';
import * as ProjectActions from '../../actions/project'; import * as ProjectActions from '../../actions/project';
import * as CollectionsActions from '../../actions/collections'; import * as CollectionsActions from '../../actions/collections';
import * as IdeActions from '../../actions/ide'; import * as IdeActions from '../../actions/ide';
@ -81,7 +82,7 @@ class CollectionListRowBase extends React.Component {
handleCollectionDelete = () => { handleCollectionDelete = () => {
this.closeAll(); 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); this.props.deleteCollection(this.props.collection.id);
} }
} }
@ -130,7 +131,7 @@ class CollectionListRowBase extends React.Component {
onClick={this.toggleOptions} onClick={this.toggleOptions}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
aria-label="Toggle Open/Close collection options" aria-label={this.props.t('CollectionListRow.ToggleCollectionOptionsARIA')}
> >
<DownFilledTriangleIcon title="Menu" /> <DownFilledTriangleIcon title="Menu" />
</button> </button>
@ -145,7 +146,7 @@ class CollectionListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Add sketch {this.props.t('CollectionListRow.AddSketch')}
</button> </button>
</li> </li>
{userIsOwner && {userIsOwner &&
@ -156,7 +157,7 @@ class CollectionListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Delete {this.props.t('CollectionListRow.Delete')}
</button> </button>
</li>} </li>}
{userIsOwner && {userIsOwner &&
@ -167,7 +168,7 @@ class CollectionListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Rename {this.props.t('CollectionListRow.Rename')}
</button> </button>
</li>} </li>}
</ul> </ul>
@ -248,6 +249,7 @@ CollectionListRowBase.propTypes = {
editCollection: PropTypes.func.isRequired, editCollection: PropTypes.func.isRequired,
onAddSketches: PropTypes.func.isRequired, onAddSketches: PropTypes.func.isRequired,
mobile: PropTypes.bool, mobile: PropTypes.bool,
t: PropTypes.func.isRequired
}; };
CollectionListRowBase.defaultProps = { CollectionListRowBase.defaultProps = {
@ -258,4 +260,4 @@ function mapDispatchToPropsSketchListRow(dispatch) {
return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions, ToastActions), dispatch); return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions, ToastActions), dispatch);
} }
export default connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase); export default withTranslation()(connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase));

View file

@ -1,4 +1,6 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import { bindActionCreators } from 'redux'; 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 consoleEvents = useSelector(state => state.console);
const isExpanded = useSelector(state => state.ide.consoleIsExpanded); const isExpanded = useSelector(state => state.ide.consoleIsExpanded);
const { theme, fontSize } = useSelector(state => state.preferences); const { theme, fontSize } = useSelector(state => state.preferences);
@ -98,19 +100,19 @@ const Console = () => {
return ( return (
<section className={consoleClass} > <section className={consoleClass} >
<header className="preview-console__header"> <header className="preview-console__header">
<h2 className="preview-console__header-title">Console</h2> <h2 className="preview-console__header-title">{t('Console.Title')}</h2>
<div className="preview-console__header-buttons"> <div className="preview-console__header-buttons">
<button className="preview-console__clear" onClick={clearConsole} aria-label="Clear console"> <button className="preview-console__clear" onClick={clearConsole} aria-label={t('Console.ClearARIA')}>
Clear {t('Console.Clear')}
</button> </button>
<button <button
className="preview-console__collapse" className="preview-console__collapse"
onClick={collapseConsole} onClick={collapseConsole}
aria-label="Close console" aria-label={t('Console.CloseARIA')}
> >
<DownArrowIcon focusable="false" aria-hidden="true" /> <DownArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
<button className="preview-console__expand" onClick={expandConsole} aria-label="Open console" > <button className="preview-console__expand" onClick={expandConsole} aria-label={t('Console.OpenARIA')} >
<UpArrowIcon focusable="false" aria-hidden="true" /> <UpArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
@ -140,5 +142,9 @@ const Console = () => {
); );
}; };
Console.propTypes = {
t: PropTypes.func.isRequired,
};
export default Console;
export default withTranslation()(Console);

View file

@ -1,15 +1,16 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { withTranslation } from 'react-i18next';
class ErrorModal extends React.Component { class ErrorModal extends React.Component {
forceAuthentication() { forceAuthentication() {
return ( return (
<p> <p>
In order to save sketches, you must be logged in. Please&nbsp; {this.props.t('ErrorModal.MessageLogin')}
<Link to="/login" onClick={this.props.closeModal}>Login</Link> <Link to="/login" onClick={this.props.closeModal}> {this.props.t('ErrorModal.Login')}</Link>
&nbsp;or&nbsp; {this.props.t('ErrorModal.LoginOr')}
<Link to="/signup" onClick={this.props.closeModal}>Sign Up</Link>. <Link to="/signup" onClick={this.props.closeModal}>{this.props.t('ErrorModal.SignUp')}</Link>.
</p> </p>
); );
} }
@ -17,8 +18,8 @@ class ErrorModal extends React.Component {
staleSession() { staleSession() {
return ( return (
<p> <p>
It looks like you&apos;ve been logged out. Please&nbsp; {this.props.t('ErrorModal.MessageLoggedOut')}
<Link to="/login" onClick={this.props.closeModal}>log in</Link>. <Link to="/login" onClick={this.props.closeModal}>{this.props.t('ErrorModal.LogIn')}</Link>.
</p> </p>
); );
} }
@ -26,8 +27,7 @@ class ErrorModal extends React.Component {
staleProject() { staleProject() {
return ( return (
<p> <p>
The project you have attempted to save has been saved from another window. {this.props.t('ErrorModal.SavedDifferentWindow')}
Please refresh the page to see the latest version.
</p> </p>
); );
} }
@ -51,7 +51,8 @@ class ErrorModal extends React.Component {
ErrorModal.propTypes = { ErrorModal.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
closeModal: PropTypes.func.isRequired closeModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
export default ErrorModal; export default withTranslation()(ErrorModal);

View file

@ -3,6 +3,8 @@ import React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { withTranslation } from 'react-i18next';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files'; import * as FileActions from '../actions/files';
import DownArrowIcon from '../../../images/down-filled-triangle.svg'; import DownArrowIcon from '../../../images/down-filled-triangle.svg';
@ -152,7 +154,9 @@ export class FileNode extends React.Component {
} }
handleClickDelete = () => { 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.setState({ isDeleting: true });
this.props.resetSelectedFile(this.props.id); this.props.resetSelectedFile(this.props.id);
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100); 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 isFolder = this.props.fileType === 'folder';
const isRoot = this.props.name === 'root'; const isRoot = this.props.name === 'root';
const { t } = this.props;
return ( return (
<div className={itemClass} > <div className={itemClass} >
{ !isRoot && { !isRoot &&
@ -252,14 +258,14 @@ export class FileNode extends React.Component {
<button <button
className="sidebar__file-item-closed" className="sidebar__file-item-closed"
onClick={this.showFolderChildren} onClick={this.showFolderChildren}
aria-label="Open folder contents" aria-label={t('FileNode.OpenFolderARIA')}
> >
<FolderRightIcon className="folder-right" focusable="false" aria-hidden="true" /> <FolderRightIcon className="folder-right" focusable="false" aria-hidden="true" />
</button> </button>
<button <button
className="sidebar__file-item-open" className="sidebar__file-item-open"
onClick={this.hideFolderChildren} onClick={this.hideFolderChildren}
aria-label="Close file contents" aria-label={t('FileNode.CloseFolderARIA')}
> >
<FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" /> <FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" />
</button> </button>
@ -286,7 +292,7 @@ export class FileNode extends React.Component {
/> />
<button <button
className="sidebar__file-item-show-options" className="sidebar__file-item-show-options"
aria-label="Toggle open/close file options" aria-label={t('FileNode.ToggleFileOptionsARIA')}
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }} ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0" tabIndex="0"
onClick={this.toggleFileOptions} onClick={this.toggleFileOptions}
@ -301,35 +307,35 @@ export class FileNode extends React.Component {
<React.Fragment> <React.Fragment>
<li> <li>
<button <button
aria-label="add folder" aria-label={t('FileNode.AddFolderARIA')}
onClick={this.handleClickAddFolder} onClick={this.handleClickAddFolder}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
className="sidebar__file-item-option" className="sidebar__file-item-option"
> >
Create folder {t('FileNode.AddFolder')}
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="add file" aria-label={t('FileNode.AddFileARIA')}
onClick={this.handleClickAddFile} onClick={this.handleClickAddFile}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
className="sidebar__file-item-option" className="sidebar__file-item-option"
> >
Create file {t('FileNode.AddFile')}
</button> </button>
</li> </li>
{ this.props.authenticated && { this.props.authenticated &&
<li> <li>
<button <button
aria-label="upload file" aria-label={t('FileNode.UploadFileARIA')}
onClick={this.handleClickUploadFile} onClick={this.handleClickUploadFile}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Upload file {t('FileNode.UploadFile')}
</button> </button>
</li> </li>
} }
@ -342,7 +348,7 @@ export class FileNode extends React.Component {
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
className="sidebar__file-item-option" className="sidebar__file-item-option"
> >
Rename {t('FileNode.Rename')}
</button> </button>
</li> </li>
<li> <li>
@ -352,7 +358,7 @@ export class FileNode extends React.Component {
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
className="sidebar__file-item-option" className="sidebar__file-item-option"
> >
Delete {t('FileNode.Delete')}
</button> </button>
</li> </li>
</ul> </ul>
@ -388,6 +394,7 @@ FileNode.propTypes = {
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
openUploadFileModal: PropTypes.func.isRequired, openUploadFileModal: PropTypes.func.isRequired,
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
onClickFile: PropTypes.func onClickFile: PropTypes.func
}; };
@ -408,5 +415,8 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign(FileActions, IDEActions), 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; export default ConnectedFileNode;

View file

@ -3,6 +3,7 @@ import React from 'react';
import Dropzone from 'dropzone'; import Dropzone from 'dropzone';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import * as UploaderActions from '../actions/uploader'; import * as UploaderActions from '../actions/uploader';
import getConfig from '../../../utils/getConfig'; import getConfig from '../../../utils/getConfig';
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils'; import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
@ -30,7 +31,7 @@ class FileUploader extends React.Component {
thumbnailWidth: 200, thumbnailWidth: 200,
thumbnailHeight: 200, thumbnailHeight: 200,
acceptedFiles: fileExtensionsAndMimeTypes, 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), accept: this.props.dropzoneAcceptCallback.bind(this, userId),
sending: this.props.dropzoneSendingCallback, sending: this.props.dropzoneSendingCallback,
complete: this.props.dropzoneCompleteCallback complete: this.props.dropzoneCompleteCallback
@ -59,7 +60,8 @@ FileUploader.propTypes = {
}), }),
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string id: PropTypes.string
}) }),
t: PropTypes.func.isRequired
}; };
FileUploader.defaultProps = { FileUploader.defaultProps = {
@ -84,4 +86,4 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(UploaderActions, dispatch); return bindActionCreators(UploaderActions, dispatch);
} }
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(FileUploader));

View file

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { withTranslation } from 'react-i18next';
import Icons from './Icons'; import Icons from './Icons';
const Item = ({ const Item = ({
isAdded, onSelect, name, url isAdded, onSelect, name, url, t
}) => { }) => {
const buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection'; const buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection';
return ( return (
@ -20,7 +21,7 @@ const Item = ({
target="_blank" target="_blank"
onClick={e => e.stopPropogation()} onClick={e => e.stopPropogation()}
> >
View {t('QuickAddList.View')}
</Link> </Link>
</li> </li>
); );
@ -37,10 +38,11 @@ Item.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
isAdded: PropTypes.bool.isRequired, isAdded: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
const QuickAddList = ({ const QuickAddList = ({
items, onAdd, onRemove items, onAdd, onRemove, t
}) => { }) => {
const handleAction = (item) => { const handleAction = (item) => {
if (item.isAdded) { if (item.isAdded) {
@ -53,6 +55,7 @@ const QuickAddList = ({
return ( return (
<ul className="quick-add">{items.map(item => (<Item <ul className="quick-add">{items.map(item => (<Item
key={item.id} key={item.id}
t={t}
{...item} {...item}
onSelect={ onSelect={
(event) => { (event) => {
@ -70,6 +73,7 @@ QuickAddList.propTypes = {
items: PropTypes.arrayOf(ItemType).isRequired, items: PropTypes.arrayOf(ItemType).isRequired,
onAdd: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
export default QuickAddList; export default withTranslation()(QuickAddList);

View file

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { withTranslation } from 'react-i18next';
import CopyableInput from './CopyableInput'; import CopyableInput from './CopyableInput';
class ShareModal extends React.PureComponent { class ShareModal extends React.PureComponent {
@ -16,21 +17,21 @@ class ShareModal extends React.PureComponent {
{projectName} {projectName}
</h3> </h3>
<CopyableInput <CopyableInput
label="Embed" label={this.props.t('ShareModal.Embed')}
value={`<iframe src="${hostname}/${ownerUsername}/embed/${projectId}"></iframe>`} value={`<iframe src="${hostname}/${ownerUsername}/embed/${projectId}"></iframe>`}
/> />
<CopyableInput <CopyableInput
label="Present" label={this.props.t('ShareModal.Present')}
hasPreviewLink hasPreviewLink
value={`${hostname}/${ownerUsername}/present/${projectId}`} value={`${hostname}/${ownerUsername}/present/${projectId}`}
/> />
<CopyableInput <CopyableInput
label="Fullscreen" label={this.props.t('ShareModal.Fullscreen')}
hasPreviewLink hasPreviewLink
value={`${hostname}/${ownerUsername}/full/${projectId}`} value={`${hostname}/${ownerUsername}/full/${projectId}`}
/> />
<CopyableInput <CopyableInput
label="Edit" label={this.props.t('ShareModal.Edit')}
hasPreviewLink hasPreviewLink
value={`${hostname}/${ownerUsername}/sketches/${projectId}`} value={`${hostname}/${ownerUsername}/sketches/${projectId}`}
/> />
@ -42,7 +43,8 @@ class ShareModal extends React.PureComponent {
ShareModal.propTypes = { ShareModal.propTypes = {
projectId: PropTypes.string.isRequired, projectId: PropTypes.string.isRequired,
ownerUsername: 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);

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { withTranslation } from 'react-i18next';
import ConnectedFileNode from './FileNode'; import ConnectedFileNode from './FileNode';
import DownArrowIcon from '../../../images/down-filled-triangle.svg'; import DownArrowIcon from '../../../images/down-filled-triangle.svg';
@ -71,11 +73,11 @@ class Sidebar extends React.Component {
<section className={sidebarClass}> <section className={sidebarClass}>
<header className="sidebar__header" onContextMenu={this.toggleProjectOptions}> <header className="sidebar__header" onContextMenu={this.toggleProjectOptions}>
<h3 className="sidebar__title"> <h3 className="sidebar__title">
<span>Sketch Files</span> <span>{this.props.t('Sidebar.Title')}</span>
</h3> </h3>
<div className="sidebar__icons"> <div className="sidebar__icons">
<button <button
aria-label="Toggle open/close sketch file options" aria-label={this.props.t('Sidebar.ToggleARIA')}
className="sidebar__add" className="sidebar__add"
tabIndex="0" tabIndex="0"
ref={(element) => { this.sidebarOptions = element; }} ref={(element) => { this.sidebarOptions = element; }}
@ -88,7 +90,7 @@ class Sidebar extends React.Component {
<ul className="sidebar__project-options"> <ul className="sidebar__project-options">
<li> <li>
<button <button
aria-label="add folder" aria-label={this.props.t('Sidebar.AddFolderARIA')}
onClick={() => { onClick={() => {
this.props.newFolder(rootFile.id); this.props.newFolder(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0); setTimeout(this.props.closeProjectOptions, 0);
@ -96,12 +98,12 @@ class Sidebar extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Create folder {this.props.t('Sidebar.AddFolder')}
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="add file" aria-label={this.props.t('Sidebar.AddFileARIA')}
onClick={() => { onClick={() => {
this.props.newFile(rootFile.id); this.props.newFile(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0); setTimeout(this.props.closeProjectOptions, 0);
@ -109,14 +111,14 @@ class Sidebar extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Create file {this.props.t('Sidebar.AddFile')}
</button> </button>
</li> </li>
{ {
this.props.user.authenticated && this.props.user.authenticated &&
<li> <li>
<button <button
aria-label="upload file" aria-label={this.props.t('Sidebar.UploadFileARIA')}
onClick={() => { onClick={() => {
this.props.openUploadFileModal(rootFile.id); this.props.openUploadFileModal(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0); setTimeout(this.props.closeProjectOptions, 0);
@ -124,7 +126,7 @@ class Sidebar extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Upload file {this.props.t('Sidebar.UploadFile')}
</button> </button>
</li> </li>
} }
@ -159,11 +161,12 @@ Sidebar.propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
authenticated: PropTypes.bool.isRequired authenticated: PropTypes.bool.isRequired
}).isRequired }).isRequired,
t: PropTypes.func.isRequired,
}; };
Sidebar.defaultProps = { Sidebar.defaultProps = {
owner: undefined owner: undefined
}; };
export default Sidebar; export default withTranslation()(Sidebar);

View file

@ -2,6 +2,7 @@ import format from 'date-fns/format';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -148,14 +149,14 @@ class SketchListRowBase extends React.Component {
handleSketchDelete = () => { handleSketchDelete = () => {
this.closeAll(); 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); this.props.deleteProject(this.props.sketch.id);
} }
} }
renderViewButton = sketchURL => ( renderViewButton = sketchURL => (
<td className="sketch-list__dropdown-column"> <td className="sketch-list__dropdown-column">
<Link to={sketchURL}>View</Link> <Link to={sketchURL}>{this.props.t('SketchList.View')}</Link>
</td> </td>
) )
@ -170,7 +171,7 @@ class SketchListRowBase extends React.Component {
onClick={this.toggleOptions} onClick={this.toggleOptions}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
aria-label="Toggle Open/Close Sketch Options" aria-label={this.props.t('SketchList.ToggleLabelARIA')}
> >
<DownFilledTriangleIcon focusable="false" aria-hidden="true" /> <DownFilledTriangleIcon focusable="false" aria-hidden="true" />
</button> </button>
@ -186,7 +187,7 @@ class SketchListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Rename {this.props.t('SketchList.DropdownRename')}
</button> </button>
</li>} </li>}
<li> <li>
@ -196,7 +197,7 @@ class SketchListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Download {this.props.t('SketchList.DropdownDownload')}
</button> </button>
</li> </li>
{this.props.user.authenticated && {this.props.user.authenticated &&
@ -207,7 +208,7 @@ class SketchListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Duplicate {this.props.t('SketchList.DropdownDuplicate')}
</button> </button>
</li>} </li>}
{this.props.user.authenticated && {this.props.user.authenticated &&
@ -221,7 +222,7 @@ class SketchListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Add to collection {this.props.t('SketchList.DropdownAddToCollection')}
</button> </button>
</li>} </li>}
{ /* <li> { /* <li>
@ -242,7 +243,7 @@ class SketchListRowBase extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
Delete {this.props.t('SketchList.DropdownDelete')}
</button> </button>
</li>} </li>}
</ul>} </ul>}
@ -262,8 +263,6 @@ class SketchListRowBase extends React.Component {
url = `/${username}/sketches/${slugify(sketch.name, '_')}`; url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
} }
if (this.props.mobile) url = `/mobile${url}`;
const name = ( const name = (
<React.Fragment> <React.Fragment>
<Link to={url}> <Link to={url}>
@ -319,7 +318,8 @@ SketchListRowBase.propTypes = {
exportProjectAsZip: PropTypes.func.isRequired, exportProjectAsZip: PropTypes.func.isRequired,
changeProjectName: PropTypes.func.isRequired, changeProjectName: PropTypes.func.isRequired,
onAddToCollection: PropTypes.func.isRequired, onAddToCollection: PropTypes.func.isRequired,
mobile: PropTypes.bool mobile: PropTypes.bool,
t: PropTypes.func.isRequired
}; };
SketchListRowBase.defaultProps = { SketchListRowBase.defaultProps = {
@ -354,9 +354,9 @@ class SketchList extends React.Component {
getSketchesTitle() { getSketchesTitle() {
if (this.props.username === this.props.user.username) { if (this.props.username === this.props.user.username) {
return 'p5.js Web Editor | My sketches'; return this.props.t('SketchList.Title');
} }
return `p5.js Web Editor | ${this.props.username}'s sketches`; return this.props.t('SketchList.AnothersTitle', { anotheruser: this.props.username });
} }
hasSketches() { hasSketches() {
@ -374,7 +374,7 @@ class SketchList extends React.Component {
_renderEmptyTable() { _renderEmptyTable() {
if (!this.isLoading() && this.props.sketches.length === 0) { if (!this.isLoading() && this.props.sketches.length === 0) {
return (<p className="sketches-table__empty">No sketches.</p>); return (<p className="sketches-table__empty">{this.props.t('SketchList.NoSketches')}</p>);
} }
return null; return null;
} }
@ -384,14 +384,14 @@ class SketchList extends React.Component {
let buttonLabel; let buttonLabel;
if (field !== fieldName) { if (field !== fieldName) {
if (field === 'name') { if (field === 'name') {
buttonLabel = `Sort by ${displayName} ascending.`; buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', { displayName });
} else { } else {
buttonLabel = `Sort by ${displayName} descending.`; buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', { displayName });
} }
} else if (direction === SortingActions.DIRECTION.ASC) { } else if (direction === SortingActions.DIRECTION.ASC) {
buttonLabel = `Sort by ${displayName} descending.`; buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', { displayName });
} else { } else {
buttonLabel = `Sort by ${displayName} ascending.`; buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', { displayName });
} }
return buttonLabel; return buttonLabel;
} }
@ -412,10 +412,10 @@ class SketchList extends React.Component {
> >
<span className={headerClass}>{displayName}</span> <span className={headerClass}>{displayName}</span>
{field === fieldName && direction === SortingActions.DIRECTION.ASC && {field === fieldName && direction === SortingActions.DIRECTION.ASC &&
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" /> <ArrowUpIcon role="img" aria-label={this.props.t('SketchList.DirectionAscendingARIA')} focusable="false" />
} }
{field === fieldName && direction === SortingActions.DIRECTION.DESC && {field === fieldName && direction === SortingActions.DIRECTION.DESC &&
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" /> <ArrowDownIcon role="img" aria-label={this.props.t('SketchList.DirectionDescendingARIA')} focusable="false" />
} }
</button> </button>
</th> </th>
@ -436,9 +436,9 @@ class SketchList extends React.Component {
<table className="sketches-table" summary="table containing all saved projects"> <table className="sketches-table" summary="table containing all saved projects">
<thead> <thead>
<tr> <tr>
{this._renderFieldHeader('name', 'Sketch')} {this._renderFieldHeader('name', this.props.t('SketchList.HeaderName'))}
{this._renderFieldHeader('createdAt', `${mobile ? '' : 'Date '}Created`)} {this._renderFieldHeader('createdAt', this.props.t('SketchList.HeaderCreatedAt', { context: mobile ? 'mobile' : '' }))}
{this._renderFieldHeader('updatedAt', `${mobile ? '' : 'Date '}Updated`)} {this._renderFieldHeader('updatedAt', this.props.t('SketchList.HeaderUpdatedAt', { context: mobile ? 'mobile' : '' }))}
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
@ -453,6 +453,7 @@ class SketchList extends React.Component {
onAddToCollection={() => { onAddToCollection={() => {
this.setState({ sketchToAddToCollection: sketch }); this.setState({ sketchToAddToCollection: sketch });
}} }}
t={this.props.t}
/>))} />))}
</tbody> </tbody>
</table>} </table>}
@ -460,7 +461,7 @@ class SketchList extends React.Component {
this.state.sketchToAddToCollection && this.state.sketchToAddToCollection &&
<Overlay <Overlay
isFixedHeight isFixedHeight
title="Add to collection" title={this.props.t('SketchList.AddToCollectionOverlayTitle')}
closeOverlay={() => this.setState({ sketchToAddToCollection: null })} closeOverlay={() => this.setState({ sketchToAddToCollection: null })}
> >
<AddToCollectionList <AddToCollectionList
@ -496,6 +497,7 @@ SketchList.propTypes = {
direction: PropTypes.string.isRequired direction: PropTypes.string.isRequired
}).isRequired, }).isRequired,
mobile: PropTypes.bool, mobile: PropTypes.bool,
t: PropTypes.func.isRequired
}; };
SketchList.defaultProps = { SketchList.defaultProps = {
@ -520,4 +522,4 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(SketchList); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(SketchList));

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { withTranslation } from 'react-i18next';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import getConfig from '../../../utils/getConfig'; import getConfig from '../../../utils/getConfig';
import FileUploader from './FileUploader'; import FileUploader from './FileUploader';
@ -14,7 +15,8 @@ const limitText = prettyBytes(limit);
class UploadFileModal extends React.Component { class UploadFileModal extends React.Component {
propTypes = { propTypes = {
reachedTotalSizeLimit: PropTypes.bool.isRequired, reachedTotalSizeLimit: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired closeModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
} }
componentDidMount() { componentDidMount() {
@ -31,22 +33,18 @@ class UploadFileModal extends React.Component {
<section className="modal" ref={(element) => { this.modal = element; }}> <section className="modal" ref={(element) => { this.modal = element; }}>
<div className="modal-content"> <div className="modal-content">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Upload File</h2> <h2 className="modal__title">{this.props.t('UploadFileModal.Title')}</h2>
<button <button
className="modal__exit-button" className="modal__exit-button"
onClick={this.props.closeModal} onClick={this.props.closeModal}
aria-label="Close upload file modal" aria-label={this.props.t('UploadFileModal.CloseButtonARIA')}
> >
<ExitIcon focusable="false" aria-hidden="true" /> <ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
{ this.props.reachedTotalSizeLimit && { this.props.reachedTotalSizeLimit &&
<p> <p>
{ {this.props.t('UploadFileModal.SizeLimitError', { sizeLimit: limitText })}
`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 `
}
<Link to="/assets" onClick={this.props.closeModal}>assets</Link> <Link to="/assets" onClick={this.props.closeModal}>assets</Link>
. .
</p> </p>
@ -68,4 +66,4 @@ function mapStateToProps(state) {
}; };
} }
export default connect(mapStateToProps)(UploadFileModal); export default withTranslation()(connect(mapStateToProps)(UploadFileModal));

View file

@ -418,15 +418,15 @@ class IDEView extends React.Component {
<Overlay <Overlay
title={this.props.t('IDEView.SubmitFeedback')} title={this.props.t('IDEView.SubmitFeedback')}
previousPath={this.props.ide.previousPath} previousPath={this.props.ide.previousPath}
ariaLabel="submit-feedback" ariaLabel={this.props.t('IDEView.SubmitFeedbackARIA')}
> >
<Feedback previousPath={this.props.ide.previousPath} /> <Feedback previousPath={this.props.ide.previousPath} />
</Overlay> </Overlay>
)} )}
{this.props.location.pathname.match(/add-to-collection$/) && ( {this.props.location.pathname.match(/add-to-collection$/) && (
<Overlay <Overlay
ariaLabel="add to collection" ariaLabel={this.props.t('IDEView.AddCollectionARIA')}
title="Add to collection" title={this.props.t('IDEView.AddCollectionTitle')}
previousPath={this.props.ide.previousPath} previousPath={this.props.ide.previousPath}
actions={<CollectionSearchbar />} actions={<CollectionSearchbar />}
isFixedHeight isFixedHeight
@ -440,8 +440,8 @@ class IDEView extends React.Component {
)} )}
{this.props.ide.shareModalVisible && ( {this.props.ide.shareModalVisible && (
<Overlay <Overlay
title="Share" title={this.props.t('IDEView.ShareTitle')}
ariaLabel="share" ariaLabel={this.props.t('IDEView.ShareARIA')}
closeOverlay={this.props.closeShareModal} closeOverlay={this.props.closeShareModal}
> >
<ShareModal <ShareModal
@ -462,8 +462,8 @@ class IDEView extends React.Component {
)} )}
{this.props.ide.errorType && ( {this.props.ide.errorType && (
<Overlay <Overlay
title="Error" title={this.props.t('Common.Error')}
ariaLabel={this.props.t('Common.Error')} ariaLabel={this.props.t('Common.ErrorARIA')}
closeOverlay={this.props.hideErrorModal} closeOverlay={this.props.hideErrorModal}
> >
<ErrorModal <ErrorModal

View file

@ -12,7 +12,7 @@ import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import * as ConsoleActions from '../actions/console'; import * as ConsoleActions from '../actions/console';
import * as PreferencesActions from '../actions/preferences'; import * as PreferencesActions from '../actions/preferences';
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
// Local Imports // Local Imports
import Editor from '../components/Editor'; import Editor from '../components/Editor';
@ -58,18 +58,20 @@ const NavItem = styled.li`
position: relative; position: relative;
`; `;
const getNavOptions = (username = undefined) => const getNavOptions = (username = undefined, logoutUser = () => {}, toggleForceDesktop = () => {}) =>
(username (username
? [ ? [
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', }, { icon: PreferencesIcon, title: 'Preferences', href: '/preferences', },
{ icon: PreferencesIcon, title: 'My Stuff', href: `/mobile/${username}/sketches` }, { icon: PreferencesIcon, title: 'My Stuff', href: `/${username}/sketches` },
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' }, { icon: PreferencesIcon, title: 'Examples', href: '/p5/sketches' },
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', }, { icon: PreferencesIcon, title: 'Original Editor', action: toggleForceDesktop, },
{ icon: PreferencesIcon, title: 'Logout', action: logoutUser, },
] ]
: [ : [
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', }, { icon: PreferencesIcon, title: 'Preferences', href: '/preferences', },
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/p5/sketches' }, { icon: PreferencesIcon, title: 'Examples', href: '/p5/sketches' },
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', }, { 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 autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) => {
const { const {
autosaveProject, preferences, ide, selectedFile: file, project, user autosaveProject, preferences, ide, selectedFile: file, project
} = props; } = props;
const { selectedFile: oldFile } = prevProps; 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 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 { const {
ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, 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; } = props;
@ -204,7 +219,7 @@ const MobileIDEView = (props) => {
// Screen Modals // Screen Modals
const [toggleNavDropdown, NavDropDown] = useAsModal(<Dropdown const [toggleNavDropdown, NavDropDown] = useAsModal(<Dropdown
items={getNavOptions(username)} items={getNavOptions(username, logoutUser, toggleForceDesktop)}
align="right" align="right"
/>); />);
@ -239,6 +254,7 @@ const MobileIDEView = (props) => {
subtitle={filename} subtitle={filename}
> >
<NavItem> <NavItem>
<IconButton <IconButton
onClick={toggleNavDropdown} onClick={toggleNavDropdown}
icon={MoreIcon} icon={MoreIcon}
@ -247,7 +263,7 @@ const MobileIDEView = (props) => {
<NavDropDown /> <NavDropDown />
</NavItem> </NavItem>
<li> <li>
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" /> <IconButton to="/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
</li> </li>
</Header> </Header>
@ -307,12 +323,22 @@ MobileIDEView.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
}).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({ user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
id: PropTypes.string, id: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
}).isRequired, }).isRequired,
logoutUser: PropTypes.func.isRequired,
getProject: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired, clearPersistedState: PropTypes.func.isRequired,
params: PropTypes.shape({ params: PropTypes.shape({
@ -320,10 +346,9 @@ MobileIDEView.propTypes = {
username: PropTypes.string username: PropTypes.string
}).isRequired, }).isRequired,
unsavedChanges: PropTypes.bool.isRequired,
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
autosaveProject: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired,
@ -351,7 +376,8 @@ const mapDispatchToProps = dispatch => bindActionCreators({
...ProjectActions, ...ProjectActions,
...IDEActions, ...IDEActions,
...ConsoleActions, ...ConsoleActions,
...PreferencesActions ...PreferencesActions,
...EditorAccessibilityActions
}, dispatch); }, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView)); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));

View file

@ -1,7 +1,8 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const initialState = { const initialState = {
lintMessages: [] lintMessages: [],
forceDesktop: false
}; };
let messageId = 0; let messageId = 0;
@ -16,6 +17,8 @@ const editorAccessibility = (state = initialState, action) => {
}); });
case ActionTypes.CLEAR_LINT_MESSAGE: case ActionTypes.CLEAR_LINT_MESSAGE:
return Object.assign({}, state, { lintMessages: [] }); return Object.assign({}, state, { lintMessages: [] });
case ActionTypes.TOGGLE_FORCE_DESKTOP:
return Object.assign({}, state, { forceDesktop: !(state.forceDesktop) });
default: default:
return state; return state;
} }

View file

@ -138,8 +138,8 @@ const Panels = {
const navOptions = username => [ const navOptions = username => [
{ title: 'Create Sketch', href: '/mobile' }, { title: 'Create Sketch', href: '/' },
{ title: 'Create Collection', href: `/mobile/${username}/collections/create` } { title: 'Create Collection', href: `/${username}/collections/create` }
]; ];
@ -185,7 +185,7 @@ const MobileDashboard = ({ params, location }) => {
<NavDropdown /> <NavDropdown />
</NavItem> </NavItem>
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" /> <IconButton to="/" icon={ExitIcon} aria-label="Return to ide view" />
</Header> </Header>

View file

@ -69,7 +69,7 @@ const MobilePreferences = () => {
<Screen fullscreen> <Screen fullscreen>
<section> <section>
<Header transparent title="Preferences"> <Header transparent title="Preferences">
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" /> <IconButton to="/" icon={ExitIcon} aria-label="Return to ide view" />
</Header> </Header>
<section className="preferences"> <section className="preferences">
<Content> <Content>

View file

@ -39,7 +39,7 @@ const MobileSketchView = () => {
return ( return (
<Screen fullscreen> <Screen fullscreen>
<Header <Header
leftButton={<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />} leftButton={<IconButton to="/" icon={ExitIcon} aria-label="Return to original editor" />}
title={projectName} title={projectName}
/> />
<Content> <Content>

View file

@ -229,7 +229,7 @@ export function updateSettings(formValues) {
dispatch(updateSettingsSuccess(response.data)); dispatch(updateSettingsSuccess(response.data));
browserHistory.push('/'); browserHistory.push('/');
dispatch(showToast(5500)); dispatch(showToast(5500));
dispatch(setToastText('Settings saved.')); dispatch(setToastText('Toast.SettingsSaved'));
}) })
.catch((error) => { .catch((error) => {
const { response } = error; const { response } = error;

View file

@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { withTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import Button from '../../../common/Button'; 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 ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import RemoveIcon from '../../../images/close.svg'; import RemoveIcon from '../../../images/close.svg';
const ShareURL = ({ value }) => { const ShareURL = ({ value, t }) => {
const [showURL, setShowURL] = useState(false); const [showURL, setShowURL] = useState(false);
const node = useRef(); const node = useRef();
@ -56,11 +57,11 @@ const ShareURL = ({ value }) => {
onClick={() => setShowURL(!showURL)} onClick={() => setShowURL(!showURL)}
iconAfter={<DropdownArrowIcon />} iconAfter={<DropdownArrowIcon />}
> >
Share {t('Collection.Share')}
</Button> </Button>
{ showURL && { showURL &&
<div className="collection__share-dropdown"> <div className="collection__share-dropdown">
<CopyableInput value={value} label="Link to Collection" /> <CopyableInput value={value} label={t('Collection.URLLink')} />
</div> </div>
} }
</div> </div>
@ -69,22 +70,23 @@ const ShareURL = ({ value }) => {
ShareURL.propTypes = { ShareURL.propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
t: PropTypes.func.isRequired
}; };
const CollectionItemRowBase = ({ const CollectionItemRowBase = ({
collection, item, isOwner, removeFromCollection collection, item, isOwner, removeFromCollection, t
}) => { }) => {
const projectIsDeleted = item.isDeleted; const projectIsDeleted = item.isDeleted;
const handleSketchRemove = () => { const handleSketchRemove = () => {
const name = projectIsDeleted ? 'deleted sketch' : item.project.name; 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); removeFromCollection(collection.id, item.projectId);
} }
}; };
const name = projectIsDeleted ? <span>Sketch was deleted</span> : ( const name = projectIsDeleted ? <span>{t('Collection.SketchDeleted')}</span> : (
<Link to={`/${item.project.user.username}/sketches/${item.projectId}`}> <Link to={`/${item.project.user.username}/sketches/${item.projectId}`}>
{item.project.name} {item.project.name}
</Link> </Link>
@ -106,7 +108,7 @@ const CollectionItemRowBase = ({
<button <button
className="collection-row__remove-button" className="collection-row__remove-button"
onClick={handleSketchRemove} onClick={handleSketchRemove}
aria-label="Remove sketch from collection" aria-label={t('Collection.SketchRemoveARIA')}
> >
<RemoveIcon focusable="false" aria-hidden="true" /> <RemoveIcon focusable="false" aria-hidden="true" />
</button> </button>
@ -138,7 +140,8 @@ CollectionItemRowBase.propTypes = {
username: PropTypes.string, username: PropTypes.string,
authenticated: PropTypes.bool.isRequired authenticated: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
removeFromCollection: PropTypes.func.isRequired removeFromCollection: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
function mapDispatchToPropsSketchListRow(dispatch) { function mapDispatchToPropsSketchListRow(dispatch) {
@ -163,9 +166,9 @@ class Collection extends React.Component {
getTitle() { getTitle() {
if (this.props.username === this.props.user.username) { 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() { getUsername() {
@ -257,27 +260,27 @@ class Collection extends React.Component {
InputComponent="textarea" InputComponent="textarea"
value={description} value={description}
onChange={handleEditCollectionDescription} onChange={handleEditCollectionDescription}
emptyPlaceholder="Add description" emptyPlaceholder={this.props.t('Collection.DescriptionPlaceholder')}
/> : /> :
description description
} }
</p> </p>
<p className="collection-metadata__user">Collection by{' '} <p className="collection-metadata__user">{this.props.t('Collection.By')}
<Link to={`${hostname}/${username}/sketches`}>{owner.username}</Link> <Link to={`${hostname}/${username}/sketches`}>{owner.username}</Link>
</p> </p>
<p className="collection-metadata__user">{items.length} sketch{items.length === 1 ? '' : 'es'}</p> <p className="collection-metadata__user">{this.props.t('Collection.NumSketches', { count: items.length }) }</p>
</div> </div>
<div className="collection-metadata__column--right"> <div className="collection-metadata__column--right">
<p className="collection-metadata__share"> <p className="collection-metadata__share">
<ShareURL value={`${baseURL}${id}`} /> <ShareURL value={`${baseURL}${id}`} t={this.props.t} />
</p> </p>
{ {
this.isOwner() && this.isOwner() &&
<Button onClick={this.showAddSketches}> <Button onClick={this.showAddSketches}>
Add Sketch {this.props.t('Collection.AddSketch')}
</Button> </Button>
} }
</div> </div>
@ -304,7 +307,7 @@ class Collection extends React.Component {
this.props.collection.items.length > 0; this.props.collection.items.length > 0;
if (!isLoading && !hasCollectionItems) { if (!isLoading && !hasCollectionItems) {
return (<p className="collection-empty-message">No sketches in collection</p>); return (<p className="collection-empty-message">{this.props.t('Collection.NoSketches')}</p>);
} }
return null; return null;
} }
@ -314,14 +317,14 @@ class Collection extends React.Component {
let buttonLabel; let buttonLabel;
if (field !== fieldName) { if (field !== fieldName) {
if (field === 'name') { if (field === 'name') {
buttonLabel = `Sort by ${displayName} ascending.`; buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { displayName });
} else { } else {
buttonLabel = `Sort by ${displayName} descending.`; buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { displayName });
} }
} else if (direction === SortingActions.DIRECTION.ASC) { } else if (direction === SortingActions.DIRECTION.ASC) {
buttonLabel = `Sort by ${displayName} descending.`; buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { displayName });
} else { } else {
buttonLabel = `Sort by ${displayName} ascending.`; buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { displayName });
} }
return buttonLabel; return buttonLabel;
} }
@ -342,10 +345,10 @@ class Collection extends React.Component {
> >
<span className={headerClass}>{displayName}</span> <span className={headerClass}>{displayName}</span>
{field === fieldName && direction === SortingActions.DIRECTION.ASC && {field === fieldName && direction === SortingActions.DIRECTION.ASC &&
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" /> <ArrowUpIcon role="img" aria-label={this.props.t('Collection.DirectionAscendingARIA')} focusable="false" />
} }
{field === fieldName && direction === SortingActions.DIRECTION.DESC && {field === fieldName && direction === SortingActions.DIRECTION.DESC &&
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" /> <ArrowDownIcon role="img" aria-label={this.props.t('Collection.DirectionDescendingARIA')} focusable="false" />
} }
</button> </button>
</th> </th>
@ -371,9 +374,9 @@ class Collection extends React.Component {
<table className="sketches-table" summary="table containing all collections"> <table className="sketches-table" summary="table containing all collections">
<thead> <thead>
<tr> <tr>
{this._renderFieldHeader('name', 'Name')} {this._renderFieldHeader('name', this.props.t('Collection.HeaderName'))}
{this._renderFieldHeader('createdAt', 'Date Added')} {this._renderFieldHeader('createdAt', this.props.t('Collection.HeaderCreatedAt'))}
{this._renderFieldHeader('user', 'Owner')} {this._renderFieldHeader('user', this.props.t('Collection.HeaderUser'))}
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
@ -386,6 +389,7 @@ class Collection extends React.Component {
username={this.getUsername()} username={this.getUsername()}
collection={this.props.collection} collection={this.props.collection}
isOwner={isOwner} isOwner={isOwner}
t={this.props.t}
/>))} />))}
</tbody> </tbody>
</table> </table>
@ -393,14 +397,15 @@ class Collection extends React.Component {
{ {
this.state.isAddingSketches && ( this.state.isAddingSketches && (
<Overlay <Overlay
title="Add sketch" title={this.props.t('Collection.AddSketch')}
actions={<SketchSearchbar />} actions={<SketchSearchbar />}
closeOverlay={this.hideAddSketches} closeOverlay={this.hideAddSketches}
isFixedHeight isFixedHeight
> >
<div className="collection-add-sketch"> <AddToCollectionSketchList
<AddToCollectionSketchList username={this.props.username} collection={this.props.collection} /> username={this.props.username}
</div> collection={this.props.collection}
/>
</Overlay> </Overlay>
) )
} }
@ -436,7 +441,8 @@ Collection.propTypes = {
sorting: PropTypes.shape({ sorting: PropTypes.shape({
field: PropTypes.string.isRequired, field: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired direction: PropTypes.string.isRequired
}).isRequired }).isRequired,
t: PropTypes.func.isRequired
}; };
Collection.defaultProps = { Collection.defaultProps = {
@ -467,4 +473,4 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(Collection); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Collection));

View file

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as CollectionsActions from '../../IDE/actions/collections'; import * as CollectionsActions from '../../IDE/actions/collections';
@ -24,7 +25,7 @@ class CollectionCreate extends React.Component {
} }
getTitle() { getTitle() {
return 'p5.js Web Editor | Create collection'; return this.props.t('CollectionCreate.Title');
} }
handleTextChange = field => (evt) => { handleTextChange = field => (evt) => {
@ -55,34 +56,34 @@ class CollectionCreate extends React.Component {
</Helmet> </Helmet>
<div className="sketches-table-container"> <div className="sketches-table-container">
<form className="form" onSubmit={this.handleCreateCollection}> <form className="form" onSubmit={this.handleCreateCollection}>
{creationError && <span className="form-error">Couldn&apos;t create collection</span>} {creationError && <span className="form-error">{this.props.t('CollectionCreate.FormError')}</span>}
<p className="form__field"> <p className="form__field">
<label htmlFor="name" className="form__label">Collection name</label> <label htmlFor="name" className="form__label">{this.props.t('CollectionCreate.FormLabel')}</label>
<input <input
className="form__input" className="form__input"
aria-label="name" aria-label={this.props.t('CollectionCreate.FormLabelARIA')}
type="text" type="text"
id="name" id="name"
value={name} value={name}
placeholder={generatedCollectionName} placeholder={generatedCollectionName}
onChange={this.handleTextChange('name')} onChange={this.handleTextChange('name')}
/> />
{invalid && <span className="form-error">Collection name is required</span>} {invalid && <span className="form-error">{this.props.t('CollectionCreate.NameRequired')}</span>}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="description" className="form__label">Description (optional)</label> <label htmlFor="description" className="form__label">{this.props.t('CollectionCreate.Description')}</label>
<textarea <textarea
className="form__input form__input-flexible-height" className="form__input form__input-flexible-height"
aria-label="description" aria-label={this.props.t('CollectionCreate.DescriptionARIA')}
type="text" type="text"
id="description" id="description"
value={description} value={description}
onChange={this.handleTextChange('description')} onChange={this.handleTextChange('description')}
placeholder="My fave sketches" placeholder={this.props.t('CollectionCreate.DescriptionPlaceholder')}
rows="4" rows="4"
/> />
</p> </p>
<Button type="submit" disabled={invalid}>Create collection</Button> <Button type="submit" disabled={invalid}>{this.props.t('CollectionCreate.SubmitCollectionCreate')}</Button>
</form> </form>
</div> </div>
</div> </div>
@ -95,7 +96,8 @@ CollectionCreate.propTypes = {
username: PropTypes.string, username: PropTypes.string,
authenticated: PropTypes.bool.isRequired authenticated: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
createCollection: PropTypes.func.isRequired createCollection: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
function mapStateToProps(state, ownProps) { function mapStateToProps(state, ownProps) {
@ -108,4 +110,4 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign({}, CollectionsActions), dispatch); return bindActionCreators(Object.assign({}, CollectionsActions), dispatch);
} }
export default connect(mapStateToProps, mapDispatchToProps)(CollectionCreate); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionCreate));

View file

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { withTranslation } from 'react-i18next';
import { Link } from 'react-router'; import { Link } from 'react-router';
const TabKey = { const TabKey = {
@ -28,12 +29,14 @@ Tab.propTypes = {
to: PropTypes.string.isRequired, to: PropTypes.string.isRequired,
}; };
const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => ( const DashboardTabSwitcher = ({
currentTab, isOwner, username, t
}) => (
<ul className="dashboard-header__switcher"> <ul className="dashboard-header__switcher">
<div className="dashboard-header__tabs"> <div className="dashboard-header__tabs">
<Tab to={`/${username}/sketches`} isSelected={currentTab === TabKey.sketches}>Sketches</Tab> <Tab to={`/${username}/sketches`} isSelected={currentTab === TabKey.sketches}>{t('DashboardTabSwitcher.Sketches')}</Tab>
<Tab to={`/${username}/collections`} isSelected={currentTab === TabKey.collections}>Collections</Tab> <Tab to={`/${username}/collections`} isSelected={currentTab === TabKey.collections}>{t('DashboardTabSwitcher.Collections')}</Tab>
{isOwner && <Tab to={`/${username}/assets`} isSelected={currentTab === TabKey.assets}>Assets</Tab>} {isOwner && <Tab to={`/${username}/assets`} isSelected={currentTab === TabKey.assets}>{t('DashboardTabSwitcher.Assets')}</Tab>}
</div> </div>
</ul> </ul>
); );
@ -42,6 +45,9 @@ DashboardTabSwitcher.propTypes = {
currentTab: PropTypes.string.isRequired, currentTab: PropTypes.string.isRequired,
isOwner: PropTypes.bool.isRequired, isOwner: PropTypes.bool.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
t: PropTypes.func.isRequired
}; };
export { DashboardTabSwitcher as default, TabKey };
const DashboardTabSwitcherPublic = withTranslation()(DashboardTabSwitcher);
export { DashboardTabSwitcherPublic as default, TabKey };

View file

@ -0,0 +1,45 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { remSize } from '../../../theme';
const ResponsiveForm = styled.div`
.form-container__content {
width: unset !important;
padding-top: ${remSize(16)};
padding-bottom: ${remSize(64)};
}
.form__input {
min-width: unset;
padding: 0px ${remSize(12)};
height: ${remSize(28)};
}
.form-container__title { margin-bottom: ${remSize(14)}}
p.form__field { margin-top: 0px !important; }
label.form__label { margin-top: ${remSize(8)} !important; }
.form-error { width: 100% }
.nav__items-right:last-child { display: none }
.form-container {
height: 100%
}
.nav__dropdown {
right: 0 !important;
left: unset !important;
}
.form-container__stack {
svg {
width: ${remSize(12)};
height: ${remSize(12)}
}
a { padding: 0px }
}
`;
export default ResponsiveForm;

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import CollectionCreate from '../components/CollectionCreate'; import CollectionCreate from '../components/CollectionCreate';
@ -25,10 +27,10 @@ class CollectionView extends React.Component {
pageTitle() { pageTitle() {
if (this.isCreatePage()) { if (this.isCreatePage()) {
return 'Create collection'; return this.props.t('CollectionView.TitleCreate');
} }
return 'collection'; return this.props.t('CollectionView.TitleDefault');
} }
isOwner() { isOwner() {
@ -87,6 +89,7 @@ CollectionView.propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
username: PropTypes.string, username: PropTypes.string,
}), }),
t: PropTypes.func.isRequired
}; };
export default connect(mapStateToProps, mapDispatchToProps)(CollectionView); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionView));

View file

@ -3,6 +3,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import { withTranslation } from 'react-i18next';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
@ -17,7 +18,7 @@ import SketchList from '../../IDE/components/SketchList';
import { CollectionSearchbar, SketchSearchbar } from '../../IDE/components/Searchbar'; import { CollectionSearchbar, SketchSearchbar } from '../../IDE/components/Searchbar';
import CollectionCreate from '../components/CollectionCreate'; import CollectionCreate from '../components/CollectionCreate';
import DashboardTabSwitcher, { TabKey } from '../components/DashboardTabSwitcher'; import DashboardTabSwitcherPublic, { TabKey } from '../components/DashboardTabSwitcher';
class DashboardView extends React.Component { class DashboardView extends React.Component {
static defaultProps = { static defaultProps = {
@ -75,7 +76,7 @@ class DashboardView extends React.Component {
browserHistory.push(`/${this.ownerName()}/collections`); browserHistory.push(`/${this.ownerName()}/collections`);
} }
renderActionButton(tabKey, username) { renderActionButton(tabKey, username, t) {
switch (tabKey) { switch (tabKey) {
case TabKey.assets: case TabKey.assets:
return this.isOwner() && <AssetSize />; return this.isOwner() && <AssetSize />;
@ -83,7 +84,7 @@ class DashboardView extends React.Component {
return this.isOwner() && ( return this.isOwner() && (
<React.Fragment> <React.Fragment>
<Button to={`/${username}/collections/create`}> <Button to={`/${username}/collections/create`}>
Create collection {t('DashboardView.CreateCollection')}
</Button> </Button>
<CollectionSearchbar /> <CollectionSearchbar />
</React.Fragment>); </React.Fragment>);
@ -91,7 +92,7 @@ class DashboardView extends React.Component {
default: default:
return ( return (
<React.Fragment> <React.Fragment>
{this.isOwner() && <Button to="/">New sketch</Button>} {this.isOwner() && <Button to="/">{t('DashboardView.NewSketch')}</Button>}
<SketchSearchbar /> <SketchSearchbar />
</React.Fragment> </React.Fragment>
); );
@ -114,7 +115,7 @@ class DashboardView extends React.Component {
const currentTab = this.selectedTabKey(); const currentTab = this.selectedTabKey();
const isOwner = this.isOwner(); const isOwner = this.isOwner();
const { username } = this.props.params; const { username } = this.props.params;
const actions = this.renderActionButton(currentTab, username); const actions = this.renderActionButton(currentTab, username, this.props.t);
return ( return (
<div className="dashboard"> <div className="dashboard">
@ -124,7 +125,7 @@ class DashboardView extends React.Component {
<div className="dashboard-header__header"> <div className="dashboard-header__header">
<h2 className="dashboard-header__header__title">{this.ownerName()}</h2> <h2 className="dashboard-header__header__title">{this.ownerName()}</h2>
<div className="dashboard-header__nav"> <div className="dashboard-header__nav">
<DashboardTabSwitcher currentTab={currentTab} isOwner={isOwner} username={username} /> <DashboardTabSwitcherPublic currentTab={currentTab} isOwner={isOwner} username={username} />
{actions && {actions &&
<div className="dashboard-header__actions"> <div className="dashboard-header__actions">
{actions} {actions}
@ -139,7 +140,7 @@ class DashboardView extends React.Component {
</main> </main>
{this.isCollectionCreate() && {this.isCollectionCreate() &&
<Overlay <Overlay
title="Create collection" title={this.props.t('DashboardView.CreateCollectionOverlay')}
closeOverlay={this.returnToDashboard} closeOverlay={this.returnToDashboard}
> >
<CollectionCreate /> <CollectionCreate />
@ -176,6 +177,7 @@ DashboardView.propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
username: PropTypes.string, username: PropTypes.string,
}), }),
t: PropTypes.func.isRequired
}; };
export default connect(mapStateToProps, mapDispatchToProps)(DashboardView); export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(DashboardView));

View file

@ -10,6 +10,7 @@ import LoginForm from '../components/LoginForm';
import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateLogin } from '../../../utils/reduxFormUtils';
import SocialAuthButton from '../components/SocialAuthButton'; import SocialAuthButton from '../components/SocialAuthButton';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import ResponsiveForm from '../components/ResponsiveForm';
class LoginView extends React.Component { class LoginView extends React.Component {
constructor(props) { constructor(props) {
@ -79,13 +80,13 @@ LoginView.propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool authenticated: PropTypes.bool
}), }),
t: PropTypes.func.isRequired t: PropTypes.func.isRequired,
}; };
LoginView.defaultProps = { LoginView.defaultProps = {
user: { user: {
authenticated: false authenticated: false
} },
}; };
export default withTranslation()(reduxForm({ export default withTranslation()(reduxForm({

View file

@ -11,6 +11,8 @@ import apiClient from '../../../utils/apiClient';
import { validateSignup } from '../../../utils/reduxFormUtils'; import { validateSignup } from '../../../utils/reduxFormUtils';
import SocialAuthButton from '../components/SocialAuthButton'; import SocialAuthButton from '../components/SocialAuthButton';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import ResponsiveForm from '../components/ResponsiveForm';
class SignupView extends React.Component { class SignupView extends React.Component {
gotoHomePage = () => { gotoHomePage = () => {
@ -110,13 +112,13 @@ SignupView.propTypes = {
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool authenticated: PropTypes.bool
}), }),
t: PropTypes.func.isRequired t: PropTypes.func.isRequired,
}; };
SignupView.defaultProps = { SignupView.defaultProps = {
user: { user: {
authenticated: false authenticated: false
} },
}; };
export default withTranslation()(reduxForm({ export default withTranslation()(reduxForm({

View file

@ -1,5 +1,6 @@
import { Route, IndexRoute } from 'react-router'; import { Route, IndexRoute } from 'react-router';
import React from 'react'; import React from 'react';
import App from './modules/App/App'; import App from './modules/App/App';
import IDEView from './modules/IDE/pages/IDEView'; import IDEView from './modules/IDE/pages/IDEView';
import MobileIDEView from './modules/IDE/pages/MobileIDEView'; import MobileIDEView from './modules/IDE/pages/MobileIDEView';
@ -19,6 +20,7 @@ import MobileDashboardView from './modules/Mobile/MobileDashboardView';
import { getUser } from './modules/User/actions'; import { getUser } from './modules/User/actions';
import { stopSketch } from './modules/IDE/actions/ide'; import { stopSketch } from './modules/IDE/actions/ide';
import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth'; import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth';
import { mobileFirst, responsiveForm } from './utils/responsive';
const checkAuth = (store) => { const checkAuth = (store) => {
store.dispatch(getUser()); store.dispatch(getUser());
@ -27,16 +29,17 @@ const checkAuth = (store) => {
// TODO: This short-circuit seems unnecessary - using the mobile <Switch /> navigator (future) should prevent this from being called // TODO: This short-circuit seems unnecessary - using the mobile <Switch /> navigator (future) should prevent this from being called
const onRouteChange = (store) => { const onRouteChange = (store) => {
const path = window.location.pathname; const path = window.location.pathname;
if (path.includes('/mobile/preview')) return; if (path.includes('preview')) return;
store.dispatch(stopSketch()); store.dispatch(stopSketch());
}; };
const routes = store => ( const routes = store => (
<Route path="/" component={App} onChange={() => { onRouteChange(store); }}> <Route path="/" component={App} onChange={() => { onRouteChange(store); }}>
<IndexRoute component={IDEView} onEnter={checkAuth(store)} /> <IndexRoute onEnter={checkAuth(store)} component={mobileFirst(MobileIDEView, IDEView)} />
<Route path="/login" component={userIsNotAuthenticated(LoginView)} />
<Route path="/signup" component={userIsNotAuthenticated(SignupView)} /> <Route path="/login" component={userIsNotAuthenticated(mobileFirst(responsiveForm(LoginView), LoginView))} />
<Route path="/signup" component={userIsNotAuthenticated(mobileFirst(responsiveForm(SignupView), SignupView))} />
<Route path="/reset-password" component={userIsNotAuthenticated(ResetPasswordView)} /> <Route path="/reset-password" component={userIsNotAuthenticated(ResetPasswordView)} />
<Route path="/verify" component={EmailVerificationView} /> <Route path="/verify" component={EmailVerificationView} />
<Route <Route
@ -46,28 +49,24 @@ const routes = store => (
<Route path="/projects/:project_id" component={IDEView} /> <Route path="/projects/:project_id" component={IDEView} />
<Route path="/:username/full/:project_id" component={FullView} /> <Route path="/:username/full/:project_id" component={FullView} />
<Route path="/full/:project_id" component={FullView} /> <Route path="/full/:project_id" component={FullView} />
<Route path="/sketches" component={createRedirectWithUsername('/:username/sketches')} />
<Route path="/:username/assets" component={userIsAuthenticated(userIsAuthorized(DashboardView))} /> <Route path="/:username/assets" component={userIsAuthenticated(userIsAuthorized(mobileFirst(MobileDashboardView, DashboardView)))} />
<Route path="/assets" component={createRedirectWithUsername('/:username/assets')} /> <Route path="/:username/sketches" component={mobileFirst(MobileDashboardView, DashboardView)} />
<Route path="/account" component={userIsAuthenticated(AccountView)} /> <Route path="/:username/sketches/:project_id" component={mobileFirst(MobileIDEView, IDEView)} />
<Route path="/:username/sketches/:project_id" component={IDEView} /> <Route path="/:username/sketches/:project_id/add-to-collection" component={mobileFirst(MobileIDEView, IDEView)} />
<Route path="/:username/sketches/:project_id/add-to-collection" component={IDEView} /> <Route path="/:username/collections" component={mobileFirst(MobileDashboardView, DashboardView)} />
<Route path="/:username/sketches" component={DashboardView} />
<Route path="/:username/collections" component={DashboardView} />
<Route path="/:username/collections/create" component={DashboardView} /> <Route path="/:username/collections/create" component={DashboardView} />
<Route path="/:username/collections/:collection_id" component={CollectionView} /> <Route path="/:username/collections/:collection_id" component={CollectionView} />
<Route path="/sketches" component={createRedirectWithUsername('/:username/sketches')} />
<Route path="/assets" component={createRedirectWithUsername('/:username/assets')} />
<Route path="/account" component={userIsAuthenticated(AccountView)} />
<Route path="/about" component={IDEView} /> <Route path="/about" component={IDEView} />
{/* Mobile-only Routes */}
<Route path="/mobile/preview" component={MobileSketchView} /> <Route path="/preview" component={MobileSketchView} />
<Route path="/mobile/preferences" component={MobilePreferences} /> <Route path="/preferences" component={MobilePreferences} />
<Route path="/mobile" component={MobileIDEView} />
<Route path="/mobile/:username/sketches/:project_id" component={MobileIDEView} />
<Route path="/mobile/:username/assets" component={userIsAuthenticated(userIsAuthorized(MobileDashboardView))} />
<Route path="/mobile/:username/sketches" component={MobileDashboardView} />
<Route path="/mobile/:username/collections" component={MobileDashboardView} />
<Route path="/mobile/:username/collections/create" component={MobileDashboardView} />
</Route> </Route>
); );

View file

@ -88,6 +88,7 @@ $themes: (
nav-border-color: $middle-light, nav-border-color: $middle-light,
error-color: $p5js-pink, error-color: $p5js-pink,
table-row-stripe-color: $medium-light, table-row-stripe-color: $medium-light,
table-row-stripe-color-alternate: $medium-light,
codefold-icon-open: url(../images/triangle-arrow-down.svg?byUrl), codefold-icon-open: url(../images/triangle-arrow-down.svg?byUrl),
codefold-icon-closed: url(../images/triangle-arrow-right.svg?byUrl), codefold-icon-closed: url(../images/triangle-arrow-right.svg?byUrl),
@ -163,6 +164,7 @@ $themes: (
nav-border-color: $middle-dark, nav-border-color: $middle-dark,
error-color: $p5js-pink, error-color: $p5js-pink,
table-row-stripe-color: $dark, table-row-stripe-color: $dark,
table-row-stripe-color-alternate: $darker,
codefold-icon-open: url(../images/triangle-arrow-down-white.svg?byUrl), codefold-icon-open: url(../images/triangle-arrow-down-white.svg?byUrl),
codefold-icon-closed: url(../images/triangle-arrow-right-white.svg?byUrl), codefold-icon-closed: url(../images/triangle-arrow-right-white.svg?byUrl),
@ -236,6 +238,7 @@ $themes: (
nav-border-color: $middle-dark, nav-border-color: $middle-dark,
error-color: $p5-contrast-pink, error-color: $p5-contrast-pink,
table-row-stripe-color: $dark, table-row-stripe-color: $dark,
table-row-stripe-color-alternate: $darker,
codefold-icon-open: url(../images/triangle-arrow-down-white.svg?byUrl), codefold-icon-open: url(../images/triangle-arrow-down-white.svg?byUrl),
codefold-icon-closed: url(../images/triangle-arrow-right-white.svg?byUrl), codefold-icon-closed: url(../images/triangle-arrow-right-white.svg?byUrl),

View file

@ -1,11 +1,16 @@
.quick-add-wrapper { .quick-add-wrapper {
min-width: #{600 / $base-font-size}rem; min-width: #{600 / $base-font-size}rem;
overflow-y: scroll; padding: #{24 / $base-font-size}rem;
height: 100%;
} }
.quick-add { .quick-add {
width: auto; width: auto;
padding: #{24 / $base-font-size}rem; overflow-y: scroll;
height: 100%;
@include themify() {
border: 1px solid getThemifyVariable('modal-border-color');
}
} }
.quick-add__item { .quick-add__item {
@ -23,7 +28,7 @@
.quick-add__item:nth-child(odd) { .quick-add__item:nth-child(odd) {
@include themify() { @include themify() {
background: getThemifyVariable('table-row-stripe-color'); background: getThemifyVariable('table-row-stripe-color-alternate');
} }
} }

View file

@ -0,0 +1,23 @@
import React from 'react';
import { useSelector } from 'react-redux';
import MediaQuery from 'react-responsive';
import ResponsiveForm from '../modules/User/components/ResponsiveForm';
export const mobileEnabled = () => (window.process.env.MOBILE_ENABLED === true);
export const mobileFirst = (MobileComponent, Fallback) => (props) => {
const { forceDesktop } = useSelector(state => state.editorAccessibility);
return (
<MediaQuery minWidth={770}>
{matches => ((matches || forceDesktop || (!mobileEnabled()))
? <Fallback {...props} />
: <MobileComponent {...props} />)}
</MediaQuery>
);
};
export const responsiveForm = DesktopComponent => props => (
<ResponsiveForm>
<DesktopComponent {...props} />
</ResponsiveForm>
);

32
package-lock.json generated
View file

@ -13124,6 +13124,11 @@
} }
} }
}, },
"css-mediaquery": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
"integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA="
},
"css-select": { "css-select": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
@ -19020,6 +19025,11 @@
} }
} }
}, },
"hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"i18next": { "i18next": {
"version": "19.5.4", "version": "19.5.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.5.4.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.5.4.tgz",
@ -25866,6 +25876,14 @@
"unquote": "^1.1.0" "unquote": "^1.1.0"
} }
}, },
"matchmediaquery": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz",
"integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==",
"requires": {
"css-mediaquery": "^0.1.2"
}
},
"material-colors": { "material-colors": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
@ -32233,6 +32251,17 @@
} }
} }
}, },
"react-responsive": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.1.0.tgz",
"integrity": "sha512-U8Nv2/ZWACIw/fAE9XNPbc2Xo33X5q1bcCASc2SufvJ9ifB+o/rokfogfznSVcvS22hN1rafGi0uZD6GiVFEHw==",
"requires": {
"hyphenate-style-name": "^1.0.0",
"matchmediaquery": "^0.3.0",
"prop-types": "^15.6.1",
"shallow-equal": "^1.1.0"
}
},
"react-router": { "react-router": {
"version": "3.2.5", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.5.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.5.tgz",
@ -34519,8 +34548,7 @@
"shallow-equal": { "shallow-equal": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
"dev": true
}, },
"shallowequal": { "shallowequal": {
"version": "1.1.0", "version": "1.1.0",

View file

@ -201,6 +201,7 @@
"react-hot-loader": "^4.12.19", "react-hot-loader": "^4.12.19",
"react-i18next": "^11.5.0", "react-i18next": "^11.5.0",
"react-redux": "^7.2.0", "react-redux": "^7.2.0",
"react-responsive": "^8.1.0",
"react-router": "^3.2.5", "react-router": "^3.2.5",
"react-split-pane": "^0.1.89", "react-split-pane": "^0.1.89",
"react-tabs": "^2.3.1", "react-tabs": "^2.3.1",

View file

@ -132,10 +132,6 @@ app.get(
// isomorphic rendering // isomorphic rendering
app.use('/', serverRoutes); app.use('/', serverRoutes);
if (process.env.MOBILE_ENABLED) {
app.use('/mobile', serverRoutes);
}
app.use(assetRoutes); app.use(assetRoutes);
app.use('/', embedRoutes); app.use('/', embedRoutes);

View file

@ -88,7 +88,8 @@
"SketchSaved": "Sketch saved.", "SketchSaved": "Sketch saved.",
"SketchFailedSave": "Failed to save sketch.", "SketchFailedSave": "Failed to save sketch.",
"AutosaveEnabled": "Autosave enabled.", "AutosaveEnabled": "Autosave enabled.",
"LangChange": "Language changed" "LangChange": "Language changed",
"SettingsSaved": "Settings saved."
}, },
"Toolbar": { "Toolbar": {
"Preview": "Preview", "Preview": "Preview",
@ -174,18 +175,42 @@
} }
}, },
"Sidebar": { "Sidebar": {
"Create": "Create", "Title": "Sketch Files",
"EnterName": "enter a name", "ToggleARIA": "Toggle open/close sketch file options",
"Add": "Add", "AddFolder": "Create folder",
"Folder": "Folder" "AddFolderARIA": "add folder",
"AddFile": "Create file",
"AddFileARIA": "add file",
"UploadFile": "Upload file",
"UploadFileARIA": "upload file"
},
"FileNode": {
"OpenFolderARIA": "Open folder contents",
"CloseFolderARIA": "Close folder contents",
"ToggleFileOptionsARIA": "Toggle open/close file options",
"AddFolder": "Create folder",
"AddFolderARIA": "add folder",
"AddFile": "Create file",
"AddFileARIA": "add file",
"UploadFile": "Upload file",
"UploadFileARIA": "upload file",
"Rename": "Rename",
"Delete": "Delete"
}, },
"Common": { "Common": {
"Error": "Error", "Error": "Error",
"ErrorARIA": "Error",
"Save": "Save", "Save": "Save",
"p5logoARIA": "p5.js Logo" "p5logoARIA": "p5.js Logo",
"DeleteConfirmation": "Are you sure you want to delete {{name}}?"
}, },
"IDEView": { "IDEView": {
"SubmitFeedback": "Submit Feedback" "SubmitFeedback": "Submit Feedback",
"SubmitFeedbackARIA": "submit-feedback",
"AddCollectionTitle": "Add to collection",
"AddCollectionARIA":"add to collection",
"ShareTitle": "Share",
"ShareARIA":"share"
}, },
"NewFileModal": { "NewFileModal": {
"Title": "Create File", "Title": "Create File",
@ -308,7 +333,6 @@
"AlreadyHave": "Already have an account?", "AlreadyHave": "Already have an account?",
"Login": "Log In" "Login": "Log In"
}, },
"EmailVerificationView": { "EmailVerificationView": {
"Title": "p5.js Web Editor | Email Verification", "Title": "p5.js Web Editor | Email Verification",
"Verify": "Verify your email", "Verify": "Verify your email",
@ -316,5 +340,139 @@
"Checking": "Validating token, please wait...", "Checking": "Validating token, please wait...",
"Verified": "All done, your email address has been verified.", "Verified": "All done, your email address has been verified.",
"InvalidState": "Something went wrong." "InvalidState": "Something went wrong."
},
"UploadFileModal": {
"Title": "Upload File",
"CloseButtonARIA": "Close upload file modal",
"SizeLimitError": "Error: You cannot upload any more files. You have reached the total size limit of {{sizeLimit}}.\n If you would like to upload more, please remove the ones you aren't using anymore by\n in your "
},
"FileUploader": {
"DictDefaultMessage": "Drop files here or click to use the file browser"
},
"ErrorModal": {
"MessageLogin": "In order to save sketches, you must be logged in. Please ",
"Login": "Login",
"LoginOr": " or ",
"SignUp": "Sign Up",
"MessageLoggedOut": "It looks like you've been logged out. Please ",
"LogIn": "log in",
"SavedDifferentWindow": "The project you have attempted to save has been saved from another window.\n Please refresh the page to see the latest version."
},
"ShareModal": {
"Embed": "Embed",
"Present": "Present",
"Fullscreen": "Fullscreen",
"Edit": "Edit"
},
"CollectionView": {
"TitleCreate": "Create collection",
"TitleDefault": "collection"
},
"Collection": {
"Title": "p5.js Web Editor | My collections",
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s collections",
"Share": "Share",
"URLLink": "Link to Collection",
"AddSketch": "Add Sketch",
"DeleteFromCollection": "Are you sure you want to remove {{name_sketch}} from this collection?",
"SketchDeleted": "Sketch deleted",
"SketchRemoveARIA": "Remove sketch from collection",
"DescriptionPlaceholder": "Add description",
"Description": "description",
"NumSketches": "{{count}} sketch",
"NumSketches_plural": "{{count}} sketches",
"By":"Collection by ",
"NoSketches": "No sketches in collection",
"HeaderName": "Name",
"HeaderCreatedAt": "Date Added",
"HeaderUser": "Owner",
"DirectionAscendingARIA": "Ascending",
"DirectionDescendingARIA": "Descending",
"ButtonLabelAscendingARIA": "Sort by {{displayName}} ascending.",
"ButtonLabelDescendingARIA": "Sort by {{displayName}} descending."
},
"AddToCollectionList": {
"Title": "p5.js Web Editor | My collections",
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s collections",
"Empty": "No collections"
},
"CollectionCreate": {
"Title": "p5.js Web Editor | Create collection",
"FormError": "Couldn't create collection",
"FormLabel": "Collection name",
"FormLabelARIA": "name",
"NameRequired": "Collection name is required",
"Description": "Description (optional)",
"DescriptionARIA": "description",
"DescriptionPlaceholder": "My fave sketches",
"SubmitCollectionCreate": "Create collection"
},
"DashboardView": {
"CreateCollection": "Create collection",
"NewSketch": "New sketch",
"CreateCollectionOverlay": "Create collection"
},
"DashboardTabSwitcher": {
"Sketches": "Sketches",
"Collections": "Collections",
"Assets": "Assets"
},
"CollectionList": {
"Title": "p5.js Web Editor | My collections",
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s collections",
"NoCollections": "No collections.",
"HeaderName": "Name",
"HeaderCreatedAt": "Date Created",
"HeaderCreatedAt_mobile": "Created",
"HeaderUpdatedAt": "Date Updated",
"HeaderUpdatedAt_mobile": "Updated",
"HeaderNumItems": "# sketches",
"HeaderNumItems_mobile": "# sketches",
"DirectionAscendingARIA": "Ascending",
"DirectionDescendingARIA": "Descending",
"ButtonLabelAscendingARIA": "Sort by {{displayName}} ascending.",
"ButtonLabelDescendingARIA": "Sort by {{displayName}} descending.",
"AddSketch": "Add Sketch"
},
"CollectionListRow": {
"ToggleCollectionOptionsARIA": "Toggle Open/Close collection options",
"AddSketch": "Add sketch",
"Delete": "Delete",
"Rename": "Rename"
},
"Overlay": {
"AriaLabel": "Close {{title}} overlay"
},
"QuickAddList":{
"ButtonLabelRemove": "Remove from collection",
"ButtonLabelAddToCollection": "Add to collection",
"View": "View"
},
"SketchList": {
"View": "View",
"Title": "p5.js Web Editor | My sketches",
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s sketches",
"ToggleLabelARIA": "Toggle Open/Close Sketch Options",
"DropdownRename": "Rename",
"DropdownDownload": "Download",
"DropdownDuplicate": "Duplicate",
"DropdownAddToCollection": "Add to collection",
"DropdownDelete": "Delete",
"DirectionAscendingARIA": "Ascending",
"DirectionDescendingARIA": "Descending",
"ButtonLabelAscendingARIA": "Sort by {{displayName}} ascending.",
"ButtonLabelDescendingARIA": "Sort by {{displayName}} descending.",
"AddToCollectionOverlayTitle": "Add to collection",
"HeaderName": "Sketch",
"HeaderCreatedAt": "Date Created",
"HeaderCreatedAt_mobile": "Created",
"HeaderUpdatedAt": "Date Updated",
"HeaderUpdatedAt_mobile": "Updated",
"NoSketches": "No sketches."
},
"AddToCollectionSketchList": {
"Title": "p5.js Web Editor | My sketches",
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s sketches",
"NoCollections": "No collections."
} }
} }

View file

@ -88,7 +88,8 @@
"SketchSaved": "Bosquejo guardado.", "SketchSaved": "Bosquejo guardado.",
"SketchFailedSave": "Fallo al guardar el bosquejo.", "SketchFailedSave": "Fallo al guardar el bosquejo.",
"AutosaveEnabled": "Grabado automático activado.", "AutosaveEnabled": "Grabado automático activado.",
"LangChange": "Lenguaje cambiado" "LangChange": "Lenguaje cambiado",
"SettingsSaved": "Configuración guardada."
}, },
"Toolbar": { "Toolbar": {
"Preview": "Vista previa", "Preview": "Vista previa",
@ -174,18 +175,42 @@
} }
}, },
"Sidebar": { "Sidebar": {
"Create": "Crear", "Title": "Archivos de Bosquejo",
"EnterName": "Introduce un nombre", "ToggleARIA": "Alternar abrir/cerrar opciones de archivo de bosquejo",
"Add": "Agregar", "AddFolder": "Crear directorio",
"Folder": "Directorio" "AddFolderARIA": "Agregar directorio",
"AddFile": "Crear archivo",
"AddFileARIA": "agregar archivo",
"UploadFile": "Subir archivo",
"UploadFileARIA": "Subir archivo"
},
"FileNode": {
"OpenFolderARIA": "Abrir contenidos del directorio",
"CloseFolderARIA": "Cerrar contenidos del directorio",
"ToggleFileOptionsARIA": "Alternar abrir/cerrar opciones de archivo",
"AddFolder": "Crear directorio",
"AddFolderARIA": "Agregar directorio",
"AddFile": "Crear archivo",
"AddFileARIA": "agregar archivo",
"UploadFile": "Subir archivo",
"UploadFileARIA": "Subir archivo",
"Rename": "Renombrar",
"Delete": "Borrar"
}, },
"Common": { "Common": {
"Error": "Error", "Error": "Error",
"ErrorARIA": "Error",
"Save": "Guardar", "Save": "Guardar",
"p5logoARIA": "Logo p5.js " "p5logoARIA": "Logo p5.js ",
"DeleteConfirmation": "¿Estás seguro que quieres borrar {{name}}?"
}, },
"IDEView": { "IDEView": {
"SubmitFeedback": "Enviar retroalimentación" "SubmitFeedback": "Enviar retroalimentación",
"SubmitFeedbackARIA": "Enviar retroalimentación",
"AddCollectionTitle": "Agregar a colección",
"AddCollectionARIA":"Agregar a colección",
"ShareTitle": "Compartir",
"ShareARIA":"compartir"
}, },
"NewFileModal": { "NewFileModal": {
"Title": "Crear Archivo", "Title": "Crear Archivo",
@ -216,7 +241,7 @@
"ResetPasswordView": { "ResetPasswordView": {
"Title": "Editor Web p5.js | Regenerar Contraseña", "Title": "Editor Web p5.js | Regenerar Contraseña",
"Reset": "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.", "Submitted": "Tu correo para regenerar la contraseña debe llegar pronto. Si no lo ves, revisa\n tu carpeta de spam puesto que algunas veces puede terminar ahí.",
"Login": "Ingresa", "Login": "Ingresa",
"LoginOr": "o", "LoginOr": "o",
"SignUp": "Registráte" "SignUp": "Registráte"
@ -264,7 +289,7 @@
"AccessTokensTab": "Tokens de acceso" "AccessTokensTab": "Tokens de acceso"
}, },
"APIKeyForm": { "APIKeyForm": {
"ConfirmDelete": "¿Estas seguro que quieres borrar {{key_label}}?", "ConfirmDelete": "¿Estás 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.", "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", "CreateToken": "Crear nuevo token",
"TokenLabel": "¿Para que es este token?", "TokenLabel": "¿Para que es este token?",
@ -315,5 +340,139 @@
"Checking": "Validando token, por favor espera...", "Checking": "Validando token, por favor espera...",
"Verified": "Concluido, tu correo electrónico ha sido verificado.", "Verified": "Concluido, tu correo electrónico ha sido verificado.",
"InvalidState": "Algo salió mal." "InvalidState": "Algo salió mal."
},
"UploadFileModal": {
"Title": "Subir Archivo",
"CloseButtonARIA": "Cerrar diálogo para subir archivo",
"SizeLimitError": "Error: No puedes subir archivos. Has alcanzado el limite de tamaño total de {{sizeLimit}}.\n Si quieres agregar más,por favor remueve alugnos que no estes usando en tus "
},
"FileUploader": {
"DictDefaultMessage": "Deposita los archivos aquí o haz click para usar el navegador de archivos"
},
"ErrorModal": {
"MessageLogin": "Para poder guardar bosquejos, debes ingresar a tu cuenta. Por favor ",
"Login": "Ingresa",
"LoginOr": " o ",
"SignUp": "Registráte",
"MessageLoggedOut": "Parece que has salido de tu cuenta. Por favor ",
"LogIn": "ingresa",
"SavedDifferentWindow": " El proyecto que has intentado guardar ha sido guardado desde otra ventana.\n Por favor refresca la página para ver la versión más actual."
},
"ShareModal": {
"Embed": "Incrustar",
"Present": "Presentar",
"Fullscreen": "Pantalla Completa",
"Edit": "Editar"
},
"CollectionView": {
"TitleCreate": "Crear colección",
"TitleDefault": "colección"
},
"Collection": {
"Title": "p5.js Web Editor | Mis colecciones",
"AnothersTitle": "Editor Web p5.js | Colecciones de {{anotheruser}}",
"Share": "Compartir",
"URLLink": "Liga a la Colección",
"AddSketch": "Agregar Bosquejo",
"DeleteFromCollection": "¿Estás seguro que quieres remover {{name_sketch}} de esta colección?",
"SketchDeleted": "El bosquejo fue eliminado",
"SketchRemoveARIA": "Remover bosquejo de la colección",
"DescriptionPlaceholder": "Agregar descripción",
"Description": "descripción",
"NumSketches": "{{count}} bosquejo",
"NumSketches_plural": "{{count}} bosquejos",
"By":"Colección por ",
"NoSketches": "No hay bosquejos en la colección",
"HeaderName": "Nombre",
"HeaderCreatedAt": "Fecha de agregación",
"HeaderUser": "Propietario",
"DirectionAscendingARIA": "Ascendente",
"DirectionDescendingARIA": "Descendente",
"ButtonLabelAscendingARIA": "Ordenar por {{displayName}} ascendente.",
"ButtonLabelDescendingARIA": "Ordenar por {{displayName}} descendente."
},
"AddToCollectionList": {
"Title": "p5.js Web Editor | Mis colecciones",
"AnothersTitle": "Editor Web p5.js | Colecciones de {{anotheruser}}",
"Empty": "No hay colecciones"
},
"CollectionCreate": {
"Title": "Editor Web p5.js | Crear colección",
"FormError": "No se pudo crear colección",
"FormLabel": "Nombre colección ",
"FormLabelARIA": "nombre de la colección",
"NameRequired": "Se requiere nombre de colección",
"Description": "Descripción (opcional)",
"DescriptionARIA": "descripción",
"DescriptionPlaceholder": "Mis bosquejos favoritos",
"SubmitCollectionCreate": "Crear colección"
},
"DashboardView": {
"CreateCollection": "Crear colección",
"NewSketch": "Nuevo bosquejo",
"CreateCollectionOverlay": "Crear colección"
},
"DashboardTabSwitcher": {
"Sketches": "Bosquejos",
"Collections": "Colecciones ",
"Assets": "Assets"
},
"CollectionList": {
"Title": "p5.js Web Editor | Mis colecciones",
"AnothersTitle": "Editor Web p5.js | Colecciones de {{anotheruser}}",
"NoCollections": "No hay colecciones.",
"HeaderName": "Nombre",
"HeaderCreatedAt": "Fecha Creación",
"HeaderCreatedAt_mobile": "Creación",
"HeaderUpdatedAt": "Fecha Actualización",
"HeaderUpdatedAt_mobile": "Actualización",
"HeaderNumItems": "# bosquejos",
"HeaderNumItems_mobile": "Bosquejos",
"DirectionAscendingARIA": "Ascendente",
"DirectionDescendingARIA": "Descendente",
"ButtonLabelAscendingARIA": "Ordenar por {{displayName}} ascendente.",
"ButtonLabelDescendingARIA": "Ordenar por {{displayName}} descendente.",
"AddSketch": "Agregar Bosquejo"
},
"CollectionListRow": {
"ToggleCollectionOptionsARIA": "Alternar Abrir/Cerrar opciones de colección",
"AddSketch": "Agregar bosquejo",
"Delete": "Borrar",
"Rename": "Renombrar"
},
"Overlay": {
"AriaLabel": "Cerrar la capa {{title}}"
},
"QuickAddList":{
"ButtonLabelRemove": "Remove from collection",
"ButtonLabelAddToCollection": "Add to collection",
"View": "Ver"
},
"SketchList": {
"View": "Ver",
"Title": "p5.js Web Editor | Mis bosquejos",
"AnothersTitle": "Editor Web p5.js | Bosquejos de {{anotheruser}}",
"ToggleLabelARIA": "Alternar Abrir/Cerrar Opciones de Bosquejo",
"DropdownRename": "Renombrar",
"DropdownDownload": "Descargar",
"DropdownDuplicate": "Duplicar",
"DropdownAddToCollection": "Agregar a Colección",
"DropdownDelete": "Borrar",
"DirectionAscendingARIA": "Ascendente",
"DirectionDescendingARIA": "Descendente",
"ButtonLabelAscendingARIA": "Ordenar por {{displayName}} ascendente.",
"ButtonLabelDescendingARIA": "Ordenar por {{displayName}} descendente.",
"AddToCollectionOverlayTitle": "Agregar a colección",
"HeaderName": "Bosquejo",
"HeaderCreatedAt": "Fecha Creación",
"HeaderCreatedAt_mobile": "Creación",
"HeaderUpdatedAt": "Fecha Actualización",
"HeaderUpdatedAt_mobile": "Actualización",
"NoSketches": "No hay bosquejos."
},
"AddToCollectionSketchList": {
"Title": "p5.js Web Editor | Mis bosquejos",
"AnothersTitle": "Editor Web p5.js | Bosquejos de {{anotheruser}}",
"NoCollections": "No hay colecciones."
} }
} }