blob: 7499b9f5f2df93a4c843a13d50082b402fb24032 [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.
*/
(function() {
'use strict';
cr.exportPath('settings');
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',
'zh': 'zh-CH',
};
// 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',
};
const 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.
* @implements {LanguageHelper}
*/
Polymer({
is: 'settings-languages',
behaviors: [PrefsBehavior],
properties: {
/**
* @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: function() {
return /** @type {!LanguageHelper} */ (this);
},
},
/**
* PromiseResolver to be resolved when the singleton has been initialized.
* @private {!PromiseResolver}
*/
resolver_: {
type: Object,
value: function() {
return new PromiseResolver();
},
},
/**
* Hash map of supported languages by language codes for fast lookup.
* @private {!Map<string, !chrome.languageSettingsPrivate.Language>}
*/
supportedLanguageMap_: {
type: Object,
value: function() {
return new Map();
},
},
/**
* Hash set of enabled language codes for membership testing.
* @private {!Set<string>}
*/
enabledLanguageSet_: {
type: Object,
value: function() {
return new Set();
},
},
/**
* Hash map of supported input methods by ID for fast lookup.
* @private {!Map<string, chrome.languageSettingsPrivate.InputMethod>}
*/
supportedInputMethodMap_: {
type: Object,
value: function() {
return new Map();
},
},
/**
* Hash map of input methods supported for each language.
* @type {!Map<string,
* !Array<!chrome.languageSettingsPrivate.InputMethod>>}
* @private
*/
languageInputMethods_: {
type: Object,
value: function() {
return new Map();
},
},
/** @private Prospective UI language when the page was loaded. */
originalProspectiveUILanguage_: String,
},
observers: [
// All observers wait for the model to be populated by including the
// |languages| property.
'prospectiveUILanguageChanged_(prefs.intl.app_locale.value, languages)',
'preferredLanguagesPrefChanged_(' +
'prefs.' + preferredLanguagesPrefName + '.value, languages)',
'spellCheckDictionariesPrefChanged_(' +
'prefs.spellcheck.dictionaries.value.*, ' +
'prefs.spellcheck.forced_dictionaries.value.*, languages)',
'translateLanguagesPrefChanged_(' +
'prefs.translate_blocked_languages.value.*, languages)',
'updateRemovableLanguages_(' +
'prefs.intl.app_locale.value, languages.enabled)',
// Observe Chrome OS prefs (ignored for non-Chrome OS).
'updateRemovableLanguages_(' +
'prefs.settings.language.preload_engines.value, ' +
'prefs.settings.language.enabled_extension_imes.value, ' +
'languages)',
],
/** @private {?Function} */
boundOnInputMethodChanged_: null,
/** @private {?settings.LanguagesBrowserProxy} */
browserProxy_: null,
/** @private {?LanguageSettingsPrivate} */
languageSettingsPrivate_: null,
// <if expr="chromeos">
/** @private {?InputMethodPrivate} */
inputMethodPrivate_: null,
// </if>
/** @override */
attached: function() {
this.browserProxy_ = settings.LanguagesBrowserProxyImpl.getInstance();
this.languageSettingsPrivate_ =
this.browserProxy_.getLanguageSettingsPrivate();
// <if expr="chromeos">
this.inputMethodPrivate_ = this.browserProxy_.getInputMethodPrivate();
// </if>
const promises = [];
// Wait until prefs are initialized before creating the model, so we can
// include information about enabled languages.
promises[0] = CrSettingsPrefs.initialized;
// Get the language list.
promises[1] = new Promise(resolve => {
this.languageSettingsPrivate_.getLanguageList(resolve);
});
// Get the translate target language.
promises[2] = new Promise(resolve => {
this.languageSettingsPrivate_.getTranslateTargetLanguage(resolve);
});
if (cr.isChromeOS) {
promises[3] = new Promise(resolve => {
this.languageSettingsPrivate_.getInputMethodLists(function(lists) {
resolve(lists.componentExtensionImes.concat(
lists.thirdPartyExtensionImes));
});
});
promises[4] = new Promise(resolve => {
this.inputMethodPrivate_.getCurrentInputMethod(resolve);
});
}
if (cr.isWindows || cr.isChromeOS) {
// 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;
}
// TODO(dpapad): Cleanup this code. It uses results[3] and results[4]
// which only exist for ChromeOS.
this.createModel_(results[1], results[2], results[3], results[4]);
this.resolver_.resolve();
});
if (cr.isChromeOS) {
this.boundOnInputMethodChanged_ = this.onInputMethodChanged_.bind(this);
this.inputMethodPrivate_.onChanged.addListener(
assert(this.boundOnInputMethodChanged_));
}
},
/** @override */
detached: function() {
if (cr.isChromeOS) {
this.inputMethodPrivate_.onChanged.removeListener(
assert(this.boundOnInputMethodChanged_));
this.boundOnInputMethodChanged_ = null;
}
},
/**
* Updates the prospective UI language based on the new pref value.
* @param {string} prospectiveUILanguage
* @private
*/
prospectiveUILanguageChanged_: function(prospectiveUILanguage) {
this.set(
'languages.prospectiveUILanguage',
prospectiveUILanguage || this.originalProspectiveUILanguage_);
},
/**
* Updates the list of enabled languages from the preferred languages pref.
* @private
*/
preferredLanguagesPrefChanged_: function() {
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);
},
/**
* Updates the spellCheckEnabled state of each enabled language.
* @private
*/
spellCheckDictionariesPrefChanged_: function() {
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));
for (let i = 0; i < this.languages.enabled.length; i++) {
const languageState = this.languages.enabled[i];
this.set(
`languages.enabled.${i}.spellCheckEnabled`,
!!spellCheckSet.has(languageState.language.code));
this.set(
`languages.enabled.${i}.isManaged`,
!!spellCheckForcedSet.has(languageState.language.code));
}
this.set(
'languages.forcedSpellCheckLanguages',
this.getForcedSpellCheckLanguages_(this.languages.enabled));
},
/**
* Returns an array of language codes for the spellcheck languages that are
* managed by policy, but that are not "enabled" languages.
* @param {!Array<!LanguageState>} enabledLanguages An array of enabled
* languages.
* @return {!Array<!string>}
* @private
*/
getForcedSpellCheckLanguages_: function(enabledLanguages) {
const enabledSet = this.makeSetFromArray_(/** @type {!Array<string>} */ (
enabledLanguages.map(x => x.language.code)));
const spellCheckForcedDictionaries = /** @type {!Array<string>} */ (
this.getPref('spellcheck.forced_dictionaries').value);
const forcedLanguages = [];
for (let i = 0; i < spellCheckForcedDictionaries.length; i++) {
const code = spellCheckForcedDictionaries[i];
if (!enabledSet.has(code) && this.supportedLanguageMap_.has(code)) {
forcedLanguages.push(this.supportedLanguageMap_.get(code));
}
}
return forcedLanguages;
},
/** @private */
translateLanguagesPrefChanged_: function() {
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++) {
if (this.languages.enabled[i].language.code ==
this.languages.prospectiveUILanguage) {
continue;
}
// This conversion primarily strips away the region part.
// For example "fr-CA" --> "fr".
const translateCode = this.convertLanguageCodeForTranslate(
this.languages.enabled[i].language.code);
this.set(
'languages.enabled.' + i + '.translateEnabled',
!translateBlockedSet.has(translateCode));
}
},
/**
* Constructs the languages model.
* @param {!Array<!chrome.languageSettingsPrivate.Language>}
* supportedLanguages
* @param {string} translateTarget Language code of the default translate
* target language.
* @param {!Array<!chrome.languageSettingsPrivate.InputMethod>|undefined}
* supportedInputMethods Input methods (Chrome OS only).
* @param {string|undefined} currentInputMethodId ID of the currently used
* input method (Chrome OS only).
* @private
*/
createModel_: function(
supportedLanguages, translateTarget, supportedInputMethods,
currentInputMethodId) {
// Populate the hash map of supported languages.
for (let i = 0; i < supportedLanguages.length; i++) {
const language = supportedLanguages[i];
language.supportsUI = !!language.supportsUI;
language.supportsTranslate = !!language.supportsTranslate;
language.supportsSpellcheck = !!language.supportsSpellcheck;
this.supportedLanguageMap_.set(language.code, language);
}
if (supportedInputMethods) {
// Populate the hash map of supported input methods.
for (let j = 0; j < supportedInputMethods.length; j++) {
const inputMethod = supportedInputMethods[j];
inputMethod.enabled = !!inputMethod.enabled;
// 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);
}
}
}
let prospectiveUILanguage;
if (cr.isChromeOS || cr.isWindows) {
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_(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 forcedSpellCheckLanguages =
this.getForcedSpellCheckLanguages_(enabledLanguageStates);
const model = /** @type {!LanguagesModel} */ ({
supported: supportedLanguages,
enabled: enabledLanguageStates,
translateTarget: translateTarget,
forcedSpellCheckLanguages: forcedSpellCheckLanguages,
});
if (cr.isChromeOS || cr.isWindows)
model.prospectiveUILanguage = prospectiveUILanguage;
if (cr.isChromeOS) {
model.inputMethods = /** @type {!InputMethodsModel} */ ({
supported: supportedInputMethods,
enabled: this.getEnabledInputMethods_(),
currentId: 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_: function(translateTarget, prospectiveUILanguage) {
assert(CrSettingsPrefs.isInitialized);
const pref = this.getPref(preferredLanguagesPrefName);
const enabledLanguageCodes = pref.value.split(',');
const spellCheckPref = this.getPref('spellcheck.dictionaries');
const spellCheckForcedPref = this.getPref('spellcheck.forced_dictionaries');
const spellCheckSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (
spellCheckPref.value.concat(spellCheckForcedPref.value)));
const spellCheckForcedSet = this.makeSetFromArray_(
/** @type {!Array<string>} */ (spellCheckForcedPref.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);
// Translate is considered disabled if this language maps to any translate
// language that is blocked.
const translateCode = this.convertLanguageCodeForTranslate(code);
languageState.translateEnabled = !!language.supportsTranslate &&
!translateBlockedSet.has(translateCode) &&
translateCode != translateTarget &&
(!prospectiveUILanguage || code != prospectiveUILanguage);
languageState.isManaged = !!spellCheckForcedSet.has(code);
enabledLanguageStates.push(languageState);
}
return enabledLanguageStates;
},
/**
* Returns a list of enabled input methods.
* @return {!Array<!chrome.languageSettingsPrivate.InputMethod>}
* @private
*/
getEnabledInputMethods_: function() {
assert(cr.isChromeOS);
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(','));
// Return only supported input methods.
return enabledInputMethodIds
.map(id => this.supportedInputMethodMap_.get(id))
.filter(function(inputMethod) {
return !!inputMethod;
});
},
/** @private */
updateEnabledInputMethods_: function() {
assert(cr.isChromeOS);
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);
},
/**
* Updates the |removable| property of the enabled language states based
* on what other languages and input methods are enabled.
* @private
*/
updateRemovableLanguages_: function() {
assert(this.languages);
// TODO(michaelpg): Enabled input methods can affect which languages are
// removable, so run updateEnabledInputMethods_ first (if it has been
// scheduled).
if (cr.isChromeOS)
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.language.code));
}
},
/**
* Creates a Set from the elements of the array.
* @param {!Array<T>} list
* @return {!Set<T>}
* @template T
* @private
*/
makeSetFromArray_: function(list) {
return new Set(list);
},
// LanguageHelper implementation.
// TODO(michaelpg): replace duplicate docs with @override once b/24294625
// is fixed.
/** @return {!Promise} */
whenReady: function() {
return this.resolver_.promise;
},
// <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
*/
setProspectiveUILanguage: function(languageCode) {
this.browserProxy_.setProspectiveUILanguage(languageCode);
},
/**
* True if the prospective UI language was changed from its starting value.
* @return {boolean}
*/
requiresRestart: function() {
return this.originalProspectiveUILanguage_ !=
this.languages.prospectiveUILanguage;
},
// </if>
/**
* @param {string} languageCode
* @return {boolean} True if the language is enabled.
*/
isLanguageEnabled: function(languageCode) {
return this.enabledLanguageSet_.has(languageCode);
},
/**
* Enables the language, making it available for spell check and input.
* @param {string} languageCode
*/
enableLanguage: function(languageCode) {
if (!CrSettingsPrefs.isInitialized)
return;
this.languageSettingsPrivate_.enableLanguage(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);
if (cr.isChromeOS) {
// Remove input methods that don't support any other enabled language.
const inputMethods = this.languageInputMethods_.get(languageCode) || [];
for (let i = 0; i < inputMethods.length; i++) {
const inputMethod = inputMethods[i];
const supportsOtherEnabledLanguages = inputMethod.languageCodes.some(
otherLanguageCode => otherLanguageCode != languageCode &&
this.isLanguageEnabled(otherLanguageCode));
if (!supportsOtherEnabledLanguages)
this.removeInputMethod(inputMethod.id);
}
}
// Remove the language from preferred languages.
this.languageSettingsPrivate_.disableLanguage(languageCode);
},
/**
* @param {string} languageCode Language code for an enabled language.
* @return {boolean}
*/
canDisableLanguage: function(languageCode) {
// Cannot disable the prospective UI language.
if (languageCode == this.languages.prospectiveUILanguage)
return false;
// Cannot disable the only enabled language.
if (this.languages.enabled.length == 1)
return false;
if (!cr.isChromeOS)
return true;
// If this is the only enabled language that is supported by all enabled
// component IMEs, it cannot be disabled because we need those IMEs.
const otherInputMethodsEnabled =
this.languages.enabled.some(function(languageState) {
const otherLanguageCode = languageState.language.code;
if (otherLanguageCode == languageCode)
return false;
const inputMethods =
this.languageInputMethods_.get(otherLanguageCode);
return inputMethods && inputMethods.some(function(inputMethod) {
return this.isComponentIme(inputMethod) &&
this.supportedInputMethodMap_.get(inputMethod.id).enabled;
}, this);
}, this);
return otherInputMethodsEnabled;
},
/**
* 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: function(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: function(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: function(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: function(languageCode) {
this.languageSettingsPrivate_.setEnableTranslationForLanguage(
languageCode, false);
},
/**
* Enables or disables spell check for the given language.
* @param {string} languageCode
* @param {boolean} enable
*/
toggleSpellCheck: function(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: function(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: function(languageCode) {
// The Norwegian languages fall under the 'no' macrolanguage.
if (languageCode == 'nb' || languageCode == 'nn')
return 'no';
// 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: function(languageCode) {
return this.supportedLanguageMap_.get(languageCode);
},
// <if expr="chromeos">
/** @param {string} id */
addInputMethod: function(id) {
if (!this.supportedInputMethodMap_.has(id))
return;
this.languageSettingsPrivate_.addInputMethod(id);
},
/** @param {string} id */
removeInputMethod: function(id) {
if (!this.supportedInputMethodMap_.has(id))
return;
this.languageSettingsPrivate_.removeInputMethod(id);
},
/** @param {string} id */
setCurrentInputMethod: function(id) {
this.inputMethodPrivate_.setCurrentInputMethod(id);
},
/**
* @param {string} languageCode
* @return {!Array<!chrome.languageSettingsPrivate.InputMethod>}
*/
getInputMethodsForLanguage: function(languageCode) {
return this.languageInputMethods_.get(languageCode) || [];
},
/**
* @param {!chrome.languageSettingsPrivate.InputMethod} inputMethod
* @return {boolean}
*/
isComponentIme: function(inputMethod) {
return inputMethod.id.startsWith('_comp_');
},
/** @param {string} id Input method ID. */
openInputMethodOptions: function(id) {
this.inputMethodPrivate_.openOptionsPage(id);
},
/** @param {string} id New current input method ID. */
onInputMethodChanged_: function(id) {
this.set('languages.inputMethods.currentId', id);
},
/** @param {string} id Added input method ID. */
onInputMethodAdded_: function(id) {
this.updateEnabledInputMethods_();
},
/** @param {string} id Removed input method ID. */
onInputMethodRemoved_: function(id) {
this.updateEnabledInputMethods_();
},
// </if>
});
})();