🔀 merge from develop
This commit is contained in:
commit
d7106fedef
26 changed files with 320 additions and 102 deletions
|
@ -1,11 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
import AddIcon from '../images/plus.svg';
|
import AddIcon from '../images/plus.svg';
|
||||||
import RemoveIcon from '../images/minus.svg';
|
import RemoveIcon from '../images/minus.svg';
|
||||||
|
|
||||||
const AddRemoveButton = ({ type, onClick }) => {
|
const AddRemoveButton = ({ type, onClick, t }) => {
|
||||||
const alt = type === 'add' ? 'Add to collection' : 'Remove from collection';
|
const alt = type === 'add' ? t('AddRemoveButton.AltAddARIA') : t('AddRemoveButton.AltRemoveARIA');
|
||||||
const Icon = type === 'add' ? AddIcon : RemoveIcon;
|
const Icon = type === 'add' ? AddIcon : RemoveIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -22,6 +24,7 @@ const AddRemoveButton = ({ type, onClick }) => {
|
||||||
AddRemoveButton.propTypes = {
|
AddRemoveButton.propTypes = {
|
||||||
type: PropTypes.oneOf(['add', 'remove']).isRequired,
|
type: PropTypes.oneOf(['add', 'remove']).isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddRemoveButton;
|
export default withTranslation()(AddRemoveButton);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { withRouter } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import i18next from 'i18next';
|
import { languageKeyToLabel } from '../i18n';
|
||||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||||
import * as toastActions from '../modules/IDE/actions/toast';
|
import * as toastActions from '../modules/IDE/actions/toast';
|
||||||
import * as projectActions from '../modules/IDE/actions/project';
|
import * as projectActions from '../modules/IDE/actions/project';
|
||||||
|
@ -549,7 +549,7 @@ class Nav extends React.PureComponent {
|
||||||
|
|
||||||
renderLanguageMenu(navDropdownState) {
|
renderLanguageMenu(navDropdownState) {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<React.Fragment>
|
||||||
<li className={navDropdownState.lang}>
|
<li className={navDropdownState.lang}>
|
||||||
<button
|
<button
|
||||||
onClick={this.toggleDropdownForLang}
|
onClick={this.toggleDropdownForLang}
|
||||||
|
@ -561,7 +561,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header"> {this.props.t('Nav.Lang')}</span>
|
<span className="nav__item-header"> {languageKeyToLabel(this.props.language)}</span>
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
|
@ -597,7 +597,7 @@ class Nav extends React.PureComponent {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,6 +605,7 @@ class Nav extends React.PureComponent {
|
||||||
renderUnauthenticatedUserMenu(navDropdownState) {
|
renderUnauthenticatedUserMenu(navDropdownState) {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
|
{getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<Link to="/login" className="nav__auth-button">
|
<Link to="/login" className="nav__auth-button">
|
||||||
<span className="nav__item-header">{this.props.t('Nav.Login')}</span>
|
<span className="nav__item-header">{this.props.t('Nav.Login')}</span>
|
||||||
|
@ -623,10 +624,7 @@ class Nav extends React.PureComponent {
|
||||||
renderAuthenticatedUserMenu(navDropdownState) {
|
renderAuthenticatedUserMenu(navDropdownState) {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
{getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
|
||||||
<span>{this.props.t('Nav.Auth.Hello')}, {this.props.user.username}!</span>
|
|
||||||
</li>
|
|
||||||
<span className="nav__item-spacer">|</span>
|
|
||||||
<li className={navDropdownState.account}>
|
<li className={navDropdownState.account}>
|
||||||
<button
|
<button
|
||||||
className="nav__item-header"
|
className="nav__item-header"
|
||||||
|
@ -639,7 +637,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.props.t('Nav.Auth.MyAccount')}
|
<span>{this.props.t('Nav.Auth.Hello')}, {this.props.user.username}!</span>
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
|
@ -755,7 +753,6 @@ class Nav extends React.PureComponent {
|
||||||
<header>
|
<header>
|
||||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||||
{this.renderLeftLayout(navDropdownState)}
|
{this.renderLeftLayout(navDropdownState)}
|
||||||
{getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
|
|
||||||
{this.renderUserMenu(navDropdownState)}
|
{this.renderUserMenu(navDropdownState)}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@ -809,6 +806,7 @@ Nav.propTypes = {
|
||||||
}),
|
}),
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
setLanguage: PropTypes.func.isRequired,
|
setLanguage: PropTypes.func.isRequired,
|
||||||
|
language: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
Nav.defaultProps = {
|
Nav.defaultProps = {
|
||||||
|
@ -829,7 +827,8 @@ function mapStateToProps(state) {
|
||||||
project: state.project,
|
project: state.project,
|
||||||
user: state.user,
|
user: state.user,
|
||||||
unsavedChanges: state.ide.unsavedChanges,
|
unsavedChanges: state.ide.unsavedChanges,
|
||||||
rootFile: state.files.filter(file => file.name === 'root')[0]
|
rootFile: state.files.filter(file => file.name === 'root')[0],
|
||||||
|
language: state.preferences.language
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
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';
|
||||||
|
|
||||||
import LogoIcon from '../images/p5js-logo-small.svg';
|
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||||
import CodeIcon from '../images/code.svg';
|
import CodeIcon from '../images/code.svg';
|
||||||
|
|
||||||
const PreviewNav = ({ owner, project }) => (
|
const PreviewNav = ({ owner, project, t }) => (
|
||||||
<nav className="nav preview-nav">
|
<nav className="nav preview-nav">
|
||||||
<div className="nav__items-left">
|
<div className="nav__items-left">
|
||||||
<div className="nav__item-logo">
|
<div className="nav__item-logo">
|
||||||
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
<LogoIcon role="img" aria-label={t('Common.p5logoARIA')} focusable="false" className="svg__logo" />
|
||||||
</div>
|
</div>
|
||||||
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
|
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
|
||||||
<p className="toolbar__project-owner">by</p>
|
<p className="toolbar__project-owner">{t('PreviewNav.ByUser')}</p>
|
||||||
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
|
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="nav__items-right">
|
<div className="nav__items-right">
|
||||||
<Link to={`/${owner.username}/sketches/${project.id}`} aria-label="Edit Sketch" >
|
<Link to={`/${owner.username}/sketches/${project.id}`} aria-label={t('PreviewNav.EditSketchARIA')} >
|
||||||
<CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" />
|
<CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +32,7 @@ PreviewNav.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PreviewNav;
|
export default withTranslation()(PreviewNav);
|
||||||
|
|
|
@ -46,7 +46,8 @@ describe('Nav', () => {
|
||||||
id: 'root-file'
|
id: 'root-file'
|
||||||
},
|
},
|
||||||
t: jest.fn(),
|
t: jest.fn(),
|
||||||
setLanguage: jest.fn()
|
setLanguage: jest.fn(),
|
||||||
|
language: 'en-US'
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
|
|
26
client/i18n-test.js
Normal file
26
client/i18n-test.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import translations from '../translations/locales/en-US/translations.json';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
lng: 'en-US',
|
||||||
|
fallbackLng: 'en-US',
|
||||||
|
|
||||||
|
// have a common namespace used around the full app
|
||||||
|
ns: ['translations'],
|
||||||
|
defaultNS: 'translations',
|
||||||
|
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // not needed for react!!
|
||||||
|
},
|
||||||
|
|
||||||
|
resources: { 'en-US': { translations } },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default i18n;
|
|
@ -6,6 +6,14 @@ import Backend from 'i18next-http-backend';
|
||||||
const fallbackLng = ['en-US'];
|
const fallbackLng = ['en-US'];
|
||||||
const availableLanguages = ['en-US', 'es-419'];
|
const availableLanguages = ['en-US', 'es-419'];
|
||||||
|
|
||||||
|
export function languageKeyToLabel(lang) {
|
||||||
|
const languageMap = {
|
||||||
|
'en-US': 'English',
|
||||||
|
'es-419': 'Español'
|
||||||
|
};
|
||||||
|
return languageMap[lang];
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
loadPath: '/locales/{{lng}}/translations.json',
|
loadPath: '/locales/{{lng}}/translations.json',
|
||||||
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
||||||
|
|
|
@ -3,21 +3,3 @@ 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;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { bindActionCreators } from 'redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Loader from '../../App/components/loader';
|
import Loader from '../../App/components/loader';
|
||||||
import * as AssetActions from '../actions/assets';
|
import * as AssetActions from '../actions/assets';
|
||||||
|
@ -85,7 +86,7 @@ class AssetListRowBase 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 Asset Options"
|
aria-label={this.props.t('AssetList.ToggleOpenCloseARIA')}
|
||||||
>
|
>
|
||||||
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
|
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -100,7 +101,7 @@ class AssetListRowBase extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
Delete
|
{this.props.t('AssetList.Delete')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -111,7 +112,7 @@ class AssetListRowBase extends React.Component {
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
className="asset-table__action-option"
|
className="asset-table__action-option"
|
||||||
>
|
>
|
||||||
Open in New Tab
|
{this.props.t('AssetList.OpenNewTab')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>}
|
</ul>}
|
||||||
|
@ -131,7 +132,8 @@ AssetListRowBase.propTypes = {
|
||||||
size: PropTypes.number.isRequired
|
size: PropTypes.number.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
deleteAssetRequest: PropTypes.func.isRequired,
|
deleteAssetRequest: PropTypes.func.isRequired,
|
||||||
username: PropTypes.string.isRequired
|
username: PropTypes.string.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToPropsAssetListRow(state) {
|
function mapStateToPropsAssetListRow(state) {
|
||||||
|
@ -153,7 +155,7 @@ class AssetList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssetsTitle() {
|
getAssetsTitle() {
|
||||||
return 'p5.js Web Editor | My assets';
|
return this.props.t('AssetList.Title');
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAssets() {
|
hasAssets() {
|
||||||
|
@ -167,13 +169,13 @@ class AssetList extends React.Component {
|
||||||
|
|
||||||
renderEmptyTable() {
|
renderEmptyTable() {
|
||||||
if (!this.props.loading && this.props.assetList.length === 0) {
|
if (!this.props.loading && this.props.assetList.length === 0) {
|
||||||
return (<p className="asset-table__empty">No uploaded assets.</p>);
|
return (<p className="asset-table__empty">{this.props.t('AssetList.NoUploadedAssets')}</p>);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { assetList } = this.props;
|
const { assetList, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<article className="asset-table-container">
|
<article className="asset-table-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -185,9 +187,9 @@ class AssetList extends React.Component {
|
||||||
<table className="asset-table">
|
<table className="asset-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{t('AssetList.HeaderName')}</th>
|
||||||
<th>Size</th>
|
<th>{t('AssetList.HeaderSize')}</th>
|
||||||
<th>Sketch</th>
|
<th>{t('AssetList.HeaderSketch')}</th>
|
||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -212,7 +214,8 @@ AssetList.propTypes = {
|
||||||
sketchId: PropTypes.string
|
sketchId: PropTypes.string
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
getAssets: PropTypes.func.isRequired,
|
getAssets: PropTypes.func.isRequired,
|
||||||
loading: PropTypes.bool.isRequired
|
loading: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
@ -227,4 +230,4 @@ function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(Object.assign({}, AssetActions), dispatch);
|
return bindActionCreators(Object.assign({}, AssetActions), dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AssetList);
|
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(AssetList));
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Clipboard from 'clipboard';
|
import Clipboard from 'clipboard';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ShareIcon from '../../../images/share.svg';
|
import ShareIcon from '../../../images/share.svg';
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ class CopyableInput extends React.Component {
|
||||||
<div className={copyableInputClass}>
|
<div className={copyableInputClass}>
|
||||||
<div
|
<div
|
||||||
className="copyable-input__value-container tooltipped-no-delay"
|
className="copyable-input__value-container tooltipped-no-delay"
|
||||||
aria-label="Copied to Clipboard!"
|
aria-label={this.props.t('CopyableInput.CopiedARIA')}
|
||||||
ref={(element) => { this.tooltip = element; }}
|
ref={(element) => { this.tooltip = element; }}
|
||||||
onMouseLeave={this.onMouseLeaveHandler}
|
onMouseLeave={this.onMouseLeaveHandler}
|
||||||
>
|
>
|
||||||
|
@ -69,7 +70,7 @@ class CopyableInput extends React.Component {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={value}
|
href={value}
|
||||||
className="copyable-input__preview"
|
className="copyable-input__preview"
|
||||||
aria-label={`Open ${label} view in new tab`}
|
aria-label={this.props.t('CopyableInput.CopiedARIA', { label })}
|
||||||
>
|
>
|
||||||
<ShareIcon focusable="false" aria-hidden="true" />
|
<ShareIcon focusable="false" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -82,11 +83,12 @@ class CopyableInput extends React.Component {
|
||||||
CopyableInput.propTypes = {
|
CopyableInput.propTypes = {
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
hasPreviewLink: PropTypes.bool
|
hasPreviewLink: PropTypes.bool,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
CopyableInput.defaultProps = {
|
CopyableInput.defaultProps = {
|
||||||
hasPreviewLink: false
|
hasPreviewLink: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CopyableInput;
|
export default withTranslation()(CopyableInput);
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import i18next from 'i18next';
|
||||||
import EditIcon from '../../../images/pencil.svg';
|
import EditIcon from '../../../images/pencil.svg';
|
||||||
|
|
||||||
|
|
||||||
// TODO I think this needs a description prop so that it's accessible
|
// TODO I think this needs a description prop so that it's accessible
|
||||||
function EditableInput({
|
function EditableInput({
|
||||||
validate,
|
validate,
|
||||||
|
@ -58,7 +60,7 @@ function EditableInput({
|
||||||
<button
|
<button
|
||||||
className="editable-input__label"
|
className="editable-input__label"
|
||||||
onClick={beginEditing}
|
onClick={beginEditing}
|
||||||
aria-label={`Edit ${displayValue} value`}
|
aria-label={this.props.t('EditableInput.EditValue', { display: displayValue })}
|
||||||
>
|
>
|
||||||
<span>{displayValue}</span>
|
<span>{displayValue}</span>
|
||||||
<EditIcon
|
<EditIcon
|
||||||
|
@ -84,7 +86,7 @@ function EditableInput({
|
||||||
}
|
}
|
||||||
|
|
||||||
EditableInput.defaultProps = {
|
EditableInput.defaultProps = {
|
||||||
emptyPlaceholder: 'No value',
|
emptyPlaceholder: i18next.t('EditableInput.EmptyPlaceholder'),
|
||||||
InputComponent: 'input',
|
InputComponent: 'input',
|
||||||
inputProps: {},
|
inputProps: {},
|
||||||
validate: () => true,
|
validate: () => true,
|
||||||
|
@ -99,6 +101,7 @@ EditableInput.propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
validate: PropTypes.func,
|
validate: PropTypes.func,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditableInput;
|
export default withTranslation()(EditableInput);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CodeMirror from 'codemirror';
|
import CodeMirror from 'codemirror';
|
||||||
import beautifyJS from 'js-beautify';
|
import beautifyJS from 'js-beautify';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import 'codemirror/mode/css/css';
|
import 'codemirror/mode/css/css';
|
||||||
import 'codemirror/addon/selection/active-line';
|
import 'codemirror/addon/selection/active-line';
|
||||||
import 'codemirror/addon/lint/lint';
|
import 'codemirror/addon/lint/lint';
|
||||||
|
@ -148,7 +149,7 @@ class Editor extends React.Component {
|
||||||
}, 1000));
|
}, 1000));
|
||||||
|
|
||||||
this._cm.on('keyup', () => {
|
this._cm.on('keyup', () => {
|
||||||
const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`;
|
const temp = this.props.t('Editor.KeyUpLineNumber', { lineNumber: parseInt((this._cm.getCursor().line) + 1, 10) });
|
||||||
document.getElementById('current-line').innerHTML = temp;
|
document.getElementById('current-line').innerHTML = temp;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -329,14 +330,14 @@ class Editor extends React.Component {
|
||||||
<section className={editorSectionClass} >
|
<section className={editorSectionClass} >
|
||||||
<header className="editor__header">
|
<header className="editor__header">
|
||||||
<button
|
<button
|
||||||
aria-label="Open Sketch files navigation"
|
aria-label={this.props.t('Editor.OpenSketchARIA')}
|
||||||
className="sidebar__contract"
|
className="sidebar__contract"
|
||||||
onClick={this.props.collapseSidebar}
|
onClick={this.props.collapseSidebar}
|
||||||
>
|
>
|
||||||
<LeftArrowIcon focusable="false" aria-hidden="true" />
|
<LeftArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="Close sketch files navigation"
|
aria-label={this.props.t('Editor.CloseSketchARIA')}
|
||||||
className="sidebar__expand"
|
className="sidebar__expand"
|
||||||
onClick={this.props.expandSidebar}
|
onClick={this.props.expandSidebar}
|
||||||
>
|
>
|
||||||
|
@ -347,7 +348,7 @@ class Editor extends React.Component {
|
||||||
{this.props.file.name}
|
{this.props.file.name}
|
||||||
<span className="editor__unsaved-changes">
|
<span className="editor__unsaved-changes">
|
||||||
{this.props.unsavedChanges ?
|
{this.props.unsavedChanges ?
|
||||||
<UnsavedChangesDotIcon role="img" aria-label="Sketch has unsaved changes" focusable="false" /> :
|
<UnsavedChangesDotIcon role="img" aria-label={this.props.t('Editor.UnsavedChangesARIA')} focusable="false" /> :
|
||||||
null}
|
null}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -415,7 +416,8 @@ Editor.propTypes = {
|
||||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
provideController: PropTypes.func.isRequired
|
provideController: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
Editor.defaultProps = {
|
Editor.defaultProps = {
|
||||||
|
@ -466,4 +468,4 @@ function mapDispatchToProps(dispatch) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Editor);
|
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Editor));
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
class EditorAccessibility extends React.Component {
|
class EditorAccessibility extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -17,14 +18,14 @@ class EditorAccessibility extends React.Component {
|
||||||
</li>));
|
</li>));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
messages.push(<li key={0}> There are no lint messages </li>);
|
messages.push(<li key={0}>{this.props.t('EditorAccessibility.NoLintMessages')}</li>);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="editor-accessibility">
|
<div className="editor-accessibility">
|
||||||
<ul className="editor-lintmessages" title="lint messages">
|
<ul className="editor-lintmessages" title="lint messages">
|
||||||
{messages}
|
{messages}
|
||||||
</ul>
|
</ul>
|
||||||
<p> Current line
|
<p> {this.props.t('EditorAccessibility.CurrentLine')}
|
||||||
<span className="editor-linenumber" aria-live="polite" aria-atomic="true" id="current-line"> </span>
|
<span className="editor-linenumber" aria-live="polite" aria-atomic="true" id="current-line"> </span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,6 +40,7 @@ EditorAccessibility.propTypes = {
|
||||||
message: PropTypes.string.isRequired,
|
message: PropTypes.string.isRequired,
|
||||||
id: PropTypes.number.isRequired
|
id: PropTypes.number.isRequired
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditorAccessibility;
|
export default withTranslation()(EditorAccessibility);
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import GitHubLogo from '../../../images/github.svg';
|
import GitHubLogo from '../../../images/github.svg';
|
||||||
|
|
||||||
function Feedback(props) {
|
function Feedback(props) {
|
||||||
return (
|
return (
|
||||||
<div className="feedback__content">
|
<div className="feedback__content">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Feedback</title>
|
<title>{this.props.t('Feedback.Title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="feedback__content-pane">
|
<div className="feedback__content-pane">
|
||||||
<h2 className="feedback__content-pane-header">
|
<h2 className="feedback__content-pane-header">
|
||||||
|
@ -47,4 +48,4 @@ function Feedback(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Feedback;
|
export default withTranslation()(Feedback);
|
||||||
|
|
|
@ -67,7 +67,7 @@ FileName.propTypes = {
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FileNode extends React.Component {
|
class FileNode extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -419,4 +419,7 @@ const TranslatedFileNode = withTranslation()(FileNode);
|
||||||
|
|
||||||
const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(TranslatedFileNode);
|
const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(TranslatedFileNode);
|
||||||
|
|
||||||
export default ConnectedFileNode;
|
export {
|
||||||
|
TranslatedFileNode as FileNode,
|
||||||
|
ConnectedFileNode as default
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from '../../../test-utils';
|
||||||
import { FileNode } from './FileNode';
|
import { FileNode } from './FileNode';
|
||||||
|
|
||||||
describe('<FileNode />', () => {
|
describe('<FileNode />', () => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Icons from './Icons';
|
||||||
const Item = ({
|
const Item = ({
|
||||||
isAdded, onSelect, name, url, t
|
isAdded, onSelect, name, url, t
|
||||||
}) => {
|
}) => {
|
||||||
const buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection';
|
const buttonLabel = isAdded ? t('QuickAddList.ButtonRemoveARIA') : t('QuickAddList.ButtonAddToCollectionARIA');
|
||||||
return (
|
return (
|
||||||
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
|
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
|
||||||
<button className="quick-add__item-toggle" onClick={onSelect} aria-label={buttonLabel}>
|
<button className="quick-add__item-toggle" onClick={onSelect} aria-label={buttonLabel}>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import i18next from 'i18next';
|
||||||
import * as SortingActions from '../../actions/sorting';
|
import * as SortingActions from '../../actions/sorting';
|
||||||
|
|
||||||
import Searchbar from './Searchbar';
|
import Searchbar from './Searchbar';
|
||||||
|
|
||||||
|
|
||||||
const scope = 'collection';
|
const scope = 'collection';
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
searchLabel: 'Search collections...',
|
searchLabel: i18next.t('Searchbar.SearchCollection'),
|
||||||
searchTerm: state.search[`${scope}SearchTerm`],
|
searchTerm: state.search[`${scope}SearchTerm`],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import i18next from 'i18next';
|
||||||
import SearchIcon from '../../../../images/magnifyingglass.svg';
|
import SearchIcon from '../../../../images/magnifyingglass.svg';
|
||||||
|
|
||||||
|
|
||||||
class Searchbar extends React.Component {
|
class Searchbar extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -50,7 +52,7 @@ class Searchbar extends React.Component {
|
||||||
<button
|
<button
|
||||||
className="searchbar__clear-button"
|
className="searchbar__clear-button"
|
||||||
onClick={this.handleResetSearch}
|
onClick={this.handleResetSearch}
|
||||||
>clear
|
>{this.props.t('Searchbar.ClearTerm')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -62,10 +64,11 @@ Searchbar.propTypes = {
|
||||||
setSearchTerm: PropTypes.func.isRequired,
|
setSearchTerm: PropTypes.func.isRequired,
|
||||||
resetSearchTerm: PropTypes.func.isRequired,
|
resetSearchTerm: PropTypes.func.isRequired,
|
||||||
searchLabel: PropTypes.string,
|
searchLabel: PropTypes.string,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
Searchbar.defaultProps = {
|
Searchbar.defaultProps = {
|
||||||
searchLabel: 'Search sketches...',
|
searchLabel: i18next.t('Searchbar.SearchSketch')
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Searchbar;
|
export default withTranslation()(Searchbar);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import i18next from 'i18next';
|
||||||
import * as SortingActions from '../../actions/sorting';
|
import * as SortingActions from '../../actions/sorting';
|
||||||
|
|
||||||
import Searchbar from './Searchbar';
|
import Searchbar from './Searchbar';
|
||||||
|
@ -8,6 +9,7 @@ const scope = 'sketch';
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
|
searchLabel: i18next.t('Searchbar.SearchSketch'),
|
||||||
searchTerm: state.search[`${scope}SearchTerm`],
|
searchTerm: state.search[`${scope}SearchTerm`],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import differenceInMilliseconds from 'date-fns/difference_in_milliseconds';
|
||||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
class Timer extends React.Component {
|
class Timer extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -23,17 +24,19 @@ class Timer extends React.Component {
|
||||||
showSavedTime() {
|
showSavedTime() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
if (Math.abs(differenceInMilliseconds(now, this.props.projectSavedTime) < 10000)) {
|
if (Math.abs(differenceInMilliseconds(now, this.props.projectSavedTime) < 10000)) {
|
||||||
return 'Saved: just now';
|
return this.props.t('Timer.SavedJustNow');
|
||||||
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 20000) {
|
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 20000) {
|
||||||
return 'Saved: 15 seconds ago';
|
return this.props.t('Timer.Saved15Seconds');
|
||||||
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 30000) {
|
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 30000) {
|
||||||
return 'Saved: 25 seconds ago';
|
return this.props.t('Timer.Saved25Seconds');
|
||||||
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 46000) {
|
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 46000) {
|
||||||
return 'Saved: 35 seconds ago';
|
return this.props.t('Timer.Saved35Seconds');
|
||||||
}
|
}
|
||||||
return `Saved: ${distanceInWordsToNow(this.props.projectSavedTime, {
|
|
||||||
|
const timeAgo = distanceInWordsToNow(this.props.projectSavedTime, {
|
||||||
includeSeconds: true
|
includeSeconds: true
|
||||||
})} ago`;
|
});
|
||||||
|
return this.props.t('Timer.SavedAgo', { timeAgo });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -51,11 +54,12 @@ class Timer extends React.Component {
|
||||||
|
|
||||||
Timer.propTypes = {
|
Timer.propTypes = {
|
||||||
projectSavedTime: PropTypes.string.isRequired,
|
projectSavedTime: PropTypes.string.isRequired,
|
||||||
isUserOwner: PropTypes.bool
|
isUserOwner: PropTypes.bool,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
Timer.defaultProps = {
|
Timer.defaultProps = {
|
||||||
isUserOwner: false
|
isUserOwner: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Timer;
|
export default withTranslation()(Timer);
|
||||||
|
|
|
@ -86,7 +86,7 @@ class Toolbar extends React.Component {
|
||||||
this.props.setTextOutput(true);
|
this.props.setTextOutput(true);
|
||||||
this.props.setGridOutput(true);
|
this.props.setGridOutput(true);
|
||||||
}}
|
}}
|
||||||
aria-label="Play sketch"
|
aria-label={this.props.t('Toolbar.PlaySketchARIA')}
|
||||||
disabled={this.props.infiniteLoop}
|
disabled={this.props.infiniteLoop}
|
||||||
>
|
>
|
||||||
<PlayIcon focusable="false" aria-hidden="true" />
|
<PlayIcon focusable="false" aria-hidden="true" />
|
||||||
|
@ -94,7 +94,7 @@ class Toolbar extends React.Component {
|
||||||
<button
|
<button
|
||||||
className={playButtonClass}
|
className={playButtonClass}
|
||||||
onClick={this.props.startSketch}
|
onClick={this.props.startSketch}
|
||||||
aria-label="Play only visual sketch"
|
aria-label={this.props.t('Toolbar.PlayOnlyVisualSketchARIA')}
|
||||||
disabled={this.props.infiniteLoop}
|
disabled={this.props.infiniteLoop}
|
||||||
>
|
>
|
||||||
<PlayIcon focusable="false" aria-hidden="true" />
|
<PlayIcon focusable="false" aria-hidden="true" />
|
||||||
|
@ -102,7 +102,7 @@ class Toolbar extends React.Component {
|
||||||
<button
|
<button
|
||||||
className={stopButtonClass}
|
className={stopButtonClass}
|
||||||
onClick={this.props.stopSketch}
|
onClick={this.props.stopSketch}
|
||||||
aria-label="Stop sketch"
|
aria-label={this.props.t('Toolbar.StopSketchARIA')}
|
||||||
>
|
>
|
||||||
<StopIcon focusable="false" aria-hidden="true" />
|
<StopIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -129,7 +129,7 @@ class Toolbar extends React.Component {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!canEditProjectName}
|
disabled={!canEditProjectName}
|
||||||
aria-label="Edit sketch name"
|
aria-label={this.props.t('Toolbar.EditSketchARIA')}
|
||||||
>
|
>
|
||||||
<span>{this.props.project.name}</span>
|
<span>{this.props.project.name}</span>
|
||||||
{
|
{
|
||||||
|
@ -145,7 +145,7 @@ class Toolbar extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
maxLength="128"
|
maxLength="128"
|
||||||
className="toolbar__project-name-input"
|
className="toolbar__project-name-input"
|
||||||
aria-label="New sketch name"
|
aria-label={this.props.t('Toolbar.NewSketchNameARIA')}
|
||||||
value={this.state.projectNameInputValue}
|
value={this.state.projectNameInputValue}
|
||||||
onChange={this.handleProjectNameChange}
|
onChange={this.handleProjectNameChange}
|
||||||
ref={(element) => { this.projectNameInput = element; }}
|
ref={(element) => { this.projectNameInput = element; }}
|
||||||
|
@ -165,7 +165,7 @@ class Toolbar extends React.Component {
|
||||||
<button
|
<button
|
||||||
className={preferencesButtonClass}
|
className={preferencesButtonClass}
|
||||||
onClick={this.props.openPreferences}
|
onClick={this.props.openPreferences}
|
||||||
aria-label="Open Preferences"
|
aria-label={this.props.t('Toolbar.OpenPreferencesARIA')}
|
||||||
>
|
>
|
||||||
<PreferencesIcon focusable="false" aria-hidden="true" />
|
<PreferencesIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -200,6 +200,7 @@ Toolbar.propTypes = {
|
||||||
saveProject: PropTypes.func.isRequired,
|
saveProject: PropTypes.func.isRequired,
|
||||||
currentUser: PropTypes.string,
|
currentUser: PropTypes.string,
|
||||||
t: PropTypes.func.isRequired
|
t: PropTypes.func.isRequired
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Toolbar.defaultProps = {
|
Toolbar.defaultProps = {
|
||||||
|
@ -225,6 +226,5 @@ const mapDispatchToProps = {
|
||||||
...projectActions,
|
...projectActions,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolbarComponent = Toolbar;
|
export const ToolbarComponent = withTranslation()(Toolbar);
|
||||||
// export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);
|
export default connect(mapStateToProps, mapDispatchToProps)(ToolbarComponent);
|
||||||
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Toolbar));
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
import { fireEvent, render, screen, waitFor } from '../../../test-utils';
|
||||||
import { ToolbarComponent } from './Toolbar';
|
import { ToolbarComponent } from './Toolbar';
|
||||||
|
|
||||||
const renderComponent = (extraProps = {}) => {
|
const renderComponent = (extraProps = {}) => {
|
||||||
|
|
40
client/test-utils.js
Normal file
40
client/test-utils.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* This file re-exports @testing-library but ensures that
|
||||||
|
* any calls to render have translations available.
|
||||||
|
*
|
||||||
|
* This means tested components will be able to call
|
||||||
|
* `t()` and have the translations of the default
|
||||||
|
* language
|
||||||
|
*
|
||||||
|
* See: https://react.i18next.com/misc/testing#testing-without-stubbing
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import i18n from './i18n-test';
|
||||||
|
|
||||||
|
// re-export everything
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
export * from '@testing-library/react';
|
||||||
|
|
||||||
|
const Providers = ({ children }) => (
|
||||||
|
// eslint-disable-next-line react/jsx-filename-extension
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
{children}
|
||||||
|
</I18nextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
Providers.propTypes = {
|
||||||
|
children: PropTypes.element.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const customRender = (ui, options) =>
|
||||||
|
render(ui, { wrapper: Providers, ...options });
|
||||||
|
|
||||||
|
// override render method
|
||||||
|
export { customRender as render };
|
|
@ -38,6 +38,22 @@ export const hijackConsoleErrorsScript = (offs) => {
|
||||||
}], '*');
|
}], '*');
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
// catch rejected promises
|
||||||
|
window.onunhandledrejection = function (event) {
|
||||||
|
if (event.reason && event.reason.message && event.reason.stack){
|
||||||
|
var errorNum = event.reason.stack.split('about:srcdoc:')[1].split(':')[0];
|
||||||
|
var fileInfo = getScriptOff(errorNum);
|
||||||
|
var data = event.reason.message + ' (' + fileInfo[1] + ': line ' + fileInfo[0] + ')';
|
||||||
|
window.parent.postMessage([{
|
||||||
|
log: [{
|
||||||
|
method: 'error',
|
||||||
|
data: [data],
|
||||||
|
id: Date.now().toString()
|
||||||
|
}],
|
||||||
|
source: fileInfo[1]
|
||||||
|
}], '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
`;
|
`;
|
||||||
return s;
|
return s;
|
||||||
};
|
};
|
||||||
|
@ -46,7 +62,7 @@ export const startTag = '@fs-';
|
||||||
|
|
||||||
export const getAllScriptOffsets = (htmlFile) => {
|
export const getAllScriptOffsets = (htmlFile) => {
|
||||||
const offs = [];
|
const offs = [];
|
||||||
const hijackConsoleErrorsScriptLength = 36;
|
const hijackConsoleErrorsScriptLength = 52;
|
||||||
const embeddedJSStart = 'script crossorigin=""';
|
const embeddedJSStart = 'script crossorigin=""';
|
||||||
let foundJSScript = true;
|
let foundJSScript = true;
|
||||||
let foundEmbeddedJS = true;
|
let foundEmbeddedJS = true;
|
||||||
|
|
|
@ -341,6 +341,30 @@
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
"AssetList": {
|
||||||
|
"Title": "p5.js Web Editor | My assets",
|
||||||
|
"ToggleOpenCloseARIA": "Toggle Open/Close Asset Options",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"OpenNewTab": "Open in New Tab",
|
||||||
|
"NoUploadedAssets": "No uploaded assets.",
|
||||||
|
"HeaderName": "Name",
|
||||||
|
"HeaderSize": "Size",
|
||||||
|
"HeaderSketch": "Sketch"
|
||||||
|
},
|
||||||
|
"Feedback": {
|
||||||
|
"Title": "p5.js Web Editor | Feedback",
|
||||||
|
"ViaGithubHeader": "Via Github Issues",
|
||||||
|
"ViaGithubDescription": "If you're familiar with Github, this is our preferred method for receiving bug reports and feedback.",
|
||||||
|
"GoToGithub": "Go to Github",
|
||||||
|
"ViaGoogleHeader": "Via Google Form",
|
||||||
|
"ViaGoogleDescription": "You can also submit this quick form.",
|
||||||
|
"GoToForm": "Go to Form"
|
||||||
|
},
|
||||||
|
"Searchbar": {
|
||||||
|
"SearchSketch": "Search sketches...",
|
||||||
|
"SearchCollection": "Search collections...",
|
||||||
|
"ClearTerm": "clear"
|
||||||
|
},
|
||||||
"UploadFileModal": {
|
"UploadFileModal": {
|
||||||
"Title": "Upload File",
|
"Title": "Upload File",
|
||||||
"CloseButtonARIA": "Close upload file modal",
|
"CloseButtonARIA": "Close upload file modal",
|
||||||
|
@ -444,8 +468,8 @@
|
||||||
"AriaLabel": "Close {{title}} overlay"
|
"AriaLabel": "Close {{title}} overlay"
|
||||||
},
|
},
|
||||||
"QuickAddList":{
|
"QuickAddList":{
|
||||||
"ButtonLabelRemove": "Remove from collection",
|
"ButtonRemoveARIA": "Remove from collection",
|
||||||
"ButtonLabelAddToCollection": "Add to collection",
|
"ButtonAddToCollectionARIA": "Add to collection",
|
||||||
"View": "View"
|
"View": "View"
|
||||||
},
|
},
|
||||||
"SketchList": {
|
"SketchList": {
|
||||||
|
@ -474,5 +498,38 @@
|
||||||
"Title": "p5.js Web Editor | My sketches",
|
"Title": "p5.js Web Editor | My sketches",
|
||||||
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s sketches",
|
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s sketches",
|
||||||
"NoCollections": "No collections."
|
"NoCollections": "No collections."
|
||||||
|
},
|
||||||
|
"Editor": {
|
||||||
|
"OpenSketchARIA": "Open Sketch files navigation",
|
||||||
|
"CloseSketchARIA": "Close Sketch files navigation",
|
||||||
|
"UnsavedChangesARIA": "Sketch has unsaved changes",
|
||||||
|
"KeyUpLineNumber": "line {{lineNumber}}"
|
||||||
|
},
|
||||||
|
"EditorAccessibility": {
|
||||||
|
"NoLintMessages": "There are no lint messages ",
|
||||||
|
"CurrentLine": " Current line"
|
||||||
|
},
|
||||||
|
"Timer": {
|
||||||
|
"SavedJustNow": "Saved: just now",
|
||||||
|
"Saved15Seconds": "Saved: 15 seconds ago",
|
||||||
|
"Saved25Seconds": "Saved: 25 seconds ago",
|
||||||
|
"Saved35Seconds": "Saved: 35 seconds ago",
|
||||||
|
"SavedAgo": "Saved: {{timeAgo}} ago"
|
||||||
|
},
|
||||||
|
"AddRemoveButton": {
|
||||||
|
"AltAddARIA": "Add to collection",
|
||||||
|
"AltRemoveARIA": "Remove from collection"
|
||||||
|
},
|
||||||
|
"CopyableInput": {
|
||||||
|
"CopiedARIA": "Copied to Clipboard!",
|
||||||
|
"OpenViewTabARIA": "Open {{label}} view in new tab"
|
||||||
|
},
|
||||||
|
"EditableInput": {
|
||||||
|
"EditValue": "Edit {{display}} value",
|
||||||
|
"EmptyPlaceholder": "No value"
|
||||||
|
},
|
||||||
|
"PreviewNav": {
|
||||||
|
"EditSketchARIA": "Edit Sketch",
|
||||||
|
"ByUser": "by"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -341,6 +341,30 @@
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
"AssetList": {
|
||||||
|
"Title": "Editor Web p5.js | Mis assets",
|
||||||
|
"ToggleOpenCloseARIA": "Alternar Abrir/Cerrar Opciones de Asset",
|
||||||
|
"Delete": "Borrar",
|
||||||
|
"OpenNewTab": "Abrir en Nueva Pestaña",
|
||||||
|
"NoUploadedAssets": "No has subido archivos.",
|
||||||
|
"HeaderName": "Nombre",
|
||||||
|
"HeaderSize": "Tamaño",
|
||||||
|
"HeaderSketch": "Bosquejo"
|
||||||
|
},
|
||||||
|
"Feedback": {
|
||||||
|
"Title": "Editor Web p5.js | Retroalimentación",
|
||||||
|
"ViaGithubHeader": "Vía Github Issues",
|
||||||
|
"ViaGithubDescription": " Si estas familiarizado con Github, este es nuestro método favorito para recibir reporte de errores y retroalimentación.",
|
||||||
|
"GoToGithub": "Ir a Github",
|
||||||
|
"ViaGoogleHeader": "Vía Formulario de Google",
|
||||||
|
"ViaGoogleDescription": "También puedes enviar tu retroalimetnación usando esta vía rapida.",
|
||||||
|
"GoToForm": "Ir a Formulario"
|
||||||
|
},
|
||||||
|
"Searchbar": {
|
||||||
|
"SearchSketch": "Buscar en bosquejos...",
|
||||||
|
"SearchCollection": "Buscar en colecciones...",
|
||||||
|
"ClearTerm": "limpiar"
|
||||||
|
},
|
||||||
"UploadFileModal": {
|
"UploadFileModal": {
|
||||||
"Title": "Subir Archivo",
|
"Title": "Subir Archivo",
|
||||||
"CloseButtonARIA": "Cerrar diálogo para subir archivo",
|
"CloseButtonARIA": "Cerrar diálogo para subir archivo",
|
||||||
|
@ -444,8 +468,8 @@
|
||||||
"AriaLabel": "Cerrar la capa {{title}}"
|
"AriaLabel": "Cerrar la capa {{title}}"
|
||||||
},
|
},
|
||||||
"QuickAddList":{
|
"QuickAddList":{
|
||||||
"ButtonLabelRemove": "Remove from collection",
|
"ButtonRemoveARIA": "Remover de colección",
|
||||||
"ButtonLabelAddToCollection": "Add to collection",
|
"ButtonAddToCollectionARIA": "Agregar a colección",
|
||||||
"View": "Ver"
|
"View": "Ver"
|
||||||
},
|
},
|
||||||
"SketchList": {
|
"SketchList": {
|
||||||
|
@ -474,5 +498,38 @@
|
||||||
"Title": "p5.js Web Editor | Mis bosquejos",
|
"Title": "p5.js Web Editor | Mis bosquejos",
|
||||||
"AnothersTitle": "Editor Web p5.js | Bosquejos de {{anotheruser}}",
|
"AnothersTitle": "Editor Web p5.js | Bosquejos de {{anotheruser}}",
|
||||||
"NoCollections": "No hay colecciones."
|
"NoCollections": "No hay colecciones."
|
||||||
|
},
|
||||||
|
"Editor": {
|
||||||
|
"OpenSketchARIA": "Abrir navegación de archivos de bosquejo",
|
||||||
|
"CloseSketchARIA": "Cerrar navegación de archivos de bosquejo",
|
||||||
|
"UnsavedChangesARIA": "El bosquejo tiene cambios sin guardar",
|
||||||
|
"KeyUpLineNumber": "línea {{lineNumber}}"
|
||||||
|
},
|
||||||
|
"EditorAccessibility": {
|
||||||
|
"NoLintMessages": "No hay mensajes de Lint",
|
||||||
|
"CurrentLine": " Línea actual"
|
||||||
|
},
|
||||||
|
"Timer": {
|
||||||
|
"SavedJustNow": "Guardado: justo ahora",
|
||||||
|
"Saved15Seconds": "Guardado: hace 15 segundos",
|
||||||
|
"Saved25Seconds": "Guardado: hace 25 segundos",
|
||||||
|
"Saved35Seconds": "Guardado: hace 35 segundos",
|
||||||
|
"SavedAgo": "Guardado: hace {{timeAgo}}"
|
||||||
|
},
|
||||||
|
"AddRemoveButton": {
|
||||||
|
"AltAddARIA": "Agregar a colección",
|
||||||
|
"AltRemoveARIA": "Remover de colección"
|
||||||
|
},
|
||||||
|
"CopyableInput": {
|
||||||
|
"CopiedARIA": "¡Copiado en el portapapeles!",
|
||||||
|
"OpenViewTabARIA": "Abrir la vista {{label}} en nueva pestaña"
|
||||||
|
},
|
||||||
|
"EditableInput": {
|
||||||
|
"EditValue": "Editar valor de {{display}}",
|
||||||
|
"EmptyPlaceholder": "Sin valor"
|
||||||
|
},
|
||||||
|
"PreviewNav": {
|
||||||
|
"EditSketchARIA": "Editar Bosquejo",
|
||||||
|
"ByUser": "por"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue