Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add locale defaulting for SpeechRecognition and support check method #1468

Merged
merged 3 commits into from
Jul 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"test": true,
"fixture": true,
"CustomEvent": true,
"ANSWERS": true
"ANSWERS": true,
"SpeechRecognition": true,
"webkitSpeechRecognition": true
}
}
6 changes: 5 additions & 1 deletion conf/gulp-tasks/bundle/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const replace = require('gulp-replace');
const resolve = require('rollup-plugin-node-resolve');
const rollup = require('gulp-rollup-lightweight');
const source = require('vinyl-source-stream');
const { TRANSLATION_FLAGGER_REGEX } = require('../../i18n/constants');
const {
TRANSLATION_FLAGGER_REGEX,
SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE
} = require('../../i18n/constants');

const TranslateCallParser = require('../../i18n/translatecallparser');

Expand Down Expand Up @@ -109,6 +112,7 @@ function _buildBundle (callback, rollupConfig, bundleName, locale, libVersion, t
.pipe(source(`${bundleName}.js`))
.pipe(replace('@@LIB_VERSION', libVersion))
.pipe(replace('@@LOCALE', locale))
.pipe(replace('\'@@SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE\'', JSON.stringify(SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE)))
.pipe(replace(TRANSLATION_FLAGGER_REGEX, translateCall => {
const placeholder = new TranslateCallParser().parse(translateCall);
const translationResult = translationResolver.resolve(placeholder);
Expand Down
40 changes: 40 additions & 0 deletions conf/i18n/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,43 @@ const LANGUAGES_TO_LOCALES = {
exports.LANGUAGES_TO_LOCALES = LANGUAGES_TO_LOCALES;

exports.ALL_LANGUAGES = Object.keys(LANGUAGES_TO_LOCALES);

/**
* Of the 3 browsers that support speech recognition, only Edge will
* error out if it encounters a locale it doesn't like.
* Chrome and Safari still do their best to use a reasonable locale.
*
* This list of locales is used to manually default to the 2 character language code
* for locales not in this list, when the user's browser is Edge.
*/
exports.SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE = [
'en-ae',
'en-au',
'en-ca',
'en-de',
'en-gb',
'en-hk',
'en-ie',
'en-jm',
'en-sg',
'en-us',
'en-za',
'es-bo',
'es-co',
'es-cu',
'es-es',
'es-gt',
'es-hn',
'es-mx',
'es-ni',
'es-us',
'fr-be',
'fr-ca',
'fr-ch',
'fr-fr',
'it-it',
'de-at',
'de-ch',
'de-de',
'ja-jp'
];
3 changes: 3 additions & 0 deletions src/core/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const LIB_VERSION = '@@LIB_VERSION';
/** The current locale, injected by the build process */
export const LOCALE = '@@LOCALE';

/** The speech recognition locales supported by Microsoft Edge */
export const SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE = '@@SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE';

/** The identifier of the production environment */
export const PRODUCTION = 'production';

Expand Down
23 changes: 23 additions & 0 deletions src/core/speechrecognition/locales.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE } from '../constants';

/**
* Transforms the given locale to a locale Microsoft Edge can understand.
* This means changing the language/locale separating underscore to a dash,
* and defaulting the locale to the 2 character language code if it is not
* supported by Edge.
*
* @param {string} locale
* @returns {string}
*/
export function transformSpeechRecognitionLocaleForEdge (locale) {
const underscoreIndex = locale.indexOf('_');
if (underscoreIndex === -1) {
return locale;
}
locale = locale.replace('_', '-');
cea2aj marked this conversation as resolved.
Show resolved Hide resolved
const isCompatibleWithEdge = SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE.includes(locale.toLowerCase());
cea2aj marked this conversation as resolved.
Show resolved Hide resolved
if (isCompatibleWithEdge) {
return locale;
}
return locale.substring(0, underscoreIndex);
}
13 changes: 13 additions & 0 deletions src/core/speechrecognition/support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Whether the SpeechRecognition API is supported by the current browser.
*
* Currently all languages in the SDK (en, es, fr, de, it, ja)
* have SpeechRecognition support in browsers that support SpeechRecognition.
* However, because browser specific SpeechRecognition documentation is poor to nonexistent,
* new languages/locales will need to be manually tested for SpeechRecognition support.
*
* @returns {boolean}
*/
export function speechRecognitionIsSupported () {
return !!(SpeechRecognition || webkitSpeechRecognition);
}
15 changes: 15 additions & 0 deletions src/core/utils/useragent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Returns whether the current browser is Microsoft Edge.
* Tries to use User-Agent clients hints, and defaults to
* using the User-Agent string if clients hints are not supported.
*
* @returns {boolean}
*/
export function isMicrosoftEdge () {
const brands = navigator.userAgentData?.brands;
if (brands && brands.length > 0) {
return !!brands.find(b => b.brand === 'Microsoft Edge');
} else if (navigator.userAgent) {
return navigator.userAgent.includes('Edg');
cea2aj marked this conversation as resolved.
Show resolved Hide resolved
}
}
31 changes: 31 additions & 0 deletions tests/core/speechrecognition/locales.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { transformSpeechRecognitionLocaleForEdge } from '../../../src/core/speechrecognition/locales';

jest.mock('../../../src/core/constants', () => ({
SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE:
require('../../../conf/i18n/constants').SPEECH_RECOGNITION_LOCALES_SUPPORTED_BY_EDGE
cea2aj marked this conversation as resolved.
Show resolved Hide resolved
}));

it('does nothing when no underscore', () => {
expect(transformSpeechRecognitionLocaleForEdge('en')).toEqual('en');
});

it('will recognize supported locales that have dashes', () => {
expect(transformSpeechRecognitionLocaleForEdge('en-US')).toEqual('en-US');
expect(transformSpeechRecognitionLocaleForEdge('en-GB')).toEqual('en-GB');
});

it('defaults Edge incompatible locales to the language code', () => {
expect(transformSpeechRecognitionLocaleForEdge('ja_FAKE')).toEqual('ja');
expect(transformSpeechRecognitionLocaleForEdge('en_AI')).toEqual('en');
});

it('replaces underscores with dashes for supported locales', () => {
expect(transformSpeechRecognitionLocaleForEdge('en_US')).toEqual('en-US');
expect(transformSpeechRecognitionLocaleForEdge('en_GB')).toEqual('en-GB');
});

it('is case insensitive', () => {
expect(transformSpeechRecognitionLocaleForEdge('en_us')).toEqual('en-us');
expect(transformSpeechRecognitionLocaleForEdge('EN_AI')).toEqual('EN');
cea2aj marked this conversation as resolved.
Show resolved Hide resolved
expect(transformSpeechRecognitionLocaleForEdge('jA_AI')).toEqual('jA');
});
66 changes: 66 additions & 0 deletions tests/core/utils/useragent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { isMicrosoftEdge } from '../../../src/core/utils/useragent';

afterEach(() => {
jest.restoreAllMocks();
delete navigator.userAgentData;
});

describe('can detect edge using UserAgent client hints', () => {
it('Chrome', () => {
navigator.userAgentData = {
brands: [
{ brand: ' Not;A Brand', version: '99' },
{ brand: 'Google Chrome', version: '91' },
{ brand: 'Chromium', version: '91' }
]
};
expect(isMicrosoftEdge()).toBeFalsy();
});

it('Edge', () => {
navigator.userAgentData = {
brands: [
{ brand: ' Not;A Brand', version: '99' },
{ brand: 'Microsoft Edge', version: '91' },
{ brand: 'Chromium', version: '91' }
]
};
expect(isMicrosoftEdge()).toBeTruthy();
});
});

describe('can detect edge from the UserAgent string', () => {
const userAgents = {
macOS: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.67',
'Windows 10': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.67',
Android: 'Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36 EdgA/46.5.4.5158',
iOS: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 EdgiOS/46.3.13 Mobile/15E148 Safari/605.1.15',
'Windows 10 Mobile': 'Mozilla/5.0 (Windows Mobile 10; Android 10.0; Microsoft; Lumia 950XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36 Edge/40.15254.603'
};
for (const [scenario, userAgent] of Object.entries(userAgents)) {
it(scenario, () => {
mockUserAgent(userAgent);
expect(isMicrosoftEdge()).toBeTruthy();
});
}
});

describe('can detect browsers as NOT edge from the UserAgent string', () => {
const userAgents = {
'Chrome + macOS': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
'Safari + macOS': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
'Firefox + macOS': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0',
'Chrome + Windows 10': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Firefox + Windows 10': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0'
};
for (const [scenario, userAgent] of Object.entries(userAgents)) {
it(scenario, () => {
mockUserAgent(userAgent);
expect(isMicrosoftEdge()).toBeFalsy();
});
}
});

function mockUserAgent (userAgent) {
jest.spyOn(navigator, 'userAgent', 'get').mockImplementation(() => userAgent);
}