blob: 7a62aabd1db68419d9f8b75c6d30b5edbe8cf18e [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview 'settings-languages' handles Chrome's language and input
* method settings. The 'languages' property, which reflects the current
* language settings, must not be changed directly. Instead, changes to
* language settings should be made using the LanguageHelper APIs provided by
* this class via languageHelper.
*/
import '../../prefs/prefs.js';
import {assert} from '//resources/js/assert.m.js';
import {loadTimeData} from '//resources/js/load_time_data.m.js';
import {PromiseResolver} from '//resources/js/promise_resolver.m.js';
import {html, mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {CrSettingsPrefs} from '../../prefs/prefs_types.js';
import {PrefsBehavior, PrefsBehaviorInterface} from '../prefs_behavior.js';
import {LanguagesBrowserProxy, LanguagesBrowserProxyImpl} from './languages_browser_proxy.js';
import {InputMethodsModel, LanguageHelper, LanguagesModel, LanguageState, SpellCheckLanguageState} from './languages_types.js';
const MoveType = chrome.languageSettingsPrivate.MoveType;
// Translate server treats some language codes the same.
// See also: components/translate/core/common/translate_util.cc.
const kLanguageCodeToTranslateCode = {
'nb': 'no',
'fil': 'tl',
'zh-HK': 'zh-TW',
'zh-MO': 'zh-TW',
'zh-SG': 'zh-CN',
};
// Some ISO 639 language codes have been renamed, e.g. "he" to "iw", but
// Translate still uses the old versions. TODO(michaelpg): Chrome does too.
// Follow up with Translate owners to understand the right thing to do.
const kTranslateLanguageSynonyms = {
'he': 'iw',
'jv': 'jw',
};
// The fake language name used for ARC IMEs. The value must be in sync with the
// one in ui/base/ime/ash/extension_ime_util.h.
const kArcImeLanguage = '_arc_ime_language_';
// The IME ID for the Accessibility Common extension used by Dictation.
/** @type {string} */
const ACCESSIBILITY_COMMON_IME_ID =
'_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
/**
* @typedef {{
* initialized: boolean,
* supportedLanguages: !Array<!chrome.languageSettingsPrivate.Language>,
* translateTarget: string,
* alwaysTranslateCodes: !Array<string>,
* neverTranslateCodes: !Array<string>,
* startingUILanguage: string,
* supportedInputMethods:
* (!Array<!chrome.languageSettingsPrivate.InputMethod>|undefined),
* currentInputMethodId: (string|undefined)
* }}
*/
let ModelArgs;
/**
* Singleton element that generates the languages model on start-up and
* updates it whenever Chrome's pref store and other settings change.
*/
/**
* @constructor
* @extends {PolymerElement}
* @implements {LanguageHelper}
* @implements {PrefsBehaviorInterface}
*/
const SettingsLanguagesElementBase =
mixinBehaviors([PrefsBehavior], PolymerElement);
/** @polymer */
class SettingsLanguagesElement extends SettingsLanguagesElementBase {
static get is() {
return 'settings-languages';
}
static get properties() {
return {
/**
* @type {!LanguagesModel|undefined}
*/
languages: {
type: Object,
notify: true,
readOnly: true,
},
/**
* This element, as a LanguageHelper instance for API usage.
* @type {!LanguageHelper}
*/
languageHelper: {
type: Object,
notify: true,
readOnly: true,
value() {
return /** @type {!LanguageHelper} */ (this);
},
},
/**
* PromiseResolver to be resolved when the singleton has been initialized.
* @private {!PromiseResolver}
*/
resolver_: {
type: Object,
value() {
return new PromiseResolver();
},
},
/**
* Hash map of supported languages by language codes for fast lookup.
* @private {!Map<string, !chrome.languageSettingsPrivate.Language>}
*/
supportedLanguageMap_: {
type: Object,
value() {
return new Map();
},
},
/**
* Hash set of enabled language codes for membership testing.
* @private {!Set<string>}
*/
enabledLanguageSet_: {
type: Object,
value() {
return new Set();
},
},
/**
* Hash map of supported input methods by ID for fast lookup.
* @private {!Map<string, chrome.languageSettingsPrivate.InputMethod>}
*/
supportedInputMethodMap_: {
type: Object,
value() {
return new Map();
},
},
/**
* Hash map of input methods supported for each language.
* @type {!Map<string,
* !Array<!chrome.languageSettingsPrivate.InputMethod>>}
* @private
*/
languageInputMethods_: {
type: Object,
value() {
return new Map();
},
},
/**
* Hash set of enabled input methods id for mebership testings
* @private {!Set<string>}
*/
enabledInputMethodSet_: {
type: Object,
value() {
return new Set();
}
},
/** @private Prospective UI language when the page was loaded. */
originalProspectiveUILanguage_: String,
};
}
static get observers() {
return [
// All observers wait for the model to be populated by including the
// |languages| property.
'alwaysTranslateLanguagesPrefChanged_(' +
'prefs.translate_whitelists.value.*, languages)',
'neverTranslateLanguagesPrefChanged_(' +
'prefs.translate_blocked_languages.value.*, languages)',
'prospectiveUILanguageChanged_(prefs.intl.app_locale.value, languages)',
'preferredLanguagesPrefChanged_(' +
'prefs.intl.accept_languages.value, languages)',
'preferredLanguagesPrefChanged_(' +
'prefs.intl.forced_languages.value.*, languages)',
'spellCheckDictionariesPrefChanged_(' +
'prefs.spellcheck.dictionaries.value.*, ' +
'prefs.spellcheck.forced_dictionaries.value.*, ' +
'prefs.spellcheck.blocked_dictionaries.value.*, languages)',
'translateLanguagesPrefChanged_(' +
'prefs.translate_blocked_languages.value.*, languages)',
'translateTargetPrefChanged_(' +
'prefs.translate_recent_target.value, languages)',
'updateRemovableLanguages_(' +
'prefs.intl.app_locale.value, languages.enabled)',
'updateRemovableLanguages_(' +
'prefs.translate_blocked_languages.value.*)',
// Observe Chrome OS prefs (ignored for non-Chrome OS).
'updateRemovableLanguages_(' +
'prefs.settings.language.preload_engines.value, ' +
'prefs.settings.language.enabled_extension_imes.value, ' +
'languages)',
];
}
constructor() {
super();
/** @private {?Function} */
this.boundOnSpellcheckDictionariesChanged_ = null;
/** @private {!LanguagesBrowserProxy} */
this.browserProxy_ = LanguagesBrowserProxyImpl.getInstance();
/** @private {!LanguageSettingsPrivate} */
this.languageSettingsPrivate_ =
this.browserProxy_.getLanguageSettingsPrivate();
/** @private {!InputMethodPrivate} */
this.inputMethodPrivate_ = this.browserProxy_.getInputMethodPrivate();
/** @private {?Function} */
this.boundOnInputMethodAdded_ = null;
/** @private {?Function} */
this.boundOnInputMethodRemoved_ = null;
/** @private {?Function} */
this.boundOnInputMethodChanged_ = null;
}
/** @override */
connectedCallback() {
super.connectedCallback();
const promises = [];
/**
* An object passed into createModel to keep track of platform-specific
* arguments, populated by the "promises" array.
* @type {!ModelArgs}
*/
const args = {
initialized: false,
supportedLanguages: [],
translateTarget: '',
alwaysTranslateCodes: [],
neverTranslateCodes: [],
startingUILanguage: '',
// Only used by ChromeOS
supportedInputMethods: [],
currentInputMethodId: '',
};
// Wait until prefs are initialized before creating the model, so we can
// include information about enabled languages.
promises.push(
CrSettingsPrefs.initialized.then(result => args.initialized = result));
// Get the language list.
promises.push(new Promise(resolve => {
this.languageSettingsPrivate_.getLanguageList(resolve);
}).then(result => args.supportedLanguages = result));
// Get the translate target language.
promises.push(new Promise(resolve => {
this.languageSettingsPrivate_.getTranslateTargetLanguage(
resolve);
}).then(result => args.translateTarget = result));
promises.push(
new Promise(resolve => {
this.languageSettingsPrivate_.getInputMethodLists(function(lists) {
resolve(lists.componentExtensionImes.concat(
lists.thirdPartyExtensionImes));
});
}).then(result => args.supportedInputMethods = result));
promises.push(new Promise(resolve => {
this.inputMethodPrivate_.getCurrentInputMethod(resolve);
}).then(result => args.currentInputMethodId = result));
// Get the list of language-codes to always translate.
promises.push(new Promise(resolve => {
this.languageSettingsPrivate_.getAlwaysTranslateLanguages(
resolve);
}).then(result => args.alwaysTranslateCodes = result));
// Get the list of language-codes to never translate.
promises.push(new Promise(resolve => {
this.languageSettingsPrivate_.getNeverTranslateLanguages(
resolve);
}).then(result => args.neverTranslateCodes = result));
// Fetch the starting UI language, which affects which actions should be
// enabled.
promises.push(this.browserProxy_.getProspectiveUILanguage().then(
prospectiveUILanguage => {
this.originalProspectiveUILanguage_ =
prospectiveUILanguage || window.navigator.language;
}));
Promise.all(promises).then(results => {
if (!this.isConnected) {
// Return early if this element was detached from the DOM before
// this async callback executes (can happen during testing).
return;
}
this.createModel_(args);
this.boundOnSpellcheckDictionariesChanged_ =
this.onSpellcheckDictionariesChanged_.bind(this);
this.languageSettingsPrivate_.onSpellcheckDictionariesChanged.addListener(
this.boundOnSpellcheckDictionariesChanged_);
this.languageSettingsPrivate_.getSpellcheckDictionaryStatuses(
this.boundOnSpellcheckDictionariesChanged_);
this.resolver_.resolve();
});
this.boundOnInputMethodChanged_ = this.onInputMethodChanged_.bind(this);
this.inputMethodPrivate_.onChanged.addListener(
assert(this.boundOnInputMethodChanged_));
this.boundOnInputMethodAdded_ = this.onInputMethodAdded_.bind(this);
this.languageSettingsPrivate_.onInputMethodAdded.addListener(
this.boundOnInputMethodAdded_);
this.boundOnInputMethodRemoved_ = this.onInputMethodRemoved_.bind(this);
this.languageSettingsPrivate_.onInputMethodRemoved.addListener(
this.boundOnInputMethodRemoved_);
}
/** @override */
disconnectedCallback() {
super.disconnectedCallback();
this.inputMethodPrivate_.onChanged.removeListener(
assert(this.boundOnInputMethodChanged_));
this.boundOnInputMethodChanged_ = null;
this.languageSettingsPrivate_.onInputMethodAdded.removeListener(
assert(this.boundOnInputMethodAdded_));
this.boundOnInputMethodAdded_ = null;
this.languageSettingsPrivate_.onInputMethodRemoved.removeListener(
assert(this.boundOnInputMethodRemoved_));
this.boundOnInputMethodRemoved_ = null;
if (this.boundOnSpellcheckDictionariesChanged_) {
this.languageSettingsPrivate_.onSpellcheckDictionariesChanged
.removeListener(this.boundOnSpellcheckDictionariesChanged_);
this.boundOnSpellcheckDictionariesChanged_ = null;
}
}
/**
* Updates the prospective UI language based on the new pref value.
* @param {string} prospectiveUILanguage
* @private
*/
prospectiveUILanguageChanged_(prospectiveUILanguage) {
this.set(
'languages.prospectiveUILanguage',
prospectiveUILanguage || this.originalProspectiveUILanguage_);
}
/**
* Updates the list of enabled languages from the preferred languages pref.
* @private
*/
preferredLanguagesPrefChanged_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
const enabledLanguageStates = this.getEnabledLanguageStates_(
this.languages.translateTarget, this.languages.prospectiveUILanguage);
// Recreate the enabled language set before updating languages.enabled.
this.enabledLanguageSet_.clear();
for (let i = 0; i < enabledLanguageStates.length; i++) {
this.enabledLanguageSet_.add(enabledLanguageStates[i].language.code);
}
this.set('languages.enabled', enabledLanguageStates);
if (this.boundOnSpellcheckDictionariesChanged_) {
this.languageSettingsPrivate_.getSpellcheckDictionaryStatuses(
this.boundOnSpellcheckDictionariesChanged_);
}
// Update translate target language.
new Promise(resolve => {
this.languageSettingsPrivate_.getTranslateTargetLanguage(resolve);
}).then(result => {
this.set('languages.translateTarget', result);
});
}
/**
* Updates the spellCheckEnabled state of each enabled language.
* @private
*/
spellCheckDictionariesPrefChanged_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
const spellCheckSet = this.makeSetFromArray_(/** @type {!Array<string>} */ (
this.getPref('spellcheck.dictionaries').value));
const spellCheckForcedSet =
this.makeSetFromArray_(/** @type {!Array<string>} */ (
this.getPref('spellcheck.forced_dictionaries').value));
const spellCheckBlockedSet =
this.makeSetFromArray_(/** @type {!Array<string>} */ (
this.getPref('spellcheck.blocked_dictionaries').value));
for (let i = 0; i < this.languages.enabled.length; i++) {
const languageState = this.languages.enabled[i];
const isUser = spellCheckSet.has(languageState.language.code);
const isForced = spellCheckForcedSet.has(languageState.language.code);
const isBlocked = spellCheckBlockedSet.has(languageState.language.code);
this.set(
`languages.enabled.${i}.spellCheckEnabled`,
(isUser && !isBlocked) || isForced);
this.set(`languages.enabled.${i}.isManaged`, isForced || isBlocked);
}
const {on: spellCheckOnLanguages, off: spellCheckOffLanguages} =
this.getSpellCheckLanguages_(this.languages.supported);
this.set('languages.spellCheckOnLanguages', spellCheckOnLanguages);
this.set('languages.spellCheckOffLanguages', spellCheckOffLanguages);
}
/**
* Returns two arrays of SpellCheckLanguageStates for spell check languages:
* one for spell check on, one for spell check off.
* @param {!Array<!chrome.languageSettingsPrivate.Language>}
* supportedLanguages The list of supported languages, normally
* this.languages.supported.
* @return {{on: !Array<SpellCheckLanguageState>, off:
* !Array<SpellCheckLanguageState>}}
* @private
*/
getSpellCheckLanguages_(supportedLanguages) {
// The spell check preferences are prioritised in this order:
// forced_dictionaries, blocked_dictionaries, dictionaries.
// The set of all language codes seen thus far.
const /** !Set<string> */ seenCodes = new Set();
/**
* Gets the list of language codes indicated by the preference name, and
* de-duplicates it with all other language codes.
* @param {string} prefName
* @return {!Array<string>}
*/
const getPrefAndDedupe = prefName => {
const /** !Array<string> */ result =
this.getPref(prefName).value.filter(x => !seenCodes.has(x));
result.forEach(code => seenCodes.add(code));
return result;
};
const forcedCodes = getPrefAndDedupe('spellcheck.forced_dictionaries');
const forcedCodesSet = new Set(forcedCodes);
const blockedCodes = getPrefAndDedupe('spellcheck.blocked_dictionaries');
const blockedCodesSet = new Set(blockedCodes);
const enabledCodes = getPrefAndDedupe('spellcheck.dictionaries');
const /** !Array<SpellCheckLanguageState> */ on = [];
// We want to add newly enabled languages to the end of the "on" list, so we
// should explicitly move the forced languages to the front of the list.
for (const code of [...forcedCodes, ...enabledCodes]) {
const language = this.supportedLanguageMap_.get(code);
// language could be undefined if code is not in supportedLanguageMap_.
// This should be rare, but could happen if supportedLanguageMap_ is
// missing languages or the prefs are manually modified. We want to fail
// gracefully if this happens - throwing an error here would cause
// language settings to not load.
if (language) {
on.push({
language,
isManaged: forcedCodesSet.has(code),
spellCheckEnabled: true,
downloadDictionaryStatus: null,
downloadDictionaryFailureCount: 0,
});
}
}
// Because the list of "spell check supported" languages is only exposed
// through "supported languages", we need to filter that list along with
// whether we've seen the language before.
// We don't want to split this list in "forced" / "not-forced" like the
// spell check on list above, as we don't want to explicitly surface / hide
// blocked languages to the user.
const /** !Array<SpellCheckLanguageState> */ off = [];
for (const language of supportedLanguages) {
// If spell check is off for this language, it must either not be in any
// spell check pref, or be in the blocked dictionaries pref.
if (language.supportsSpellcheck &&
(!seenCodes.has(language.code) ||
blockedCodesSet.has(language.code))) {
off.push({
language,
isManaged: blockedCodesSet.has(language.code),
spellCheckEnabled: false,
downloadDictionaryStatus: null,
downloadDictionaryFailureCount: 0
});
}
}
return {
on,
off,
};
}
/**
* Updates the list of always translate languages from translate prefs.
* @private
*/
alwaysTranslateLanguagesPrefChanged_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
const alwaysTranslateCodes = Object.keys(
/** @type {!Object} */ (this.getPref('translate_whitelists').value));
const alwaysTranslateLanguages =
alwaysTranslateCodes.map(code => this.getLanguage(code));
this.set('languages.alwaysTranslate', alwaysTranslateLanguages);
}
/**
* Updates the list of never translate languages from translate prefs.
* @private
*/
neverTranslateLanguagesPrefChanged_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
const neverTranslateCodes =
/** @type {!Object} */ (
this.getPref('translate_blocked_languages').value);
const neverTranslateLanguages =
neverTranslateCodes.map(code => this.getLanguage(code));
this.set('languages.neverTranslate', neverTranslateLanguages);
}
/** @private */
translateLanguagesPrefChanged_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
const translateBlockedPref = this.getPref('translate_blocked_languages');
const translateBlockedSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (translateBlockedPref.value));
for (let i = 0; i < this.languages.enabled.length; i++) {
const language = this.languages.enabled[i].language;
const translateEnabled = this.isTranslateEnabled_(
language.code, !!language.supportsTranslate, translateBlockedSet,
this.languages.translateTarget, this.languages.prospectiveUILanguage);
this.set(
'languages.enabled.' + i + '.translateEnabled', translateEnabled);
}
}
/** @private */
translateTargetPrefChanged_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
this.set(
'languages.translateTarget',
this.getPref('translate_recent_target').value);
}
/**
* Constructs the languages model.
* @param {!ModelArgs} args used to populate the model
* above.
* @private
*/
createModel_(args) {
// Populate the hash map of supported languages.
for (let i = 0; i < args.supportedLanguages.length; i++) {
const language = args.supportedLanguages[i];
language.supportsUI = !!language.supportsUI;
language.supportsTranslate = !!language.supportsTranslate;
language.supportsSpellcheck = !!language.supportsSpellcheck;
language.isProhibitedLanguage = !!language.isProhibitedLanguage;
this.supportedLanguageMap_.set(language.code, language);
}
let prospectiveUILanguage;
// eslint-disable-next-line prefer-const
prospectiveUILanguage =
/** @type {string} */ (this.getPref('intl.app_locale').value) ||
this.originalProspectiveUILanguage_;
// Create a list of enabled languages from the supported languages.
const enabledLanguageStates = this.getEnabledLanguageStates_(
args.translateTarget, prospectiveUILanguage);
// Populate the hash set of enabled languages.
for (let l = 0; l < enabledLanguageStates.length; l++) {
this.enabledLanguageSet_.add(enabledLanguageStates[l].language.code);
}
const {on: spellCheckOnLanguages, off: spellCheckOffLanguages} =
this.getSpellCheckLanguages_(args.supportedLanguages);
const alwaysTranslateLanguages =
args.alwaysTranslateCodes.map(code => this.getLanguage(code));
const neverTranslateLangauges =
args.neverTranslateCodes.map(code => this.getLanguage(code));
const model = /** @type {!LanguagesModel} */ ({
supported: args.supportedLanguages,
enabled: enabledLanguageStates,
translateTarget: args.translateTarget,
alwaysTranslate: alwaysTranslateLanguages,
neverTranslate: neverTranslateLangauges,
spellCheckOnLanguages,
spellCheckOffLanguages,
});
model.prospectiveUILanguage = prospectiveUILanguage;
if (args.supportedInputMethods) {
this.createInputMethodModel_(args.supportedInputMethods);
}
model.inputMethods = /** @type {!InputMethodsModel} */ ({
supported: args.supportedInputMethods,
enabled: this.getEnabledInputMethods_(),
currentId: args.currentInputMethodId,
});
// Initialize the Polymer languages model.
this._setLanguages(model);
}
/**
* Returns a list of LanguageStates for each enabled language in the supported
* languages list.
* @param {string} translateTarget Language code of the default translate
* target language.
* @param {(string|undefined)} prospectiveUILanguage Prospective UI display
* language. Only defined on Windows and Chrome OS.
* @return {!Array<!LanguageState>}
* @private
*/
getEnabledLanguageStates_(translateTarget, prospectiveUILanguage) {
assert(CrSettingsPrefs.isInitialized);
const pref = this.getPref('intl.accept_languages');
const enabledLanguageCodes = pref.value.split(',');
const languagesForcedPref = this.getPref('intl.forced_languages');
const spellCheckPref = this.getPref('spellcheck.dictionaries');
const spellCheckForcedPref = this.getPref('spellcheck.forced_dictionaries');
const spellCheckBlockedPref =
this.getPref('spellcheck.blocked_dictionaries');
const languageForcedSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (languagesForcedPref.value));
const spellCheckSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (
spellCheckPref.value.concat(spellCheckForcedPref.value)));
const spellCheckForcedSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (spellCheckForcedPref.value));
const spellCheckBlockedSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (spellCheckBlockedPref.value));
const translateBlockedPref = this.getPref('translate_blocked_languages');
const translateBlockedSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (translateBlockedPref.value));
const enabledLanguageStates = [];
for (let i = 0; i < enabledLanguageCodes.length; i++) {
const code = enabledLanguageCodes[i];
const language = this.supportedLanguageMap_.get(code);
// Skip unsupported languages.
if (!language) {
continue;
}
const languageState = /** @type {LanguageState} */ ({});
languageState.language = language;
languageState.spellCheckEnabled =
spellCheckSet.has(code) && !spellCheckBlockedSet.has(code) ||
spellCheckForcedSet.has(code);
languageState.translateEnabled = this.isTranslateEnabled_(
code, !!language.supportsTranslate, translateBlockedSet,
translateTarget, prospectiveUILanguage);
languageState.isManaged =
spellCheckForcedSet.has(code) || spellCheckBlockedSet.has(code);
languageState.isForced = languageForcedSet.has(code);
languageState.downloadDictionaryFailureCount = 0;
enabledLanguageStates.push(languageState);
}
return enabledLanguageStates;
}
/**
* True iff we translate pages that are in the given language.
* @param {string} code Language code.
* @param {boolean} supportsTranslate If translation supports the given
* language.
* @param {!Set<string>} translateBlockedSet Set of languages for which
* translation is blocked.
* @param {string} translateTarget Language code of the default translate
* target language.
* @param {(string|undefined)} prospectiveUILanguage Prospective UI display
* language. Only defined on Windows and Chrome OS.
* @return {boolean}
* @private
*/
isTranslateEnabled_(
code, supportsTranslate, translateBlockedSet, translateTarget,
prospectiveUILanguage) {
const translateCode = this.convertLanguageCodeForTranslate(code);
return supportsTranslate && !translateBlockedSet.has(translateCode) &&
translateCode !== translateTarget &&
(!prospectiveUILanguage || code !== prospectiveUILanguage);
}
/**
* Updates the dictionary download status for spell check languages in order
* to track the number of times a spell check dictionary download has failed.
* @param {!Array<!chrome.languageSettingsPrivate.SpellcheckDictionaryStatus>}
* statuses
* @private
*/
onSpellcheckDictionariesChanged_(statuses) {
const statusMap = new Map();
statuses.forEach(status => {
statusMap.set(status.languageCode, status);
});
const collectionNames =
['enabled', 'spellCheckOnLanguages', 'spellCheckOffLanguages'];
collectionNames.forEach(collectionName => {
this.languages[collectionName].forEach((languageState, index) => {
const status = statusMap.get(languageState.language.code);
if (!status) {
return;
}
const previousStatus = languageState.downloadDictionaryStatus;
const keyPrefix = `languages.${collectionName}.${index}`;
this.set(`${keyPrefix}.downloadDictionaryStatus`, status);
const failureCountKey = `${keyPrefix}.downloadDictionaryFailureCount`;
if (status.downloadFailed &&
!(previousStatus && previousStatus.downloadFailed)) {
const failureCount = languageState.downloadDictionaryFailureCount + 1;
this.set(failureCountKey, failureCount);
} else if (
status.isReady && !(previousStatus && previousStatus.isReady)) {
this.set(failureCountKey, 0);
}
});
});
}
/**
* Updates the |removable| property of the enabled language states based
* on what other languages and input methods are enabled.
* @private
*/
updateRemovableLanguages_() {
if (this.prefs === undefined || this.languages === undefined) {
return;
}
// TODO(michaelpg): Enabled input methods can affect which languages are
// removable, so run updateEnabledInputMethods_ first (if it has been
// scheduled).
this.updateEnabledInputMethods_();
for (let i = 0; i < this.languages.enabled.length; i++) {
const languageState = this.languages.enabled[i];
this.set(
'languages.enabled.' + i + '.removable',
this.canDisableLanguage(languageState));
}
}
/**
* Creates a Set from the elements of the array.
* @param {!Array<T>} list
* @return {!Set<T>}
* @template T
* @private
*/
makeSetFromArray_(list) {
return new Set(list);
}
// LanguageHelper implementation.
// TODO(michaelpg): replace duplicate docs with @override once b/24294625
// is fixed.
/** @return {!Promise} */
whenReady() {
return this.resolver_.promise;
}
/**
* Sets the prospective UI language to the chosen language. This won't affect
* the actual UI language until a restart.
* @param {string} languageCode
*/
setProspectiveUILanguage(languageCode) {
this.browserProxy_.setProspectiveUILanguage(languageCode);
}
/**
* True if the prospective UI language was changed from its starting value.
* @return {boolean}
*/
requiresRestart() {
return this.originalProspectiveUILanguage_ !==
this.languages.prospectiveUILanguage;
}
/**
* @return {string} The language code for ARC IMEs.
*/
getArcImeLanguageCode() {
return kArcImeLanguage;
}
/**
* @param {string} languageCode
* @return {boolean} True if the language is for ARC IMEs.
*/
isLanguageCodeForArcIme(languageCode) {
return languageCode === kArcImeLanguage;
}
/**
* @param {!chrome.languageSettingsPrivate.Language} language
* @return {boolean} True if the language can be translated by Chrome.
*/
isLanguageTranslatable(language) {
if (language.code === 'zh-CN' || language.code === 'zh-TW') {
// In Translate, general Chinese is not used, and the sub code is
// necessary as a language code for the Translate server.
return true;
}
if (language.code === this.getLanguageCodeWithoutRegion(language.code) &&
language.supportsTranslate) {
return true;
}
return false;
}
/**
* @param {string} languageCode
* @return {boolean} True if the language is enabled.
*/
isLanguageEnabled(languageCode) {
return this.enabledLanguageSet_.has(languageCode);
}
/**
* Enables the language, making it available for spell check and input.
* @param {string} languageCode
*/
enableLanguage(languageCode) {
if (!CrSettingsPrefs.isInitialized) {
return;
}
this.languageSettingsPrivate_.enableLanguage(languageCode);
}
/**
* Disables the language.
* @param {string} languageCode
*/
disableLanguage(languageCode) {
if (!CrSettingsPrefs.isInitialized) {
return;
}
// For CrOS language settings V2 update 2, languages and spell check are
// decoupled so there's no need to remove the language from spell check.
if (!this.isChromeOSLanguageSettingsV2Update2_()) {
this.deletePrefListItem('spellcheck.dictionaries', languageCode);
}
// Remove the language from preferred languages.
this.languageSettingsPrivate_.disableLanguage(languageCode);
}
/**
* @return {boolean}
* @private
*/
isChromeOSLanguageSettingsV2Update2_() {
return loadTimeData.valueExists('enableLanguageSettingsV2Update2') &&
loadTimeData.getBoolean('enableLanguageSettingsV2Update2');
}
/**
* @param {!LanguageState} languageState
* @return {boolean}
*/
isOnlyTranslateBlockedLanguage(languageState) {
return !languageState.translateEnabled &&
this.languages.enabled.filter(lang => !lang.translateEnabled).length ===
1;
}
/**
* @param {!LanguageState} languageState
* @return {boolean}
*/
canDisableLanguage(languageState) {
// Cannot disable the only enabled language.
if (this.languages.enabled.length === 1) {
return false;
}
// Cannot disable the last translate blocked language.
if (this.isOnlyTranslateBlockedLanguage(languageState)) {
return false;
}
return true;
}
/**
* @param {!chrome.languageSettingsPrivate.Language} language
* @return {boolean} true if the given language can be enabled
*/
canEnableLanguage(language) {
return !(
this.isLanguageEnabled(language.code) ||
language.isProhibitedLanguage ||
this.isLanguageCodeForArcIme(language.code) /* internal use only */);
}
/**
* Sets whether a given language should always be automatically translated.
* @param {string} languageCode
* @param {boolean} alwaysTranslate
*/
setLanguageAlwaysTranslateState(languageCode, alwaysTranslate) {
this.languageSettingsPrivate_.setLanguageAlwaysTranslateState(
languageCode, alwaysTranslate);
}
/**
* Moves the language in the list of enabled languages either up (toward the
* front of the list) or down (toward the back).
* @param {string} languageCode
* @param {boolean} upDirection True if we need to move up, false if we
* need to move down
*/
moveLanguage(languageCode, upDirection) {
if (!CrSettingsPrefs.isInitialized) {
return;
}
if (upDirection) {
this.languageSettingsPrivate_.moveLanguage(languageCode, MoveType.UP);
} else {
this.languageSettingsPrivate_.moveLanguage(languageCode, MoveType.DOWN);
}
}
/**
* Moves the language directly to the front of the list of enabled languages.
* @param {string} languageCode
*/
moveLanguageToFront(languageCode) {
if (!CrSettingsPrefs.isInitialized) {
return;
}
this.languageSettingsPrivate_.moveLanguage(languageCode, MoveType.TOP);
}
/**
* Enables translate for the given language by removing the translate
* language from the blocked languages preference.
* @param {string} languageCode
*/
enableTranslateLanguage(languageCode) {
this.languageSettingsPrivate_.setEnableTranslationForLanguage(
languageCode, true);
}
/**
* Disables translate for the given language by adding the translate
* language to the blocked languages preference.
* @param {string} languageCode
*/
disableTranslateLanguage(languageCode) {
this.languageSettingsPrivate_.setEnableTranslationForLanguage(
languageCode, false);
}
/**
* Sets the translate target language and adds it to the content languages if
* not already there.
* @param {string} languageCode
*/
setTranslateTargetLanguage(languageCode) {
this.languageSettingsPrivate_.setTranslateTargetLanguage(languageCode);
}
/**
* Enables or disables spell check for the given language.
* @param {string} languageCode
* @param {boolean} enable
*/
toggleSpellCheck(languageCode, enable) {
if (!this.languages) {
return;
}
if (enable) {
const spellCheckPref = this.getPref('spellcheck.dictionaries');
this.appendPrefListItem('spellcheck.dictionaries', languageCode);
} else {
this.deletePrefListItem('spellcheck.dictionaries', languageCode);
}
}
/**
* Converts the language code for translate. There are some differences
* between the language set the Translate server uses and that for
* Accept-Language.
* @param {string} languageCode
* @return {string} The converted language code.
*/
convertLanguageCodeForTranslate(languageCode) {
if (languageCode in kLanguageCodeToTranslateCode) {
return kLanguageCodeToTranslateCode[languageCode];
}
const main = languageCode.split('-')[0];
if (main === 'zh') {
// In Translate, general Chinese is not used, and the sub code is
// necessary as a language code for the Translate server.
return languageCode;
}
if (main in kTranslateLanguageSynonyms) {
return kTranslateLanguageSynonyms[main];
}
return main;
}
/**
* Given a language code, returns just the base language. E.g., converts
* 'en-GB' to 'en'.
* @param {string} languageCode
* @return {string}
*/
getLanguageCodeWithoutRegion(languageCode) {
// The Norwegian languages fall under the 'no' macrolanguage.
if (languageCode === 'nb' || languageCode === 'nn') {
return 'no';
}
// The installer still uses the old language code "iw", instead of "he",
// for Hebrew. It needs to be converted to "he", otherwise it will not be
// found in supportedLanguageMap_.
//
// Note that this value is saved in the user's local state. Even
// if the installer is changed to use "he", because the installer does not
// overwrite this value, the conversion is still needed for old users.
if (languageCode === 'iw') {
return 'he';
}
// Match the characters before the hyphen.
const result = languageCode.match(/^([^-]+)-?/);
assert(result.length === 2);
return result[1];
}
/**
* @param {string} languageCode
* @return {!chrome.languageSettingsPrivate.Language|undefined}
*/
getLanguage(languageCode) {
// If a languageCode is not found, try language without location.
return this.supportedLanguageMap_.get(languageCode) ||
this.supportedLanguageMap_.get(
this.getLanguageCodeWithoutRegion(languageCode));
}
/**
* Retries downloading the dictionary for |languageCode|.
* @param {string} languageCode
*/
retryDownloadDictionary(languageCode) {
this.languageSettingsPrivate_.retryDownloadDictionary(languageCode);
}
/**
* Constructs the input method part of the languages model.
* @param {!Array<!chrome.languageSettingsPrivate.InputMethod>}
* supportedInputMethods Input methods.
* @private
*/
createInputMethodModel_(supportedInputMethods) {
// Populate the hash map of supported input methods.
this.supportedInputMethodMap_.clear();
this.languageInputMethods_.clear();
for (let j = 0; j < supportedInputMethods.length; j++) {
const inputMethod = supportedInputMethods[j];
inputMethod.enabled = !!inputMethod.enabled;
inputMethod.isProhibitedByPolicy = !!inputMethod.isProhibitedByPolicy;
// Add the input method to the map of IDs.
this.supportedInputMethodMap_.set(inputMethod.id, inputMethod);
// Add the input method to the list of input methods for each language
// it supports.
for (let k = 0; k < inputMethod.languageCodes.length; k++) {
const languageCode = inputMethod.languageCodes[k];
if (!this.supportedLanguageMap_.has(languageCode)) {
continue;
}
if (!this.languageInputMethods_.has(languageCode)) {
this.languageInputMethods_.set(languageCode, [inputMethod]);
} else {
this.languageInputMethods_.get(languageCode).push(inputMethod);
}
}
}
}
/**
* Returns a list of enabled input methods.
* @return {!Array<!chrome.languageSettingsPrivate.InputMethod>}
* @private
*/
getEnabledInputMethods_() {
assert(CrSettingsPrefs.isInitialized);
let enabledInputMethodIds =
this.getPref('settings.language.preload_engines').value.split(',');
enabledInputMethodIds = enabledInputMethodIds.concat(
this.getPref('settings.language.enabled_extension_imes')
.value.split(','));
this.enabledInputMethodSet_ = new Set(enabledInputMethodIds);
// Return only supported input methods. Don't include the Dictation
// (Accessibility Common) input method.
return enabledInputMethodIds
.map(id => this.supportedInputMethodMap_.get(id))
.filter(function(inputMethod) {
return !!inputMethod &&
inputMethod.id !== ACCESSIBILITY_COMMON_IME_ID;
});
}
/** @private */
updateSupportedInputMethods_() {
const promise = new Promise(resolve => {
this.languageSettingsPrivate_.getInputMethodLists(function(lists) {
resolve(
lists.componentExtensionImes.concat(lists.thirdPartyExtensionImes));
});
});
promise.then(result => {
const supportedInputMethods = result;
this.createInputMethodModel_(supportedInputMethods);
this.set('languages.inputMethods.supported', supportedInputMethods);
this.updateEnabledInputMethods_();
});
}
/** @private */
updateEnabledInputMethods_() {
const enabledInputMethods = this.getEnabledInputMethods_();
const enabledInputMethodSet = this.makeSetFromArray_(enabledInputMethods);
for (let i = 0; i < this.languages.inputMethods.supported.length; i++) {
this.set(
'languages.inputMethods.supported.' + i + '.enabled',
enabledInputMethodSet.has(this.languages.inputMethods.supported[i]));
}
this.set('languages.inputMethods.enabled', enabledInputMethods);
}
/** @param {string} id */
addInputMethod(id) {
if (!this.supportedInputMethodMap_.has(id)) {
return;
}
this.languageSettingsPrivate_.addInputMethod(id);
}
/** @param {string} id */
removeInputMethod(id) {
if (!this.supportedInputMethodMap_.has(id)) {
return;
}
this.languageSettingsPrivate_.removeInputMethod(id);
}
/** @param {string} id */
setCurrentInputMethod(id) {
this.inputMethodPrivate_.setCurrentInputMethod(id);
}
/**
* @param {string} languageCode
* @return {!Array<!chrome.languageSettingsPrivate.InputMethod>}
*/
getInputMethodsForLanguage(languageCode) {
return this.languageInputMethods_.get(languageCode) || [];
}
/**
* Returns the input methods that support any of the given languages.
* @param {!Array<string>} languageCodes
* @return {!Array<!chrome.languageSettingsPrivate.InputMethod>}
*/
getInputMethodsForLanguages(languageCodes) {
// Input methods that have already been listed for this language.
const /** !Set<string> */ usedInputMethods = new Set();
/** @type {!Array<chrome.languageSettingsPrivate.InputMethod>} */
const combinedInputMethods = [];
for (const languageCode of languageCodes) {
const inputMethods = this.getInputMethodsForLanguage(languageCode);
// Get the language's unused input methods and mark them as used.
const newInputMethods = inputMethods.filter(
inputMethod => !usedInputMethods.has(inputMethod.id));
newInputMethods.forEach(
inputMethod => usedInputMethods.add(inputMethod.id));
combinedInputMethods.push(...newInputMethods);
}
return combinedInputMethods;
}
/**
* @return {!Set<string>} list of enabled language code.
*/
getEnabledLanguageCodes() {
return this.enabledLanguageSet_;
}
/**
* @param {string} id the input method id
* @return {boolean} True if the input method is enabled
*/
isInputMethodEnabled(id) {
return this.enabledInputMethodSet_.has(id);
}
/**
* @param {!chrome.languageSettingsPrivate.InputMethod} inputMethod
* @return {boolean}
*/
isComponentIme(inputMethod) {
return inputMethod.id.startsWith('_comp_');
}
/** @param {string} id Input method ID. */
openInputMethodOptions(id) {
this.inputMethodPrivate_.openOptionsPage(id);
}
/** @param {string} id New current input method ID. */
onInputMethodChanged_(id) {
this.set('languages.inputMethods.currentId', id);
}
/** @param {string} id Added input method ID. */
onInputMethodAdded_(id) {
this.updateSupportedInputMethods_();
}
/** @param {string} id Removed input method ID. */
onInputMethodRemoved_(id) {
this.updateSupportedInputMethods_();
}
/**
* @param {string} id Input method ID.
* @return {string}
*/
getInputMethodDisplayName(id) {
const inputMethod = this.supportedInputMethodMap_.get(id);
if (inputMethod === undefined) {
return '';
}
return inputMethod.displayName;
}
}
customElements.define(SettingsLanguagesElement.is, SettingsLanguagesElement);