Skip to content

Commit d6f9eab

Browse files
Merge pull request #863 from cboard-org/feature/tts-engine-select
Support for TTS engine selection in Cordova
2 parents e9859bb + fc55f8a commit d6f9eab

File tree

11 files changed

+428
-70
lines changed

11 files changed

+428
-70
lines changed

src/components/Settings/Language/Language.component.js

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { connect } from 'react-redux';
4-
import { FormattedMessage } from 'react-intl';
4+
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
55
import ISO6391 from 'iso-639-1';
66
import List from '@material-ui/core/List';
77
import ListItem from '@material-ui/core/ListItem';
88
import ListItemText from '@material-ui/core/ListItemText';
99
import Paper from '@material-ui/core/Paper';
1010
import CheckIcon from '@material-ui/icons/Check';
11+
import WarningIcon from '@material-ui/icons/Warning';
1112
import { Button } from '@material-ui/core';
1213
import Dialog from '@material-ui/core/Dialog';
1314
import DialogTitle from '@material-ui/core/DialogTitle';
1415
import DialogContent from '@material-ui/core/DialogContent';
1516
import DialogActions from '@material-ui/core/DialogActions';
17+
import DialogContentText from '@material-ui/core/DialogContentText';
1618
import Slide from '@material-ui/core/Slide';
19+
import Select from '@material-ui/core/Select';
20+
import InputLabel from '@material-ui/core/InputLabel';
21+
import FormControl from '@material-ui/core/FormControl';
22+
import MenuItem from '@material-ui/core/MenuItem';
23+
import Divider from '@material-ui/core/Divider';
24+
import CircularProgress from '@material-ui/core/CircularProgress';
1725
import ReactMarkdown from 'react-markdown';
1826

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

2937
class Language extends React.Component {
3038
static propTypes = {
39+
intl: intlShape.isRequired,
3140
/**
3241
* Languages to display
3342
*/
@@ -48,7 +57,16 @@ class Language extends React.Component {
4857
* Callback fired when submitting selected language
4958
*/
5059
onSubmitLang: PropTypes.func.isRequired,
51-
language: PropTypes.object.isRequired
60+
language: PropTypes.object.isRequired,
61+
/**
62+
* TTS engines list
63+
*/
64+
ttsEngines: PropTypes.arrayOf(PropTypes.object),
65+
/**
66+
* TTS default engine
67+
*/
68+
ttsEngine: PropTypes.object,
69+
setTtsEngine: PropTypes.func.isRequired
5270
};
5371

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

6280
this.state = {
6381
moreLangDialog: false,
82+
loading: false,
83+
ttsEngine: props.ttsEngine.name,
84+
openTtsEngineError: false,
6485
markdown: ''
6586
};
6687
}
@@ -94,6 +115,32 @@ class Language extends React.Component {
94115
}
95116
}
96117

118+
componentDidUpdate(prevProps) {
119+
if (this.props.ttsEngine.name !== prevProps.ttsEngine.name) {
120+
this.setState({
121+
ttsEngine: this.props.ttsEngine.name,
122+
loading: false
123+
});
124+
}
125+
}
126+
127+
async handleTtsEngineChange(event) {
128+
const { setTtsEngine } = this.props;
129+
this.setState({
130+
ttsEngine: event.target.value,
131+
loading: true
132+
});
133+
try {
134+
await setTtsEngine(event.target.value);
135+
} catch (err) {
136+
this.setState({
137+
ttsEngine: this.props.ttsEngine.name,
138+
loading: false,
139+
openTtsEngineError: true
140+
});
141+
}
142+
}
143+
97144
handleMoreLangClick() {
98145
this.setState({ moreLangDialog: true });
99146
}
@@ -102,9 +149,15 @@ class Language extends React.Component {
102149
this.setState({ moreLangDialog: false });
103150
}
104151

152+
handleTtsErrorDialogClose() {
153+
this.setState({ openTtsEngineError: false });
154+
}
155+
105156
render() {
106157
const {
107158
langs,
159+
intl,
160+
ttsEngines,
108161
selectedLang,
109162
onLangClick,
110163
onClose,
@@ -152,7 +205,76 @@ class Language extends React.Component {
152205
onSubmit={onSubmitLang}
153206
>
154207
<Paper>
155-
<List>{langItems}</List>
208+
{isCordova() && (
209+
<React.Fragment>
210+
<div className="Settings__Language__TTSEnginesContainer">
211+
<FormControl
212+
className="Settings__Language__TTSEnginesContainer__Select"
213+
variant="standard"
214+
error={this.state.ttsEngineError}
215+
disabled={this.state.loading}
216+
>
217+
<InputLabel id="tts-engines-select-label">
218+
<FormattedMessage {...messages.ttsEngines} />
219+
</InputLabel>
220+
<Select
221+
labelId="tts-engines-select-label"
222+
id="tts-engines-select"
223+
autoWidth={false}
224+
value={this.state.ttsEngine}
225+
onChange={this.handleTtsEngineChange.bind(this)}
226+
inputProps={{
227+
name: 'tts-engine',
228+
id: 'language-tts-engine'
229+
}}
230+
>
231+
{ttsEngines.map((ttsEng, i) => (
232+
<MenuItem key={i} value={ttsEng.name}>
233+
{ttsEng.label}
234+
</MenuItem>
235+
))}
236+
</Select>
237+
</FormControl>
238+
</div>
239+
<Divider variant="middle" />
240+
</React.Fragment>
241+
)}
242+
{this.state.loading ? (
243+
<CircularProgress
244+
size={60}
245+
className="Settings__Language__Spinner"
246+
thickness={5}
247+
/>
248+
) : (
249+
<List>{langItems}</List>
250+
)}
251+
<Dialog
252+
onClose={this.handleTtsErrorDialogClose.bind(this)}
253+
aria-labelledby="tts-error-dialog"
254+
open={this.state.openTtsEngineError}
255+
className="CommunicatorDialog__boardInfoDialog"
256+
>
257+
<DialogTitle
258+
id="tts-error-dialog-title"
259+
onClose={this.handleTtsErrorDialogClose.bind(this)}
260+
>
261+
<WarningIcon />
262+
{intl.formatMessage(messages.ttsEngines)}
263+
</DialogTitle>
264+
<DialogContent>
265+
<DialogContentText>
266+
{intl.formatMessage(messages.ttsEngineError)}
267+
</DialogContentText>
268+
</DialogContent>
269+
<DialogActions>
270+
<Button
271+
onClick={this.handleTtsErrorDialogClose.bind(this)}
272+
color="primary"
273+
>
274+
{intl.formatMessage(messages.close)}
275+
</Button>
276+
</DialogActions>
277+
</Dialog>
156278
</Paper>
157279
<div className="Settings__Language__MoreLang">
158280
<Button color="primary" onClick={this.handleMoreLangClick.bind(this)}>
@@ -200,4 +322,4 @@ const mapDispatchToProps = {};
200322
export default connect(
201323
mapStateToProps,
202324
mapDispatchToProps
203-
)(Language);
325+
)(injectIntl(Language));

src/components/Settings/Language/Language.container.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
44
import { FormattedMessage } from 'react-intl';
55

66
import { changeLang } from '../../../providers/LanguageProvider/LanguageProvider.actions';
7+
import { setTtsEngine } from '../../../providers/SpeechProvider/SpeechProvider.actions';
78
import Language from './Language.component';
89
import messages from './Language.messages';
910
import API from '../../../api';
@@ -28,11 +29,22 @@ export class LanguageContainer extends Component {
2829
* Language list
2930
*/
3031
langs: PropTypes.arrayOf(PropTypes.string).isRequired,
31-
32+
/**
33+
* TTS engines list
34+
*/
35+
ttsEngines: PropTypes.arrayOf(PropTypes.object),
36+
/**
37+
* TTS default engine
38+
*/
39+
ttsDefaultEngine: PropTypes.object,
3240
/**
3341
* Callback fired when language changes
3442
*/
3543
onLangChange: PropTypes.func,
44+
/**
45+
* Callback fired when tts engine changes
46+
*/
47+
setTtsEngine: PropTypes.func.isRequired,
3648
/**
3749
* Callback fired when clicking the back button
3850
*/
@@ -47,8 +59,9 @@ export class LanguageContainer extends Component {
4759

4860
try {
4961
await API.updateSettings({ language: { lang: this.state.selectedLang } });
50-
} catch (e) {}
51-
62+
} catch (err) {
63+
console.log(err.message);
64+
}
5265
onLangChange(this.state.selectedLang);
5366
};
5467

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

5972
render() {
60-
const { history, lang, langs } = this.props;
73+
const {
74+
history,
75+
lang,
76+
langs,
77+
ttsEngines,
78+
ttsEngine,
79+
setTtsEngine
80+
} = this.props;
6181
const sortedLangs = sortLangs(lang, langs);
6282

6383
return (
6484
<Language
6585
title={<FormattedMessage {...messages.language} />}
6686
selectedLang={this.state.selectedLang}
6787
langs={sortedLangs}
88+
ttsEngines={ttsEngines ? ttsEngines : []}
89+
ttsEngine={ttsEngine}
6890
onLangClick={this.handleLangClick}
6991
onClose={history.goBack}
7092
onSubmitLang={this.handleSubmit}
93+
setTtsEngine={setTtsEngine}
7194
/>
7295
);
7396
}
7497
}
7598

7699
const mapStateToProps = state => ({
77100
lang: state.language.lang,
78-
langs: state.language.langs
101+
langs: state.language.langs,
102+
ttsEngines: state.speech.ttsEngines,
103+
ttsEngine: state.speech.ttsEngine
79104
});
80105

81106
const mapDispatchToProps = {
82-
onLangChange: changeLang
107+
onLangChange: changeLang,
108+
setTtsEngine
83109
};
84110

85111
export default connect(

src/components/Settings/Language/Language.messages.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,14 @@ export default defineMessages({
164164
zh: {
165165
id: 'cboard.components.Settings.Language.chinese',
166166
defaultMessage: 'Chinese'
167+
},
168+
ttsEngines: {
169+
id: 'cboard.components.Settings.Language.ttsEngines',
170+
defaultMessage: 'Text to speech engine'
171+
},
172+
ttsEngineError: {
173+
id: 'cboard.components.Settings.Language.ttsEngineError',
174+
defaultMessage:
175+
'WARNING: This text to speech engine does not include any language supported by Cboard!. Feel free to contact us and let us know.'
167176
}
168177
});

src/components/Settings/Settings.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,20 @@
5454
width: 85%;
5555
padding: 1rem;
5656
}
57+
58+
.Settings__Language__TTSEnginesContainer {
59+
padding: 1rem;
60+
padding-bottom: 2rem;
61+
position: relative;
62+
}
63+
64+
.Settings__Language__TTSEnginesContainer__Select {
65+
width: 60%;
66+
}
67+
68+
.Settings__Language__Spinner {
69+
color: rgb(121, 22, 254);
70+
position: fixed;
71+
top: 50%;
72+
left: 50%;
73+
}

src/i18n.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { addLocaleData } from 'react-intl';
22
import { alpha3TToAlpha2 } from '@cospired/i18n-iso-languages';
33
import { alpha3ToAlpha2 } from 'i18n-iso-countries';
44

5-
import { APP_LANGS } from './components/App/App.constants';
5+
import { APP_LANGS, DEFAULT_LANG } from './components/App/App.constants';
66
import { EMPTY_VOICES } from './providers/SpeechProvider/SpeechProvider.constants';
77

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

63+
export function getDefaultLang(langs) {
64+
for (let i = 0; i < langs.length; i++) {
65+
if (window.navigator.language.slice(0, 2) === langs[i].slice(0, 2)) {
66+
return langs[i];
67+
}
68+
}
69+
return langs.includes(DEFAULT_LANG) ? DEFAULT_LANG : langs[0];
70+
}
71+
6372
export function getVoicesLangs(voices) {
6473
let langs = [...new Set(voices.map(voice => voice.lang))].sort();
6574
langs = langs.map(lang => standardizeLanguageCode(lang));
6675
langs = langs.map(lang => normalizeLanguageCode(lang));
6776
return langs.filter(lang => APP_LANGS.includes(lang));
6877
}
6978

79+
export function getSupportedLangs(voices) {
80+
let supportedLangs = [];
81+
if (voices.length) {
82+
const sLanguages = getVoicesLangs(voices);
83+
if (sLanguages !== undefined && sLanguages.length) {
84+
supportedLangs = sLanguages;
85+
//hack just for Alfanum Serbian voices
86+
//https://github.com/cboard-org/cboard/issues/715
87+
if (supportedLangs.includes('sr-RS')) {
88+
supportedLangs.push('sr-SP');
89+
}
90+
//hack just for Tetum language
91+
//https://github.com/cboard-org/cboard/issues/848
92+
if (
93+
supportedLangs.includes('pt-BR') ||
94+
supportedLangs.includes('pt-PT')
95+
) {
96+
supportedLangs.push('pt-TL');
97+
}
98+
}
99+
}
100+
return supportedLangs;
101+
}
102+
70103
export function getVoiceURI(language, voices) {
71104
let nVoices = voices.map(({ voiceURI, name, lang }) => ({
72105
voiceURI,

0 commit comments

Comments
 (0)