diff --git a/client/jest.setup.js b/client/jest.setup.js index 79652c74..233c76db 100644 --- a/client/jest.setup.js +++ b/client/jest.setup.js @@ -3,3 +3,21 @@ import '@babel/polyfill'; // See: https://github.com/testing-library/jest-dom // eslint-disable-next-line import/no-extraneous-dependencies import '@testing-library/jest-dom'; + +import lodash from 'lodash'; + +// For testing, we use en-US and provide a mock implementation +// of t() that finds the correct translation +import translations from '../translations/locales/en-US/translations.json'; + +// This function name needs to be prefixed with "mock" so that Jest doesn't +// complain that it's out-of-scope in the mock below +const mockTranslate = key => lodash.get(translations, key); + +jest.mock('react-i18next', () => ({ + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => (Component) => { + Component.defaultProps = { ...Component.defaultProps, t: mockTranslate }; + return Component; + }, +})); diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index ddb6d55e..eaee9492 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -1,4 +1,6 @@ import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; import { bindActionCreators } from 'redux'; @@ -72,7 +74,7 @@ const getConsoleFeedStyle = (theme, times, fontSize) => { } }; -const Console = () => { +const Console = ({ t }) => { const consoleEvents = useSelector(state => state.console); const isExpanded = useSelector(state => state.ide.consoleIsExpanded); const { theme, fontSize } = useSelector(state => state.preferences); @@ -98,19 +100,19 @@ const Console = () => { return (
-

Console

+

{t('Console.Title')}

- -
@@ -140,5 +142,9 @@ const Console = () => { ); }; +Console.propTypes = { + t: PropTypes.func.isRequired, +}; -export default Console; + +export default withTranslation()(Console); diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index d0a10ef8..0533091a 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -3,6 +3,8 @@ import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import classNames from 'classnames'; +import { withTranslation } from 'react-i18next'; + import * as IDEActions from '../actions/ide'; import * as FileActions from '../actions/files'; import DownArrowIcon from '../../../images/down-filled-triangle.svg'; @@ -152,7 +154,9 @@ export class FileNode extends React.Component { } handleClickDelete = () => { - if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) { + const prompt = this.props.t('Common.DeleteConfirmation', { name: this.props.name }); + + if (window.confirm(prompt)) { this.setState({ isDeleting: true }); this.props.resetSelectedFile(this.props.id); setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100); @@ -237,6 +241,8 @@ export class FileNode extends React.Component { const isFolder = this.props.fileType === 'folder'; const isRoot = this.props.name === 'root'; + const { t } = this.props; + return (
{ !isRoot && @@ -252,14 +258,14 @@ export class FileNode extends React.Component { @@ -286,7 +292,7 @@ export class FileNode extends React.Component { />
  • { this.props.authenticated &&
  • } @@ -342,7 +348,7 @@ export class FileNode extends React.Component { onFocus={this.onFocusComponent} className="sidebar__file-item-option" > - Rename + {t('FileNode.Rename')}
  • @@ -352,7 +358,7 @@ export class FileNode extends React.Component { onFocus={this.onFocusComponent} className="sidebar__file-item-option" > - Delete + {t('FileNode.Delete')}
  • @@ -388,6 +394,7 @@ FileNode.propTypes = { canEdit: PropTypes.bool.isRequired, openUploadFileModal: PropTypes.func.isRequired, authenticated: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, onClickFile: PropTypes.func }; @@ -408,5 +415,8 @@ function mapDispatchToProps(dispatch) { return bindActionCreators(Object.assign(FileActions, IDEActions), dispatch); } -const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(FileNode); +const TranslatedFileNode = withTranslation()(FileNode); + +const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(TranslatedFileNode); + export default ConnectedFileNode; diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 97c8c0ec..5d480278 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -1,6 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; +import { withTranslation } from 'react-i18next'; + import ConnectedFileNode from './FileNode'; import DownArrowIcon from '../../../images/down-filled-triangle.svg'; @@ -71,11 +73,11 @@ class Sidebar extends React.Component {

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

  • { this.props.user.authenticated &&
  • } @@ -159,11 +161,12 @@ Sidebar.propTypes = { user: PropTypes.shape({ id: PropTypes.string, authenticated: PropTypes.bool.isRequired - }).isRequired + }).isRequired, + t: PropTypes.func.isRequired, }; Sidebar.defaultProps = { owner: undefined }; -export default Sidebar; +export default withTranslation()(Sidebar); diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index 9ff6b536..39c5dcfb 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -174,15 +174,33 @@ } }, "Sidebar": { - "Create": "Create", - "EnterName": "enter a name", - "Add": "Add", - "Folder": "Folder" + "Title": "Sketch Files", + "ToggleARIA": "Toggle open/close sketch file options", + "AddFolder": "Create 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": { "Error": "Error", "Save": "Save", - "p5logoARIA": "p5.js Logo" + "p5logoARIA": "p5.js Logo", + "DeleteConfirmation": "Are you sure you want to delete {{name}}?" }, "IDEView": { "SubmitFeedback": "Submit Feedback"