Skip to content

Commit

Permalink
Merge pull request #863 from cboard-org/feature/tts-engine-select
Browse files Browse the repository at this point in the history
Support for TTS engine selection in Cordova
  • Loading branch information
martinbedouret authored Apr 2, 2021
2 parents e9859bb + fc55f8a commit d6f9eab
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 70 deletions.
130 changes: 126 additions & 4 deletions src/components/Settings/Language/Language.component.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import ISO6391 from 'iso-639-1';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Paper from '@material-ui/core/Paper';
import CheckIcon from '@material-ui/icons/Check';
import WarningIcon from '@material-ui/icons/Warning';
import { Button } from '@material-ui/core';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContentText from '@material-ui/core/DialogContentText';
import Slide from '@material-ui/core/Slide';
import Select from '@material-ui/core/Select';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import MenuItem from '@material-ui/core/MenuItem';
import Divider from '@material-ui/core/Divider';
import CircularProgress from '@material-ui/core/CircularProgress';
import ReactMarkdown from 'react-markdown';

import FullScreenDialog from '../../UI/FullScreenDialog';
Expand All @@ -28,6 +36,7 @@ const Transition = React.forwardRef(function Transition(props, ref) {

class Language extends React.Component {
static propTypes = {
intl: intlShape.isRequired,
/**
* Languages to display
*/
Expand All @@ -48,7 +57,16 @@ class Language extends React.Component {
* Callback fired when submitting selected language
*/
onSubmitLang: PropTypes.func.isRequired,
language: PropTypes.object.isRequired
language: PropTypes.object.isRequired,
/**
* TTS engines list
*/
ttsEngines: PropTypes.arrayOf(PropTypes.object),
/**
* TTS default engine
*/
ttsEngine: PropTypes.object,
setTtsEngine: PropTypes.func.isRequired
};

static defaultProps = {
Expand All @@ -61,6 +79,9 @@ class Language extends React.Component {

this.state = {
moreLangDialog: false,
loading: false,
ttsEngine: props.ttsEngine.name,
openTtsEngineError: false,
markdown: ''
};
}
Expand Down Expand Up @@ -94,6 +115,32 @@ class Language extends React.Component {
}
}

componentDidUpdate(prevProps) {
if (this.props.ttsEngine.name !== prevProps.ttsEngine.name) {
this.setState({
ttsEngine: this.props.ttsEngine.name,
loading: false
});
}
}

async handleTtsEngineChange(event) {
const { setTtsEngine } = this.props;
this.setState({
ttsEngine: event.target.value,
loading: true
});
try {
await setTtsEngine(event.target.value);
} catch (err) {
this.setState({
ttsEngine: this.props.ttsEngine.name,
loading: false,
openTtsEngineError: true
});
}
}

handleMoreLangClick() {
this.setState({ moreLangDialog: true });
}
Expand All @@ -102,9 +149,15 @@ class Language extends React.Component {
this.setState({ moreLangDialog: false });
}

handleTtsErrorDialogClose() {
this.setState({ openTtsEngineError: false });
}

render() {
const {
langs,
intl,
ttsEngines,
selectedLang,
onLangClick,
onClose,
Expand Down Expand Up @@ -152,7 +205,76 @@ class Language extends React.Component {
onSubmit={onSubmitLang}
>
<Paper>
<List>{langItems}</List>
{isCordova() && (
<React.Fragment>
<div className="Settings__Language__TTSEnginesContainer">
<FormControl
className="Settings__Language__TTSEnginesContainer__Select"
variant="standard"
error={this.state.ttsEngineError}
disabled={this.state.loading}
>
<InputLabel id="tts-engines-select-label">
<FormattedMessage {...messages.ttsEngines} />
</InputLabel>
<Select
labelId="tts-engines-select-label"
id="tts-engines-select"
autoWidth={false}
value={this.state.ttsEngine}
onChange={this.handleTtsEngineChange.bind(this)}
inputProps={{
name: 'tts-engine',
id: 'language-tts-engine'
}}
>
{ttsEngines.map((ttsEng, i) => (
<MenuItem key={i} value={ttsEng.name}>
{ttsEng.label}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<Divider variant="middle" />
</React.Fragment>
)}
{this.state.loading ? (
<CircularProgress
size={60}
className="Settings__Language__Spinner"
thickness={5}
/>
) : (
<List>{langItems}</List>
)}
<Dialog
onClose={this.handleTtsErrorDialogClose.bind(this)}
aria-labelledby="tts-error-dialog"
open={this.state.openTtsEngineError}
className="CommunicatorDialog__boardInfoDialog"
>
<DialogTitle
id="tts-error-dialog-title"
onClose={this.handleTtsErrorDialogClose.bind(this)}
>
<WarningIcon />
{intl.formatMessage(messages.ttsEngines)}
</DialogTitle>
<DialogContent>
<DialogContentText>
{intl.formatMessage(messages.ttsEngineError)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={this.handleTtsErrorDialogClose.bind(this)}
color="primary"
>
{intl.formatMessage(messages.close)}
</Button>
</DialogActions>
</Dialog>
</Paper>
<div className="Settings__Language__MoreLang">
<Button color="primary" onClick={this.handleMoreLangClick.bind(this)}>
Expand Down Expand Up @@ -200,4 +322,4 @@ const mapDispatchToProps = {};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Language);
)(injectIntl(Language));
38 changes: 32 additions & 6 deletions src/components/Settings/Language/Language.container.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';

import { changeLang } from '../../../providers/LanguageProvider/LanguageProvider.actions';
import { setTtsEngine } from '../../../providers/SpeechProvider/SpeechProvider.actions';
import Language from './Language.component';
import messages from './Language.messages';
import API from '../../../api';
Expand All @@ -28,11 +29,22 @@ export class LanguageContainer extends Component {
* Language list
*/
langs: PropTypes.arrayOf(PropTypes.string).isRequired,

/**
* TTS engines list
*/
ttsEngines: PropTypes.arrayOf(PropTypes.object),
/**
* TTS default engine
*/
ttsDefaultEngine: PropTypes.object,
/**
* Callback fired when language changes
*/
onLangChange: PropTypes.func,
/**
* Callback fired when tts engine changes
*/
setTtsEngine: PropTypes.func.isRequired,
/**
* Callback fired when clicking the back button
*/
Expand All @@ -47,8 +59,9 @@ export class LanguageContainer extends Component {

try {
await API.updateSettings({ language: { lang: this.state.selectedLang } });
} catch (e) {}

} catch (err) {
console.log(err.message);
}
onLangChange(this.state.selectedLang);
};

Expand All @@ -57,29 +70,42 @@ export class LanguageContainer extends Component {
};

render() {
const { history, lang, langs } = this.props;
const {
history,
lang,
langs,
ttsEngines,
ttsEngine,
setTtsEngine
} = this.props;
const sortedLangs = sortLangs(lang, langs);

return (
<Language
title={<FormattedMessage {...messages.language} />}
selectedLang={this.state.selectedLang}
langs={sortedLangs}
ttsEngines={ttsEngines ? ttsEngines : []}
ttsEngine={ttsEngine}
onLangClick={this.handleLangClick}
onClose={history.goBack}
onSubmitLang={this.handleSubmit}
setTtsEngine={setTtsEngine}
/>
);
}
}

const mapStateToProps = state => ({
lang: state.language.lang,
langs: state.language.langs
langs: state.language.langs,
ttsEngines: state.speech.ttsEngines,
ttsEngine: state.speech.ttsEngine
});

const mapDispatchToProps = {
onLangChange: changeLang
onLangChange: changeLang,
setTtsEngine
};

export default connect(
Expand Down
9 changes: 9 additions & 0 deletions src/components/Settings/Language/Language.messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,14 @@ export default defineMessages({
zh: {
id: 'cboard.components.Settings.Language.chinese',
defaultMessage: 'Chinese'
},
ttsEngines: {
id: 'cboard.components.Settings.Language.ttsEngines',
defaultMessage: 'Text to speech engine'
},
ttsEngineError: {
id: 'cboard.components.Settings.Language.ttsEngineError',
defaultMessage:
'WARNING: This text to speech engine does not include any language supported by Cboard!. Feel free to contact us and let us know.'
}
});
17 changes: 17 additions & 0 deletions src/components/Settings/Settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,20 @@
width: 85%;
padding: 1rem;
}

.Settings__Language__TTSEnginesContainer {
padding: 1rem;
padding-bottom: 2rem;
position: relative;
}

.Settings__Language__TTSEnginesContainer__Select {
width: 60%;
}

.Settings__Language__Spinner {
color: rgb(121, 22, 254);
position: fixed;
top: 50%;
left: 50%;
}
35 changes: 34 additions & 1 deletion src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { addLocaleData } from 'react-intl';
import { alpha3TToAlpha2 } from '@cospired/i18n-iso-languages';
import { alpha3ToAlpha2 } from 'i18n-iso-countries';

import { APP_LANGS } from './components/App/App.constants';
import { APP_LANGS, DEFAULT_LANG } from './components/App/App.constants';
import { EMPTY_VOICES } from './providers/SpeechProvider/SpeechProvider.constants';

const splitLangRgx = /[_-]+/;
Expand Down Expand Up @@ -60,13 +60,46 @@ export function standardizeLanguageCode(lang) {
return `${standardLang}-${standardCountry}`;
}

export function getDefaultLang(langs) {
for (let i = 0; i < langs.length; i++) {
if (window.navigator.language.slice(0, 2) === langs[i].slice(0, 2)) {
return langs[i];
}
}
return langs.includes(DEFAULT_LANG) ? DEFAULT_LANG : langs[0];
}

export function getVoicesLangs(voices) {
let langs = [...new Set(voices.map(voice => voice.lang))].sort();
langs = langs.map(lang => standardizeLanguageCode(lang));
langs = langs.map(lang => normalizeLanguageCode(lang));
return langs.filter(lang => APP_LANGS.includes(lang));
}

export function getSupportedLangs(voices) {
let supportedLangs = [];
if (voices.length) {
const sLanguages = getVoicesLangs(voices);
if (sLanguages !== undefined && sLanguages.length) {
supportedLangs = sLanguages;
//hack just for Alfanum Serbian voices
//https://github.com/cboard-org/cboard/issues/715
if (supportedLangs.includes('sr-RS')) {
supportedLangs.push('sr-SP');
}
//hack just for Tetum language
//https://github.com/cboard-org/cboard/issues/848
if (
supportedLangs.includes('pt-BR') ||
supportedLangs.includes('pt-PT')
) {
supportedLangs.push('pt-TL');
}
}
}
return supportedLangs;
}

export function getVoiceURI(language, voices) {
let nVoices = voices.map(({ voiceURI, name, lang }) => ({
voiceURI,
Expand Down
Loading

0 comments on commit d6f9eab

Please sign in to comment.