blob: 4c2cb22e0415f8e5f433b10151b59160eb03ff7b [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' provides convenient access to
* Chrome's language and input method settings.
*
* Instances of this element have a 'languages' property, which reflects the
* current language settings. The 'languages' property is read-only, meaning
* hosts using this element cannot change it directly. Instead, changes to
* language settings should be made using the LanguageHelperImpl singleton.
*
* Use upward binding syntax to propagate changes from child to host, so that
* changes made internally to 'languages' propagate to your host element:
*
* <template>
* <settings-languages languages="{{languages}}">
* </settings-languages>
* <div>[[languages.someProperty]]</div>
* </template>
*/
var SettingsLanguagesSingletonElement;
(function() {
'use strict';
// Translate server treats some language codes the same.
// See also: components/translate/core/common/translate_util.cc.
var 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.
var kTranslateLanguageSynonyms = {
'he': 'iw',
'jv': 'jw',
};
var preferredLanguagesPrefName = cr.isChromeOS ?
'settings.language.preferred_languages' : 'intl.accept_languages';
/**
* Singleton element that generates the languages model on start-up and
* updates it whenever Chrome's pref store and other settings change. These
* updates propagate to each <settings-language> instance so that their
* 'languages' property updates like any other Polymer property.
* @implements {LanguageHelper}
*/
SettingsLanguagesSingletonElement = Polymer({
is: 'settings-languages-singleton',
behaviors: [PrefsBehavior],
properties: {
/**
* @type {LanguagesModel|undefined}
*/
languages: {
type: Object,
notify: true,
},
/**
* Object containing all preferences.
*/
prefs: {
type: Object,
notify: true,
},
},
/**
* Hash map of languages.supportedLanguages using language codes as keys for
* fast lookup.
* @private {!Object<!chrome.languageSettingsPrivate.Language>}
*/
supportedLanguageMap_: {},
/**
* Hash map of languages.enabledLanguages using language codes as keys for
* fast lookup.
* @private {!Object<!LanguageInfo>}
*/
enabledLanguageMap_: {},
observers: [
'preferredLanguagesPrefChanged_(prefs.' +
preferredLanguagesPrefName + '.value)',
'spellCheckDictionariesPrefChanged_(prefs.spellcheck.dictionaries.value.*)',
'translateLanguagesPrefChanged_(prefs.translate_blocked_languages.value.*)',
],
/** @override */
created: function() {
var languageList;
var translateTarget;
// Request language information to populate the model.
Promise.all([
// Wait until prefs are initialized before creating the model, so we can
// include information about enabled languages.
CrSettingsPrefs.initialized,
// Get the language list.
new Promise(function(resolve) {
chrome.languageSettingsPrivate.getLanguageList(function(list) {
languageList = list;
resolve();
});
}),
// Get the translate target language.
new Promise(function(resolve) {
chrome.languageSettingsPrivate.getTranslateTargetLanguage(
function(targetLanguageCode) {
translateTarget = targetLanguageCode;
resolve();
});
}),
]).then(function() {
this.createModel_(languageList, translateTarget);
this.initialized_ = true;
}.bind(this));
},
/**
* Updates the list of enabled languages from the preferred languages pref.
* @private
*/
preferredLanguagesPrefChanged_: function() {
if (!this.initialized_)
return;
var enabledLanguages =
this.getEnabledLanguages_(this.languages.translateTarget);
// Reset the enabled language map before updating
// languages.enabledLanguages.
this.enabledLanguageMap_ = {};
for (var i = 0; i < enabledLanguages.length; i++) {
var languageInfo = enabledLanguages[i];
this.enabledLanguageMap_[languageInfo.language.code] = languageInfo;
}
this.set('languages.enabledLanguages', enabledLanguages);
},
/**
* Updates the spellCheckEnabled state of each enabled language.
* @private
*/
spellCheckDictionariesPrefChanged_: function() {
if (!this.initialized_)
return;
var spellCheckMap = this.makeMapFromArray_(/** @type {!Array<string>} */(
this.getPref('spellcheck.dictionaries').value));
for (var i = 0; i < this.languages.enabledLanguages.length; i++) {
var languageCode = this.languages.enabledLanguages[i].language.code;
this.set('languages.enabledLanguages.' + i + '.state.spellCheckEnabled',
!!spellCheckMap[languageCode]);
}
},
/** @private */
translateLanguagesPrefChanged_: function() {
if (!this.initialized_)
return;
var translateBlockedPref = this.getPref('translate_blocked_languages');
var translateBlockedMap = this.makeMapFromArray_(
/** @type {!Array<string>} */(translateBlockedPref.value));
for (var i = 0; i < this.languages.enabledLanguages.length; i++) {
var translateCode = this.convertLanguageCodeForTranslate(
this.languages.enabledLanguages[i].language.code);
this.set(
'languages.enabledLanguages.' + i + '.state.translateEnabled',
!translateBlockedMap[translateCode]);
}
},
/**
* Constructs the languages model.
* @param {!Array<!chrome.languageSettingsPrivate.Language>}
* supportedLanguages
* @param {string} translateTarget Language code of the default translate
* target language.
* @private
*/
createModel_: function(supportedLanguages, translateTarget) {
// Populate the hash map of supported languages.
for (var i = 0; i < supportedLanguages.length; i++) {
var language = supportedLanguages[i];
language.supportsUI = !!language.supportsUI;
language.supportsTranslate = !!language.supportsTranslate;
language.supportsSpellcheck = !!language.supportsSpellcheck;
this.supportedLanguageMap_[language.code] = language;
}
// Create a list of enabled language info from the supported languages.
var enabledLanguages = this.getEnabledLanguages_(translateTarget);
// Populate the hash map of enabled languages.
for (var i = 0; i < enabledLanguages.length; i++) {
var languageInfo = enabledLanguages[i];
this.enabledLanguageMap_[languageInfo.language.code] = languageInfo;
}
// Initialize the Polymer languages model.
this.languages = /** @type {!LanguagesModel} */({
supportedLanguages: supportedLanguages,
enabledLanguages: enabledLanguages,
translateTarget: translateTarget,
});
},
/**
* Returns a list of LanguageInfos for each enabled language in the supported
* languages list.
* @param {string} translateTarget Language code of the default translate
* target language.
* @return {!Array<!LanguageInfo>}
* @private
*/
getEnabledLanguages_: function(translateTarget) {
assert(CrSettingsPrefs.isInitialized);
var pref = this.getPref(preferredLanguagesPrefName);
var enabledLanguageCodes = pref.value.split(',');
var enabledLanguages = /** @type {!Array<!LanguageInfo>} */ [];
var spellCheckPref = this.getPref('spellcheck.dictionaries');
var spellCheckMap = this.makeMapFromArray_(/** @type {!Array<string>} */(
spellCheckPref.value));
var translateBlockedPref = this.getPref('translate_blocked_languages');
var translateBlockedMap = this.makeMapFromArray_(
/** @type {!Array<string>} */(translateBlockedPref.value));
for (var i = 0; i < enabledLanguageCodes.length; i++) {
var code = enabledLanguageCodes[i];
var language = this.supportedLanguageMap_[code];
// Skip unsupported languages.
if (!language)
continue;
var state = /** @type {LanguageState} */({});
state.spellCheckEnabled = !!spellCheckMap[code];
// Translate is considered disabled if this language maps to any translate
// language that is blocked.
var translateCode = this.convertLanguageCodeForTranslate(code);
state.translateEnabled = !!language.supportsTranslate &&
!translateBlockedMap[translateCode] &&
translateCode != translateTarget;
enabledLanguages.push(/** @type {LanguageInfo} */(
{language: language, state: state}));
}
return enabledLanguages;
},
/**
* Creates an object whose keys are the elements of the list.
* @param {!Array<string>} list
* @return {!Object<boolean>}
* @private
*/
makeMapFromArray_: function(list) {
var map = {};
for (var i = 0; i < list.length; i++)
map[list[i]] = true;
return map;
},
// LanguageHelper implementation.
// TODO(michaelpg): replace duplicate docs with @override once b/24294625
// is fixed.
<if expr="chromeos or is_win">
/**
* Sets the prospective UI language to the chosen language. This won't affect
* the actual UI language until a restart.
* @param {string} languageCode
*/
setUILanguage: function(languageCode) {
chrome.send('setUILanguage', [languageCode]);
},
/** Resets the prospective UI language back to the actual UI language. */
resetUILanguage: function() {
chrome.send('setUILanguage', [navigator.language]);
},
/**
* Returns the "prospective" UI language, i.e. the one to be used on next
* restart. If the pref is not set, the current UI language is also the
* "prospective" language.
* @return {string} Language code of the prospective UI language.
*/
getProspectiveUILanguage: function() {
return /** @type {string} */(this.getPref('intl.app_locale').value) ||
navigator.language;
},
</if>
/**
* @param {string} languageCode
* @return {boolean} True if the language is enabled.
*/
isLanguageEnabled: function(languageCode) {
return !!this.enabledLanguageMap_[languageCode];
},
/**
* Enables the language, making it available for spell check and input.
* @param {string} languageCode
*/
enableLanguage: function(languageCode) {
if (!CrSettingsPrefs.isInitialized)
return;
var languageCodes =
this.getPref(preferredLanguagesPrefName).value.split(',');
if (languageCodes.indexOf(languageCode) > -1)
return;
languageCodes.push(languageCode);
chrome.languageSettingsPrivate.setLanguageList(languageCodes);
this.disableTranslateLanguage(languageCode);
},
/**
* Disables the language.
* @param {string} languageCode
*/
disableLanguage: function(languageCode) {
if (!CrSettingsPrefs.isInitialized)
return;
assert(this.canDisableLanguage(languageCode));
// Remove the language from spell check.
this.deletePrefListItem('spellcheck.dictionaries', languageCode);
// Remove the language from preferred languages.
var languageCodes =
this.getPref(preferredLanguagesPrefName).value.split(',');
var languageIndex = languageCodes.indexOf(languageCode);
if (languageIndex == -1)
return;
languageCodes.splice(languageIndex, 1);
chrome.languageSettingsPrivate.setLanguageList(languageCodes);
this.enableTranslateLanguage(languageCode);
},
/**
* @param {string} languageCode Language code for an enabled language.
* @return {boolean}
*/
canDisableLanguage: function(languageCode) {
// Cannot disable the prospective UI language.
if ((cr.isChromeOS || cr.isWindows) &&
languageCode == this.getProspectiveUILanguage()) {
return false;
}
// Cannot disable the only enabled language.
if (this.languages.enabledLanguages.length == 1)
return false;
return true;
},
/**
* Enables translate for the given language by removing the translate
* language from the blocked languages preference.
* @param {string} languageCode
*/
enableTranslateLanguage: function(languageCode) {
languageCode = this.convertLanguageCodeForTranslate(languageCode);
this.deletePrefListItem('translate_blocked_languages', languageCode);
},
/**
* Disables translate for the given language by adding the translate
* language to the blocked languages preference.
* @param {string} languageCode
*/
disableTranslateLanguage: function(languageCode) {
this.appendPrefListItem('translate_blocked_languages',
this.convertLanguageCodeForTranslate(languageCode));
},
/**
* Enables or disables spell check for the given language.
* @param {string} languageCode
* @param {boolean} enable
*/
toggleSpellCheck: function(languageCode, enable) {
if (!this.initialized_)
return;
if (enable) {
var 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: function(languageCode) {
if (languageCode in kLanguageCodeToTranslateCode)
return kLanguageCodeToTranslateCode[languageCode];
var 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;
},
/**
* @param {string} languageCode
* @return {!chrome.languageSettingsPrivate.Language|undefined}
*/
getLanguage: function(languageCode) {
return this.supportedLanguageMap_[languageCode];
},
});
})();
/**
* A reference to the singleton under the guise of a LanguageHelper
* implementation. This provides a limited API but implies the singleton
* should not be used directly for data binding.
*/
var LanguageHelperImpl = SettingsLanguagesSingletonElement;
cr.addSingletonGetter(LanguageHelperImpl);
/**
* This element has a reference to the singleton, exposing the singleton's
* |languages| model to the host of this element.
*/
Polymer({
is: 'settings-languages',
properties: {
/**
* Singleton element created at startup which provides the languages model.
* @type {!SettingsLanguagesSingletonElement}
*/
singleton_: {
type: Object,
value: LanguageHelperImpl.getInstance(),
},
/**
* A reference to the languages model from the singleton, exposed as a
* read-only property so hosts can bind to it, but not change it.
* @type {LanguagesModel|undefined}
*/
languages: {
type: Object,
notify: true,
readOnly: true,
},
},
ready: function() {
// Set the 'languages' property to reference the singleton's model.
this._setLanguages(this.singleton_.languages);
// Listen for changes to the singleton's languages property, so we know
// when to notify hosts of changes to (our reference to) the property.
this.listen(
this.singleton_, 'languages-changed', 'singletonLanguagesChanged_');
},
/**
* Takes changes reported by the singleton and forwards them to the host,
* manually sending a change notification for our 'languages' property (since
* it's the same object as the singleton's property, but isn't bound by
* Polymer).
* @private
*/
singletonLanguagesChanged_: function(e) {
// Forward the change notification to the host.
this.fire(e.type, e.detail, {bubbles: false});
},
});