Commit 12e14c64 authored by Akihiro Ota's avatar Akihiro Ota Committed by Commit Bot

ChromeVox lang switching: Alert user if we do not have available voice

This changes ChromeVox language switching behavior to alert users
if we do not have an available voice for the current language, instead
of failing silently or butchering pronunciation with a different voice.
This change:
1. Adds logic in LanguageSwitching.js to check if there is a matching
voice for a language before making a language assignment
2. Modifies the accessibilityPrivate.getDisplayLanguage() api to
take an additional parameter (the desired output language)
3. Adds a LanguageSwitchingTest to confirm new behavior.
4. Modifies existing API tests to confirm new behavior.

Bug: 1008481
Change-Id: I5537c916a0e6b435c5c833b8e7fe7a9e530df65b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1829547
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#706050}
parent 4ba7eb3a
...@@ -42,6 +42,13 @@ LanguageSwitching.PROBABILITY_THRESHOLD_ = 0.9; ...@@ -42,6 +42,13 @@ LanguageSwitching.PROBABILITY_THRESHOLD_ = 0.9;
*/ */
LanguageSwitching.sub_node_switching_enabled_ = false; LanguageSwitching.sub_node_switching_enabled_ = false;
/**
* An array of all available TTS voices.
* @type {!Array<!TtsVoice>}
* @private
*/
LanguageSwitching.availableVoices_ = [];
/** /**
* Initialization function for language switching. * Initialization function for language switching.
*/ */
...@@ -53,18 +60,29 @@ LanguageSwitching.init = function() { ...@@ -53,18 +60,29 @@ LanguageSwitching.init = function() {
function(enabled) { function(enabled) {
LanguageSwitching.sub_node_switching_enabled_ = enabled; LanguageSwitching.sub_node_switching_enabled_ = enabled;
}); });
// Ensure that availableVoices_ is set and stays updated.
function setAvailableVoices() {
chrome.tts.getVoices(function(voices) {
LanguageSwitching.availableVoices_ = voices || [];
});
}
setAvailableVoices();
if (speechSynthesis) {
speechSynthesis.addEventListener(
'voiceschanged', setAvailableVoices, /* useCapture */ false);
}
}; };
/* /**
* Main language switching function. * Main language switching function.
* Cut up string attribute value into multiple spans with different * Cut up string attribute value into multiple spans with different
* languages. Ranges and associated language information are returned by the * languages. Ranges and associated language information are returned by the
* languageAnnotationsForStringAttribute() function. * languageAnnotationsForStringAttribute() function.
* @param {AutomationNode} node * @param {AutomationNode} node
* @param {string} stringAttribute The string attribute for which we want to * @param {string} stringAttribute The string attribute for which we want to
* get a language annotation * get a language annotation.
* @param {Function<outputString: string, newLanguage: string>} * @param {function(string, string)} appendStringWithLanguage
* appendStringWithLanguage
* A callback that appends outputString to the output buffer in newLanguage. * A callback that appends outputString to the output buffer in newLanguage.
*/ */
LanguageSwitching.assignLanguagesForStringAttribute = function( LanguageSwitching.assignLanguagesForStringAttribute = function(
...@@ -117,16 +135,32 @@ LanguageSwitching.assignLanguagesForStringAttribute = function( ...@@ -117,16 +135,32 @@ LanguageSwitching.assignLanguagesForStringAttribute = function(
stringAttributeValue, startIndex, endIndex); stringAttributeValue, startIndex, endIndex);
var newLanguage = var newLanguage =
LanguageSwitching.decideNewLanguage(node, language, probability); LanguageSwitching.decideNewLanguage(node, language, probability);
var displayLanguage = '';
if (LanguageSwitching.didLanguageSwitch(newLanguage)) { if (LanguageSwitching.didLanguageSwitch(newLanguage)) {
LanguageSwitching.currentLanguage_ = newLanguage; LanguageSwitching.currentLanguage_ = newLanguage;
var displayLanguage = // Get human-readable language in |newLanguage|.
chrome.accessibilityPrivate.getDisplayLanguage(newLanguage); displayLanguage = chrome.accessibilityPrivate.getDisplayLanguage(
// Prepend the human-readable language to outputString if language newLanguage /* Language code to translate */,
// switched. newLanguage /* Target language code */);
// Prepend the human-readable language to outputString.
outputString = outputString =
Msgs.getMsg('language_switch', [displayLanguage, outputString]); Msgs.getMsg('language_switch', [displayLanguage, outputString]);
} }
appendStringWithLanguage(newLanguage, outputString);
if (LanguageSwitching.hasVoiceForLanguage(newLanguage)) {
appendStringWithLanguage(newLanguage, outputString);
} else {
// Translate |newLanguage| into human-readable string in the UI language.
displayLanguage = chrome.accessibilityPrivate.getDisplayLanguage(
newLanguage /* Language code to translate */,
LanguageSwitching.browserUILanguage_ /* Target language code */);
outputString =
Msgs.getMsg('voice_unavailable_for_language', [displayLanguage]);
// Alert the user that we have no available voice for the language.
appendStringWithLanguage(
LanguageSwitching.browserUILanguage_, outputString);
}
} }
}; };
...@@ -227,5 +261,38 @@ LanguageSwitching.isValidLanguageCode = function(languageCode) { ...@@ -227,5 +261,38 @@ LanguageSwitching.isValidLanguageCode = function(languageCode) {
if (langComponentArray[0].length !== 2 && langComponentArray[0].length !== 3) if (langComponentArray[0].length !== 2 && langComponentArray[0].length !== 3)
return false; return false;
// Use the accessibilityPrivate.getDisplayLanguage() API to validate language
// code. If the language code is invalid, then this API returns an empty
// string.
if (chrome.accessibilityPrivate.getDisplayLanguage(
languageCode, languageCode) === '') {
return false;
}
return true; return true;
}; };
/**
* Returns true if there is a tts voice that supports the given languageCode.
* This function is not responsible for deciding the proper output voice, it
* simply tells us if output in |languageCode| is possible.
* @param {string} languageCode
* @return {boolean}
*/
LanguageSwitching.hasVoiceForLanguage = function(languageCode) {
// Extract language from languageCode.
var languageCodeComponents = languageCode.split('-');
if (!languageCodeComponents || (languageCodeComponents.length === 0))
return false;
var language = languageCodeComponents[0];
for (var i = 0; i < LanguageSwitching.availableVoices_.length; ++i) {
// Note: availableVoices_[i].lang is always in the form of
// 'language-region'. See link for documentation on chrome.tts api:
// https://developer.chrome.com/apps/tts#type-TtsVoice
var candidateLanguage =
LanguageSwitching.availableVoices_[i].lang.toLowerCase().split('-')[0];
if (language === candidateLanguage)
return true;
}
return false;
};
...@@ -1261,13 +1261,12 @@ Output.prototype = { ...@@ -1261,13 +1261,12 @@ Output.prototype = {
* Appends outputString to the output buffer in newLanguage. * Appends outputString to the output buffer in newLanguage.
* @param {!Array<Spannable>} buff * @param {!Array<Spannable>} buff
* @param {{isUnique: (boolean|undefined), * @param {{isUnique: (boolean|undefined),
* annotation: !Array<*>}} opt_options * annotation: !Array<*>}} options
* @param {string} newLanguage * @param {string} newLanguage
* @param {string} outputString * @param {string} outputString
*/ */
var appendStringWithLanguage = function( var appendStringWithLanguage = function(
buff, options, newLanguage, buff, options, newLanguage, outputString) {
outputString) {
var speechProps = new Output.SpeechProperties(); var speechProps = new Output.SpeechProperties();
// Set output language. // Set output language.
speechProps['lang'] = newLanguage; speechProps['lang'] = newLanguage;
...@@ -1276,10 +1275,11 @@ Output.prototype = { ...@@ -1276,10 +1275,11 @@ Output.prototype = {
// Attach associated SpeechProperties if the buffer is non-empty. // Attach associated SpeechProperties if the buffer is non-empty.
if (buff.length > 0) if (buff.length > 0)
buff[buff.length - 1].setSpan(speechProps, 0, 0); buff[buff.length - 1].setSpan(speechProps, 0, 0);
}.bind(this, buff, options); };
// Cut up node name into multiple spans with different languages. // Cut up node name into multiple spans with different languages.
LanguageSwitching.assignLanguagesForStringAttribute( LanguageSwitching.assignLanguagesForStringAttribute(
node, 'name', appendStringWithLanguage); node, 'name',
appendStringWithLanguage.bind(this, buff, options));
} else { } else {
// Append entire node name. // Append entire node name.
// TODO(akihiroota): Follow-up with dtseng about why we append empty // TODO(akihiroota): Follow-up with dtseng about why we append empty
......
...@@ -55,11 +55,18 @@ chrome.virtualKeyboardPrivate.VirtualKeyboardEvent; ...@@ -55,11 +55,18 @@ chrome.virtualKeyboardPrivate.VirtualKeyboardEvent;
*/ */
chrome.virtualKeyboardPrivate.sendKeyEvent = function(keyEvent, opt_callback) { chrome.virtualKeyboardPrivate.sendKeyEvent = function(keyEvent, opt_callback) {
}; };
/** /**
* @param {function(!{a11ymode: boolean})} opt_callback * @param {function(!{a11ymode: boolean})} opt_callback
*/ */
chrome.virtualKeyboardPrivate.getKeyboardConfig = function(opt_callback) {}; chrome.virtualKeyboardPrivate.getKeyboardConfig = function(opt_callback) {};
/** /**
* @type {Object} * @type {Object}
*/ */
window.speechSynthesis; window.speechSynthesis;
/**
* @type {Event}
*/
window.speechSynthesis.onvoiceschanged;
...@@ -3742,6 +3742,9 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button ...@@ -3742,6 +3742,9 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button
<message desc="The description of the announceRichTextDescription key. Displayed in the ChromeVox menu." name="IDS_CHROMEVOX_ANNOUNCE_RICH_TEXT_DESCRIPTION"> <message desc="The description of the announceRichTextDescription key. Displayed in the ChromeVox menu." name="IDS_CHROMEVOX_ANNOUNCE_RICH_TEXT_DESCRIPTION">
Announce formatting for current item Announce formatting for current item
</message> </message>
<message desc="Announced when there is no available voice for a language." name="IDS_CHROMEVOX_VOICE_UNAVAILABLE_FOR_LANGUAGE">
No voice available for language: <ph name="language">$1<ex>English</ex></ph>
</message>
</messages> </messages>
</release> </release>
</grit> </grit>
...@@ -151,10 +151,14 @@ ...@@ -151,10 +151,14 @@
"name": "getDisplayLanguage", "name": "getDisplayLanguage",
"type": "function", "type": "function",
"nocompile": true, "nocompile": true,
"description": "Called to translate language code into human-readable string in the language of the provided language code.", "description": "Called to translate languageCodeToTranslate into human-readable string in the language specified by targetLanguageCode",
"parameters": [ "parameters": [
{ {
"name": "languageCode", "name": "languageCodeToTranslate",
"type": "string"
},
{
"name": "targetLanguageCode",
"type": "string" "type": "string"
} }
], ],
......
...@@ -50,20 +50,61 @@ RequestResult AccessibilityPrivateHooksDelegate::HandleRequest( ...@@ -50,20 +50,61 @@ RequestResult AccessibilityPrivateHooksDelegate::HandleRequest(
*parse_result.arguments); *parse_result.arguments);
} }
// Called to translate a language code into human-readable string in the // Called to translate |language_code_to_translate| into human-readable string
// language of the provided language code. Language codes can have optional // in the language specified by |target_language_code|. For example, if
// country code e.g. 'en' and 'en-us' both yield an output of 'English'. // language_code_to_translate = 'en' and target_language_code = 'fr', then this
// As another example, the code 'fr' would produce 'francais' as output. // function returns 'anglais'.
// If language_code_to_translate = 'fr' and target_language_code = 'en', then
// this function returns 'french'.
RequestResult AccessibilityPrivateHooksDelegate::HandleGetDisplayLanguage( RequestResult AccessibilityPrivateHooksDelegate::HandleGetDisplayLanguage(
ScriptContext* script_context, ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& parsed_arguments) { const std::vector<v8::Local<v8::Value>>& parsed_arguments) {
DCHECK(script_context->extension()); DCHECK(script_context->extension());
DCHECK_EQ(1u, parsed_arguments.size()); DCHECK_EQ(2u, parsed_arguments.size());
DCHECK(parsed_arguments[0]->IsString()); DCHECK(parsed_arguments[0]->IsString());
icu::Locale locale = icu::Locale( DCHECK(parsed_arguments[1]->IsString());
gin::V8ToString(script_context->isolate(), parsed_arguments[0]).c_str());
std::string language_code_to_translate =
gin::V8ToString(script_context->isolate(), parsed_arguments[0]);
std::string target_language_code =
gin::V8ToString(script_context->isolate(), parsed_arguments[1]);
// The locale whose language code we want to translate.
icu::Locale locale_to_translate =
icu::Locale(language_code_to_translate.c_str());
// The locale that |display_language| should be in.
icu::Locale target_locale = icu::Locale(target_language_code.c_str());
// Validate locales.
// Get list of available locales. Please see the ICU User Guide for more
// details: http://userguide.icu-project.org/locale.
bool valid_arg1 = false;
bool valid_arg2 = false;
int32_t num_locales = 0;
const icu::Locale* available_locales =
icu::Locale::getAvailableLocales(num_locales);
for (int32_t i = 0; i < num_locales; ++i) {
// Check both the language and country for each locale.
const char* current_language = available_locales[i].getLanguage();
const char* current_country = available_locales[i].getCountry();
if (strcmp(locale_to_translate.getLanguage(), current_language) == 0 &&
strcmp(locale_to_translate.getCountry(), current_country) == 0) {
valid_arg1 = true;
}
if (strcmp(target_locale.getLanguage(), current_language) == 0 &&
strcmp(target_locale.getCountry(), current_country) == 0) {
valid_arg2 = true;
}
}
// If either of the language codes is invalid, we should return empty string.
if (!(valid_arg1 && valid_arg2)) {
RequestResult empty_result(RequestResult::HANDLED);
empty_result.return_value = gin::StringToV8(script_context->isolate(), "");
return empty_result;
}
icu::UnicodeString display_language; icu::UnicodeString display_language;
locale.getDisplayLanguage(locale, display_language); locale_to_translate.getDisplayLanguage(target_locale, display_language);
std::string language_result = std::string language_result =
base::UTF16ToUTF8(base::i18n::UnicodeStringToString16(display_language)); base::UTF16ToUTF8(base::i18n::UnicodeStringToString16(display_language));
// Instead of returning "und", which is what the ICU Locale class returns for // Instead of returning "und", which is what the ICU Locale class returns for
......
...@@ -63,13 +63,17 @@ TEST_F(AccessibilityPrivateHooksDelegateTest, TestGetDisplayLanguage) { ...@@ -63,13 +63,17 @@ TEST_F(AccessibilityPrivateHooksDelegateTest, TestGetDisplayLanguage) {
}; };
// Test behavior. // Test behavior.
EXPECT_EQ(R"("")", run_get_display_language("''")); EXPECT_EQ(R"("")", run_get_display_language("'',''"));
EXPECT_EQ(R"("")", run_get_display_language("'not a language code'")); EXPECT_EQ(R"("")", run_get_display_language("'not a language code','ja-JP'"));
EXPECT_EQ(R"("English")", run_get_display_language("'en'")); EXPECT_EQ(R"("")",
EXPECT_EQ(R"("English")", run_get_display_language("'en-US'")); run_get_display_language("'zh-TW', 'not a language code'"));
EXPECT_EQ(R"("français")", run_get_display_language("'fr'")); EXPECT_EQ(R"("English")", run_get_display_language("'en','en'"));
EXPECT_EQ(R"("español")", run_get_display_language("'es'")); EXPECT_EQ(R"("English")", run_get_display_language("'en-US','en'"));
EXPECT_EQ(R"("日本語")", run_get_display_language("'ja'")); EXPECT_EQ(R"("français")", run_get_display_language("'fr','fr'"));
EXPECT_EQ(R"("español")", run_get_display_language("'es','es'"));
EXPECT_EQ(R"("日本語")", run_get_display_language("'ja','ja'"));
EXPECT_EQ(R"("anglais")", run_get_display_language("'en','fr'"));
EXPECT_EQ(R"("Japanese")", run_get_display_language("'ja','en'"));
} }
} // namespace extensions } // namespace extensions
...@@ -2,27 +2,30 @@ ...@@ -2,27 +2,30 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
var allTests = [ var allTests = [function getDisplayLanguage() {
function getDisplayLanguage() { chrome.test.assertEq(
chrome.test.assertEq( '', chrome.accessibilityPrivate.getDisplayLanguage('', ''));
'', chrome.test.assertEq(
chrome.accessibilityPrivate.getDisplayLanguage('')); '',
chrome.test.assertEq( chrome.accessibilityPrivate.getDisplayLanguage(
'', 'not a language code', 'ja-JP'));
chrome.accessibilityPrivate.getDisplayLanguage('not a language code')); chrome.test.assertEq(
chrome.test.assertEq( '',
'English', chrome.accessibilityPrivate.getDisplayLanguage(
chrome.accessibilityPrivate.getDisplayLanguage('en')); 'zh-TW', 'not a language code'));
chrome.test.assertEq( chrome.test.assertEq(
'English', 'English', chrome.accessibilityPrivate.getDisplayLanguage('en', 'en'));
chrome.accessibilityPrivate.getDisplayLanguage('en-US')); chrome.test.assertEq(
chrome.test.assertEq( 'English', chrome.accessibilityPrivate.getDisplayLanguage('en-US', 'en'));
'français', chrome.test.assertEq(
chrome.accessibilityPrivate.getDisplayLanguage('fr')); 'français',
chrome.test.assertEq( chrome.accessibilityPrivate.getDisplayLanguage('fr', 'fr-FR'));
'日本語', chrome.test.assertEq(
chrome.accessibilityPrivate.getDisplayLanguage('ja')); '日本語', chrome.accessibilityPrivate.getDisplayLanguage('ja', 'ja'));
chrome.test.succeed(); chrome.test.assertEq(
} 'anglais', chrome.accessibilityPrivate.getDisplayLanguage('en', 'fr-CA'));
]; chrome.test.assertEq(
'Japanese', chrome.accessibilityPrivate.getDisplayLanguage('ja', 'en'));
chrome.test.succeed();
}];
chrome.test.runTests(allTests); chrome.test.runTests(allTests);
...@@ -1119,6 +1119,17 @@ ...@@ -1119,6 +1119,17 @@
static AutomationNode getNextTextMatch( static AutomationNode getNextTextMatch(
DOMString searchStr, boolean backward); DOMString searchStr, boolean backward);
// Returns the detected languages for the provided string attribute as an
// array of LanguageSpan objects. There are several guarantees about the
// format of the LanguageSpan array:
// 1. Is either empty or contains LanguageSpans that cover all indices in
// the associated string attribute value.
// 2. Is sorted by increasing startIndex (those with smaller startIndex
// appear first).
// 3. LanguageSpans are non-overlapping and contain exactly one language.
static LanguageSpan[] languageAnnotationForStringAttribute(
DOMString attribute);
}; };
// Called when the <code>AutomationNode</code> for the page is available. // Called when the <code>AutomationNode</code> for the page is available.
...@@ -1165,16 +1176,5 @@ ...@@ -1165,16 +1176,5 @@
// the value where the selection starts or ends, respectively. // the value where the selection starts or ends, respectively.
[nocompile] static void setDocumentSelection( [nocompile] static void setDocumentSelection(
SetDocumentSelectionParams params); SetDocumentSelectionParams params);
// Returns the detected languages for the provided string attribute as an
// array of LanguageSpan objects. There are several guarantees about the
// format of the LanguageSpan array:
// 1. Is either empty or contains LanguageSpans that cover all indices in
// the associated string attribute value.
// 2. Is sorted by increasing startIndex (those with smaller startIndex
// appear first).
// 3. LanguageSpans are non-overlapping and contain exactly one language.
[nocompile] LanguageSpan[] languageAnnotationForStringAttribute(
DOMString attribute);
}; };
}; };
...@@ -148,12 +148,13 @@ chrome.accessibilityPrivate.FocusType = { ...@@ -148,12 +148,13 @@ chrome.accessibilityPrivate.FocusType = {
chrome.accessibilityPrivate.FocusRingInfo; chrome.accessibilityPrivate.FocusRingInfo;
/** /**
* Called to translate language code into human-readable string in the language * Called to translate languageCodeToTranslate into human-readable string in the
* of the provided language code. * language specified by targetLanguageCode
* @param {string} languageCode * @param {string} languageCodeToTranslate
* @param {string} targetLanguageCode
* @return {string} The human-readable language string in the provided language. * @return {string} The human-readable language string in the provided language.
*/ */
chrome.accessibilityPrivate.getDisplayLanguage = function(languageCode) {}; chrome.accessibilityPrivate.getDisplayLanguage = function(languageCodeToTranslate, targetLanguageCode) {};
/** /**
* Called to request battery status from Chrome OS system. * Called to request battery status from Chrome OS system.
...@@ -205,13 +206,6 @@ chrome.accessibilityPrivate.setKeyboardListener = function(enabled, capture) {}; ...@@ -205,13 +206,6 @@ chrome.accessibilityPrivate.setKeyboardListener = function(enabled, capture) {};
*/ */
chrome.accessibilityPrivate.darkenScreen = function(enabled) {}; chrome.accessibilityPrivate.darkenScreen = function(enabled) {};
/**
* Change the keyboard keys captured by Switch Access.
* @param {!Array<number>} key_codes The key codes for the keys that will be
* captured.
*/
chrome.accessibilityPrivate.setSwitchAccessKeys = function(key_codes) {};
/** /**
* Shows or hides the Switch Access menu. If shown, it is at the indicated * Shows or hides the Switch Access menu. If shown, it is at the indicated
* location. * location.
...@@ -283,11 +277,12 @@ chrome.accessibilityPrivate.toggleDictation = function() {}; ...@@ -283,11 +277,12 @@ chrome.accessibilityPrivate.toggleDictation = function() {};
chrome.accessibilityPrivate.setVirtualKeyboardVisible = function(isVisible) {}; chrome.accessibilityPrivate.setVirtualKeyboardVisible = function(isVisible) {};
/** /**
* Opens a settings subpage, specified by the portion of the page's URL after * Opens a specified settings subpage. To open a page with url
* "chrome://settings/" * chrome://settings/manageAccessibility/tts, pass in the substring
* 'manageAccessibility/tts'.
* @param {string} subpage * @param {string} subpage
*/ */
chrome.accessibilityPrivate.openSettingsSubpage = function (subpage) {} chrome.accessibilityPrivate.openSettingsSubpage = function(subpage) {};
/** /**
* Fired whenever ChromeVox should output introduction. * Fired whenever ChromeVox should output introduction.
......
...@@ -1719,6 +1719,19 @@ chrome.automation.AutomationNode.prototype.matches = function(params) {}; ...@@ -1719,6 +1719,19 @@ chrome.automation.AutomationNode.prototype.matches = function(params) {};
*/ */
chrome.automation.AutomationNode.prototype.getNextTextMatch = function(searchStr, backward) {}; chrome.automation.AutomationNode.prototype.getNextTextMatch = function(searchStr, backward) {};
/**
* Returns the detected languages for the provided string attribute as an array
* of LanguageSpan objects. There are several guarantees about the format of the
* LanguageSpan array: 1. Is either empty or contains LanguageSpans that cover
* all indices in the associated string attribute value. 2. Is sorted by
* increasing startIndex (those with smaller startIndex appear first). 3.
* LanguageSpans are non-overlapping and contain exactly one language.
* @param {string} attribute
* @return {!Array<!chrome.automation.LanguageSpan>}
* @see https://developer.chrome.com/extensions/automation#method-languageAnnotationForStringAttribute
*/
chrome.automation.AutomationNode.prototype.languageAnnotationForStringAttribute = function(attribute) {};
/** /**
* Get the automation tree for the tab with the given tabId, or the current tab * Get the automation tree for the tab with the given tabId, or the current tab
...@@ -1784,16 +1797,3 @@ chrome.automation.removeTreeChangeObserver = function(observer) {}; ...@@ -1784,16 +1797,3 @@ chrome.automation.removeTreeChangeObserver = function(observer) {};
* @see https://developer.chrome.com/extensions/automation#method-setDocumentSelection * @see https://developer.chrome.com/extensions/automation#method-setDocumentSelection
*/ */
chrome.automation.setDocumentSelection = function(params) {}; chrome.automation.setDocumentSelection = function(params) {};
/**
* Returns the detected languages for the provided string attribute as an array
* of LanguageSpan objects. There are several guarantees about the format of the
* LanguageSpan array: 1. Is either empty or contains LanguageSpans that cover
* all indices in the associated string attribute value. 2. Is sorted by
* increasing startIndex (those with smaller startIndex appear first). 3.
* LanguageSpans are non-overlapping and contain exactly one language.
* @param {string} attribute
* @return {!Array<!chrome.automation.LanguageSpan>}
* @see https://developer.chrome.com/extensions/automation#method-languageAnnotationForStringAttribute
*/
chrome.automation.languageAnnotationForStringAttribute = function(attribute) {};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment