diff --git a/.tx/config b/.tx/config index de8d6bb3d68424fb997ea2fb0ef3947464d94960..9e6a74a684eeb0ead249ab72b4126cf4674c6886 100644 --- a/.tx/config +++ b/.tx/config @@ -1,7 +1,7 @@ [main] host = https://www.transifex.com -[experimental-scratch.scratch-gui] +[scratch-editor.interface] file_filter = translations/<lang>.json source_file = translations/en.json source_lang = en diff --git a/src/components/controls/controls.jsx b/src/components/controls/controls.jsx index 8fec705b08ac9ad19a3d5a1669fded868b4b8a3a..84d7dbcfa49148e43f7470d7cc4800f261905e95 100644 --- a/src/components/controls/controls.jsx +++ b/src/components/controls/controls.jsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; import GreenFlag from '../green-flag/green-flag.jsx'; import StopAll from '../stop-all/stop-all.jsx'; @@ -8,10 +9,24 @@ import TurboMode from '../turbo-mode/turbo-mode.jsx'; import styles from './controls.css'; +const messages = defineMessages({ + goTitle: { + id: 'gui.controls.go', + defaultMessage: 'Go', + description: 'Green flag button title' + }, + stopTitle: { + id: 'gui.controls.stop', + defaultMessage: 'Stop', + description: 'Stop button title' + } +}); + const Controls = function (props) { const { active, className, + intl, onGreenFlagClick, onStopAllClick, turbo, @@ -24,10 +39,12 @@ const Controls = function (props) { > <GreenFlag active={active} + title={intl.formatMessage(messages.goTitle)} onClick={onGreenFlagClick} /> <StopAll active={active} + title={intl.formatMessage(messages.stopTitle)} onClick={onStopAllClick} /> {turbo ? ( @@ -40,6 +57,7 @@ const Controls = function (props) { Controls.propTypes = { active: PropTypes.bool, className: PropTypes.string, + intl: intlShape.isRequired, onGreenFlagClick: PropTypes.func.isRequired, onStopAllClick: PropTypes.func.isRequired, turbo: PropTypes.bool @@ -50,4 +68,4 @@ Controls.defaultProps = { turbo: false }; -export default Controls; +export default injectIntl(Controls); diff --git a/src/components/crash-message/crash-message.jsx b/src/components/crash-message/crash-message.jsx index d5fc45d163072d6394c5da665ee6427b6d0159a4..8b749c7933842bd8d7d2aaed9b3bc49174f6ef47 100644 --- a/src/components/crash-message/crash-message.jsx +++ b/src/components/crash-message/crash-message.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; +import {FormattedMessage} from 'react-intl'; import styles from './crash-message.css'; import reloadIcon from './reload.svg'; @@ -13,13 +14,20 @@ const CrashMessage = props => ( src={reloadIcon} /> <h2> - Oops! Something went wrong. + <FormattedMessage + defaultMessage="Oops! Something went wrong." + description="Unhandled error title" + id="gui.crashMessage.title" + /> </h2> <p> - We are so sorry, but it looks like Scratch has crashed. This bug has been - automatically reported to the Scratch Team. Please refresh your page to try - again. - + { /* eslint-disable max-len */ } + <FormattedMessage + defaultMessage="We are so sorry, but it looks like Scratch has crashed. This bug has been automatically reported to the Scratch Team. Please refresh your page to try again." + description="Unhandled error description" + id="gui.crashMessage.description" + /> + { /* eslint-enable max-len */ } </p> <button className={styles.reloadButton} diff --git a/src/components/forms/label.jsx b/src/components/forms/label.jsx index 62f077a746fb5bf6d70c828fa7f34097f89054ac..e005844587e9b62774e63c9e713bde0c154b8c40 100644 --- a/src/components/forms/label.jsx +++ b/src/components/forms/label.jsx @@ -15,7 +15,7 @@ const Label = props => ( Label.propTypes = { children: PropTypes.node, secondary: PropTypes.bool, - text: PropTypes.string.isRequired + text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired }; Label.defaultProps = { diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 55303854055695efdc8371989815b1b87423ac3f..41ed6dffcc7c70bbf0ea3b66cad9ab358d78ccfb 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; import LibraryItem from '../library-item/library-item.jsx'; import Modal from '../../containers/modal.jsx'; @@ -14,6 +15,14 @@ import styles from './library.css'; const ALL_TAG_TITLE = 'All'; const tagListPrefix = [{title: ALL_TAG_TITLE}]; +const messages = defineMessages({ + filterPlaceholder: { + id: 'gui.library.filterPlaceholder', + defaultMessage: 'Search', + description: 'Placeholder text for library search field' + } +}); + class LibraryComponent extends React.Component { constructor (props) { super(props); @@ -101,6 +110,7 @@ class LibraryComponent extends React.Component { )} filterQuery={this.state.filterQuery} inputClassName={styles.filterInput} + placeholderText={this.props.intl.formatMessage(messages.filterPlaceholder)} onChange={this.handleFilterChange} onClear={this.handleFilterClear} /> @@ -176,6 +186,7 @@ LibraryComponent.propTypes = { ), filterable: PropTypes.bool, id: PropTypes.string.isRequired, + intl: intlShape.isRequired, onItemMouseEnter: PropTypes.func, onItemMouseLeave: PropTypes.func, onItemSelected: PropTypes.func, @@ -188,4 +199,4 @@ LibraryComponent.defaultProps = { filterable: true }; -export default LibraryComponent; +export default injectIntl(LibraryComponent); diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index 0683b9bba778394ed82b91f8c2dceeb321aa931f..dff3ce8537b748e0c5c554d25652b8f30615efdc 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import ReactModal from 'react-modal'; +import {FormattedMessage} from 'react-intl'; import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; @@ -46,7 +47,11 @@ const ModalComponent = props => ( iconSrc={backIcon} onClick={props.onRequestClose} > - Back + <FormattedMessage + defaultMessage="Back" + description="Back button in modal" + id="gui.modal.back" + /> </Button> ) : ( <CloseButton diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx index 72a9174f4a7d88f60338bc80c09fb97d7f177ec0..c233a3277794fb8decf0730201e209b0b1ce45d0 100644 --- a/src/components/record-modal/playback-step.jsx +++ b/src/components/record-modal/playback-step.jsx @@ -4,12 +4,36 @@ import Box from '../box/box.jsx'; import Waveform from '../waveform/waveform.jsx'; import Meter from '../meter/meter.jsx'; import AudioTrimmer from '../../containers/audio-trimmer.jsx'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; import styles from './record-modal.css'; import backIcon from './icon--back.svg'; import stopIcon from './icon--stop-playback.svg'; import playIcon from './icon--play.svg'; +const messages = defineMessages({ + stopMsg: { + defaultMessage: 'Stop', + description: 'Stop/Play button in recording playback', + id: 'gui.playbackStep.stopMsg' + }, + playMsg: { + defaultMessage: 'Play', + description: 'Stop/Play button in recording playback', + id: 'gui.playbackStep.playMsg' + }, + loadingMsg: { + defaultMessage: 'Loading...', + description: 'Loading/Save button in recording playback', + id: 'gui.playbackStep.loadingMsg' + }, + saveMsg: { + defaultMessage: 'Save', + description: 'Loading/Save button in recording playback', + id: 'gui.playbackStep.saveMsg' + } +}); + const PlaybackStep = props => ( <Box> <Box className={styles.visualizationContainer}> @@ -48,7 +72,10 @@ const PlaybackStep = props => ( /> <div className={styles.helpText}> <span className={styles.playingText}> - {props.playing ? 'Stop' : 'Play'} + {props.playing ? + props.intl.formatMessage(messages.stopMsg) : + props.intl.formatMessage(messages.playMsg) + } </span> </div> </button> @@ -68,7 +95,10 @@ const PlaybackStep = props => ( disabled={props.encoding} onClick={props.onSubmit} > - {props.encoding ? 'Loading...' : 'Save'} + {props.encoding ? + props.intl.formatMessage(messages.loadingMsg) : + props.intl.formatMessage(messages.saveMsg) + } </button> </Box> </Box> @@ -76,6 +106,7 @@ const PlaybackStep = props => ( PlaybackStep.propTypes = { encoding: PropTypes.bool.isRequired, + intl: intlShape.isRequired, levels: PropTypes.arrayOf(PropTypes.number).isRequired, onBack: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, @@ -89,4 +120,4 @@ PlaybackStep.propTypes = { trimStart: PropTypes.number.isRequired }; -export default PlaybackStep; +export default injectIntl(PlaybackStep); diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx index 675d3e887c20c24ff0babaf611b8b5298394993f..2adc9c14774b9fcf8992c78a53a531c26328429d 100644 --- a/src/components/record-modal/record-modal.jsx +++ b/src/components/record-modal/record-modal.jsx @@ -1,15 +1,24 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; import RecordingStep from '../../containers/recording-step.jsx'; import PlaybackStep from '../../containers/playback-step.jsx'; import Modal from '../modal/modal.jsx'; import styles from './record-modal.css'; +const messages = defineMessages({ + title: { + defaultMessage: 'Record Sound', + description: 'Recording modal title', + id: 'gui.recordModal.title' + } +}); + const RecordModal = props => ( <Modal className={styles.modalContent} - contentLabel={'Record Sound'} + contentLabel={props.intl.formatMessage(messages.title)} onRequestClose={props.onCancel} > <Box className={styles.body}> @@ -44,6 +53,7 @@ const RecordModal = props => ( RecordModal.propTypes = { encoding: PropTypes.bool.isRequired, + intl: intlShape.isRequired, levels: PropTypes.arrayOf(PropTypes.number), onBack: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, @@ -64,4 +74,4 @@ RecordModal.propTypes = { trimStart: PropTypes.number.isRequired }; -export default RecordModal; +export default injectIntl(RecordModal); diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx index 7d0acf952b9dc3ed8320dcd835deb090c1a5df62..76c1548df341ffabffd9b539d1dd4e67ee84d73f 100644 --- a/src/components/sprite-info/sprite-info.jsx +++ b/src/components/sprite-info/sprite-info.jsx @@ -7,6 +7,7 @@ import Box from '../box/box.jsx'; import Label from '../forms/label.jsx'; import Input from '../forms/input.jsx'; import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import {FormattedMessage} from 'react-intl'; import layout from '../../lib/layout-constants.js'; import styles from './sprite-info.css'; @@ -31,17 +32,52 @@ class SpriteInfo extends React.Component { ); } render () { + const sprite = ( + <FormattedMessage + defaultMessage="Sprite" + description="Sprite info label" + id="gui.SpriteInfo.sprite" + /> + ); + const spritePlaceholder = ( + <FormattedMessage + defaultMessage="Name" + description="Placeholder text for sprite name" + id="gui.SpriteInfo.spritePlaceholder" + /> + ); + const showLabel = ( + <FormattedMessage + defaultMessage="Show" + description="Sprite info show label" + id="gui.SpriteInfo.show" + /> + ); + const sizeLabel = ( + <FormattedMessage + defaultMessage="Size" + description="Sprite info size label" + id="gui.SpriteInfo.size" + /> + ); + const directionLabel = ( + <FormattedMessage + defaultMessage="Direction" + description="Sprite info direction label" + id="gui.SpriteInfo.direction" + /> + ); return ( <Box className={styles.spriteInfo} > <div className={classNames(styles.row, styles.rowPrimary)}> <div className={styles.group}> - <Label text="Sprite"> + <Label text={sprite}> <BufferedInput className={styles.spriteInput} disabled={this.props.disabled} - placeholder="Name" + placeholder={spritePlaceholder} tabIndex="0" type="text" value={this.props.disabled ? '' : this.props.name} @@ -102,7 +138,7 @@ class SpriteInfo extends React.Component { <MediaQuery minWidth={layout.fullSizeMinWidth}> <Label secondary - text="Show" + text={showLabel} /> </MediaQuery> <div> @@ -149,12 +185,12 @@ class SpriteInfo extends React.Component { <div className={classNames(styles.group, styles.largerInput)}> <Label secondary - text="Size" + text={sizeLabel} > <BufferedInput small disabled={this.props.disabled} - label="Size" + label={sizeLabel} tabIndex="0" type="text" value={this.props.disabled ? '' : this.props.size} @@ -165,12 +201,12 @@ class SpriteInfo extends React.Component { <div className={classNames(styles.group, styles.largerInput)}> <Label secondary - text="Direction" + text={directionLabel} > <BufferedInput small disabled={this.props.disabled} - label="Direction" + label={directionLabel} tabIndex="0" type="text" value={this.props.disabled ? '' : this.props.direction} diff --git a/src/components/stage-header/stage-header.jsx b/src/components/stage-header/stage-header.jsx index cb6e1fdaa76ba3a0b901e2350f66f58bb896a5cc..7a2beec7cfdf849175a762be24146737211389d8 100644 --- a/src/components/stage-header/stage-header.jsx +++ b/src/components/stage-header/stage-header.jsx @@ -19,24 +19,29 @@ import styles from './stage-header.css'; const messages = defineMessages({ largeStageSizeMessage: { - defaultMessage: 'Stage Size Toggle - Large', + defaultMessage: 'Switch to large stage', description: 'Button to change stage size to large', - id: 'gui.gui.stageSizeLarge' + id: 'gui.stageHeader.stageSizeLarge' }, smallStageSizeMessage: { - defaultMessage: 'Stage Size Toggle - Small', + defaultMessage: 'Switch to small stage', description: 'Button to change stage size to small', - id: 'gui.gui.stageSizeSmall' + id: 'gui.stageHeader.stageSizeSmall' }, fullStageSizeMessage: { - defaultMessage: 'Stage Size Toggle - Full Screen', + defaultMessage: 'Enter full screen mode', description: 'Button to change stage size to full screen', - id: 'gui.gui.stageSizeFull' + id: 'gui.stageHeader.stageSizeFull' }, unFullStageSizeMessage: { - defaultMessage: 'Stage Size Toggle - Un-full screen', + defaultMessage: 'Exit full screen mode', description: 'Button to get out of full screen mode', - id: 'gui.gui.stageSizeUnFull' + id: 'gui.stageHeader.stageSizeUnFull' + }, + fullscreenControl: { + defaultMessage: 'Full Screen Control', + description: 'Button to enter/exit full screen mode', + id: 'gui.stageHeader.fullscreenControl' } }); @@ -71,7 +76,7 @@ const StageHeaderComponent = function (props) { className={styles.stageButtonIcon} draggable={false} src={unFullScreenIcon} - title="Full Screen Control" + title={props.intl.formatMessage(messages.fullscreenControl)} /> </Button> </Box> @@ -133,7 +138,7 @@ const StageHeaderComponent = function (props) { className={styles.stageButtonIcon} draggable={false} src={fullScreenIcon} - title="Full Screen Control" + title={props.intl.formatMessage(messages.fullscreenControl)} /> </Button> </div> diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index d30daf98a908210e2db25d9a5a8984619c3ff9dd..24127dca3fd7ecaa237bb041a1c1b03e479aba66 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -2,6 +2,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; import extensionLibraryContent from '../lib/libraries/extensions/index'; @@ -9,6 +10,19 @@ import analytics from '../lib/analytics'; import LibraryComponent from '../components/library/library.jsx'; import extensionIcon from '../components/action-menu/icon--sprite.svg'; +const messages = defineMessages({ + extensionTitle: { + defaultMessage: 'Choose an Extension', + description: 'Heading for the extension library', + id: 'gui.extensionLibrary.chooseAnExtension' + }, + extensionUrl: { + defaultMessage: 'Enter the URL of the extension', + description: 'Prompt for unoffical extension url', + id: 'gui.extensionLibrary.extensionUrl' + } +}); + class ExtensionLibrary extends React.PureComponent { constructor (props) { super(props); @@ -20,7 +34,7 @@ class ExtensionLibrary extends React.PureComponent { let url = item.extensionURL; if (!item.disabled && !item.extensionURL) { // eslint-disable-next-line no-alert - url = prompt('Enter the URL of the extension'); + url = prompt(this.props.intl.formatMessage(messages.extensionUrl)); } if (url && !item.disabled) { if (this.props.vm.extensionManager.isExtensionLoaded(url)) { @@ -47,7 +61,7 @@ class ExtensionLibrary extends React.PureComponent { data={extensionLibraryThumbnailData} filterable={false} id="extensionLibrary" - title="Choose an Extension" + title={this.props.intl.formatMessage(messages.extensionTitle)} visible={this.props.visible} onItemSelected={this.handleItemSelect} onRequestClose={this.props.onRequestClose} @@ -57,10 +71,11 @@ class ExtensionLibrary extends React.PureComponent { } ExtensionLibrary.propTypes = { + intl: intlShape.isRequired, onCategorySelected: PropTypes.func, onRequestClose: PropTypes.func, visible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types }; -export default ExtensionLibrary; +export default injectIntl(ExtensionLibrary); diff --git a/src/containers/recording-step.jsx b/src/containers/recording-step.jsx index 4e82e6ec8b3d1cf3683016795486172063632f1f..a1540527890d03aeeb8378476915c0f26a38327e 100644 --- a/src/containers/recording-step.jsx +++ b/src/containers/recording-step.jsx @@ -3,6 +3,15 @@ import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; import RecordingStepComponent from '../components/record-modal/recording-step.jsx'; import AudioRecorder from '../lib/audio/audio-recorder.js'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; + +const messages = defineMessages({ + alertMsg: { + defaultMessage: 'Could not start recording', + description: 'Alert for recording error', + id: 'gui.recordingStep.alertMsg' + } +}); class RecordingStep extends React.Component { constructor (props) { @@ -32,7 +41,7 @@ class RecordingStep extends React.Component { this.setState({listening: true}); } handleRecordingError () { - alert('Could not start recording'); // eslint-disable-line no-alert + alert(this.props.intl.formatMessage(messages.extensionUrl)); // eslint-disable-line no-alert } handleLevelUpdate (level) { this.setState({level}); @@ -68,9 +77,10 @@ class RecordingStep extends React.Component { } RecordingStep.propTypes = { + intl: intlShape.isRequired, onRecord: PropTypes.func.isRequired, onStopRecording: PropTypes.func.isRequired, recording: PropTypes.bool }; -export default RecordingStep; +export default injectIntl(RecordingStep); diff --git a/test/unit/components/controls.test.jsx b/test/unit/components/controls.test.jsx index fcf22208d410c9815af9705b0941b40883fa8673..78abb474cdd58612ddc8d643a4b8aa50598aaaf6 100644 --- a/test/unit/components/controls.test.jsx +++ b/test/unit/components/controls.test.jsx @@ -1,20 +1,20 @@ import React from 'react'; -import {shallowWithIntl} from '../../helpers/intl-helpers.jsx'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; import Controls from '../../../src/components/controls/controls'; import TurboMode from '../../../src/components/turbo-mode/turbo-mode'; +import GreenFlag from '../../../src/components/green-flag/green-flag'; +import StopAll from '../../../src/components/stop-all/stop-all'; describe('Controls component', () => { const defaultProps = () => ({ active: false, - greenFlagTitle: 'Go', onGreenFlagClick: jest.fn(), onStopAllClick: jest.fn(), - stopAllTitle: 'Stop', turbo: false }); test('shows turbo mode when in turbo mode', () => { - const component = shallowWithIntl( + const component = mountWithIntl( <Controls {...defaultProps()} /> @@ -26,15 +26,15 @@ describe('Controls component', () => { test('triggers the right callbacks when clicked', () => { const props = defaultProps(); - const component = shallowWithIntl( + const component = mountWithIntl( <Controls {...props} /> ); - component.find('[title="Go"]').simulate('click'); + component.find(GreenFlag).simulate('click'); expect(props.onGreenFlagClick).toHaveBeenCalled(); - component.find('[title="Stop"]').simulate('click'); + component.find(StopAll).simulate('click'); expect(props.onStopAllClick).toHaveBeenCalled(); }); });