Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 // TODO(kochi): Generalize the notification as a component and put it
      6 // in js/cr/ui/notification.js .
      7 
      8 cr.define('options', function() {
      9   /** @const */ var Page = cr.ui.pageManager.Page;
     10   /** @const */ var PageManager = cr.ui.pageManager.PageManager;
     11   /** @const */ var LanguageList = options.LanguageList;
     12   /** @const */ var ThirdPartyImeConfirmOverlay =
     13       options.ThirdPartyImeConfirmOverlay;
     14 
     15   /**
     16    * Spell check dictionary download status.
     17    * @type {Enum}
     18    */
     19   /** @const*/ var DOWNLOAD_STATUS = {
     20     IN_PROGRESS: 1,
     21     FAILED: 2
     22   };
     23 
     24   /**
     25    * The preference is a boolean that enables/disables spell checking.
     26    * @type {string}
     27    * @const
     28    */
     29   var ENABLE_SPELL_CHECK_PREF = 'browser.enable_spellchecking';
     30 
     31   /**
     32    * The preference is a CSV string that describes preload engines
     33    * (i.e. active input methods).
     34    * @type {string}
     35    * @const
     36    */
     37   var PRELOAD_ENGINES_PREF = 'settings.language.preload_engines';
     38 
     39   /**
     40    * The preference that lists the extension IMEs that are enabled in the
     41    * language menu.
     42    * @type {string}
     43    * @const
     44    */
     45   var ENABLED_EXTENSION_IME_PREF = 'settings.language.enabled_extension_imes';
     46 
     47   /**
     48    * The preference that lists the languages which are not translated.
     49    * @type {string}
     50    * @const
     51    */
     52   var TRANSLATE_BLOCKED_LANGUAGES_PREF = 'translate_blocked_languages';
     53 
     54   /**
     55    * The preference key that is a string that describes the spell check
     56    * dictionary language, like "en-US".
     57    * @type {string}
     58    * @const
     59    */
     60   var SPELL_CHECK_DICTIONARY_PREF = 'spellcheck.dictionary';
     61 
     62   /**
     63    * The preference that indicates if the Translate feature is enabled.
     64    * @type {string}
     65    * @const
     66    */
     67   var ENABLE_TRANSLATE = 'translate.enabled';
     68 
     69   /////////////////////////////////////////////////////////////////////////////
     70   // LanguageOptions class:
     71 
     72   /**
     73    * Encapsulated handling of ChromeOS language options page.
     74    * @constructor
     75    * @extends {cr.ui.pageManager.Page}
     76    */
     77   function LanguageOptions(model) {
     78     Page.call(this, 'languages',
     79               loadTimeData.getString('languagePageTabTitle'), 'languagePage');
     80   }
     81 
     82   cr.addSingletonGetter(LanguageOptions);
     83 
     84   // Inherit LanguageOptions from Page.
     85   LanguageOptions.prototype = {
     86     __proto__: Page.prototype,
     87 
     88     /**
     89      * For recording the prospective language (the next locale after relaunch).
     90      * @type {?string}
     91      * @private
     92      */
     93     prospectiveUiLanguageCode_: null,
     94 
     95     /**
     96      * Map from language code to spell check dictionary download status for that
     97      * language.
     98      * @type {Array}
     99      * @private
    100      */
    101     spellcheckDictionaryDownloadStatus_: [],
    102 
    103     /**
    104      * Number of times a spell check dictionary download failed.
    105      * @type {number}
    106      * @private
    107      */
    108     spellcheckDictionaryDownloadFailures_: 0,
    109 
    110     /**
    111      * The list of preload engines, like ['mozc', 'pinyin'].
    112      * @type {Array}
    113      * @private
    114      */
    115     preloadEngines_: [],
    116 
    117     /**
    118      * The list of extension IMEs that are enabled out of the language menu.
    119      * @type {Array}
    120      * @private
    121      */
    122     enabledExtensionImes_: [],
    123 
    124     /**
    125      * The list of the languages which is not translated.
    126      * @type {Array}
    127      * @private
    128      */
    129     translateBlockedLanguages_: [],
    130 
    131     /**
    132      * The list of the languages supported by Translate server
    133      * @type {Array}
    134      * @private
    135      */
    136     translateSupportedLanguages_: [],
    137 
    138     /**
    139      * The preference is a string that describes the spell check dictionary
    140      * language, like "en-US".
    141      * @type {string}
    142      * @private
    143      */
    144     spellCheckDictionary_: '',
    145 
    146     /**
    147      * The map of language code to input method IDs, like:
    148      * {'ja': ['mozc', 'mozc-jp'], 'zh-CN': ['pinyin'], ...}
    149      * @type {Object}
    150      * @private
    151      */
    152     languageCodeToInputMethodIdsMap_: {},
    153 
    154     /**
    155      * The value that indicates if Translate feature is enabled or not.
    156      * @type {boolean}
    157      * @private
    158      */
    159     enableTranslate_: false,
    160 
    161     /** @override */
    162     initializePage: function() {
    163       Page.prototype.initializePage.call(this);
    164 
    165       var languageOptionsList = $('language-options-list');
    166       LanguageList.decorate(languageOptionsList);
    167 
    168       languageOptionsList.addEventListener('change',
    169           this.handleLanguageOptionsListChange_.bind(this));
    170       languageOptionsList.addEventListener('save',
    171           this.handleLanguageOptionsListSave_.bind(this));
    172 
    173       this.prospectiveUiLanguageCode_ =
    174           loadTimeData.getString('prospectiveUiLanguageCode');
    175       this.addEventListener('visibleChange',
    176                             this.handleVisibleChange_.bind(this));
    177 
    178       if (cr.isChromeOS) {
    179         this.initializeInputMethodList_();
    180         this.initializeLanguageCodeToInputMethodIdsMap_();
    181       }
    182 
    183       var checkbox = $('offer-to-translate-in-this-language');
    184       checkbox.addEventListener('click',
    185           this.handleOfferToTranslateCheckboxClick_.bind(this));
    186 
    187       Preferences.getInstance().addEventListener(
    188           TRANSLATE_BLOCKED_LANGUAGES_PREF,
    189           this.handleTranslateBlockedLanguagesPrefChange_.bind(this));
    190       Preferences.getInstance().addEventListener(SPELL_CHECK_DICTIONARY_PREF,
    191           this.handleSpellCheckDictionaryPrefChange_.bind(this));
    192       Preferences.getInstance().addEventListener(ENABLE_TRANSLATE,
    193           this.handleEnableTranslatePrefChange_.bind(this));
    194       this.translateSupportedLanguages_ =
    195           loadTimeData.getValue('translateSupportedLanguages');
    196 
    197       // Set up add button.
    198       var onclick = function(e) {
    199         // Add the language without showing the overlay if it's specified in
    200         // the URL hash (ex. lang_add=ja).  Used for automated testing.
    201         var match = document.location.hash.match(/\blang_add=([\w-]+)/);
    202         if (match) {
    203           var addLanguageCode = match[1];
    204           $('language-options-list').addLanguage(addLanguageCode);
    205           this.addBlockedLanguage_(addLanguageCode);
    206         } else {
    207           PageManager.showPageByName('addLanguage');
    208         }
    209       };
    210       $('language-options-add-button').onclick = onclick.bind(this);
    211 
    212       if (!cr.isMac) {
    213         // Set up the button for editing custom spelling dictionary.
    214         $('edit-dictionary-button').onclick = function(e) {
    215           PageManager.showPageByName('editDictionary');
    216         };
    217         $('dictionary-download-retry-button').onclick = function(e) {
    218           chrome.send('retryDictionaryDownload');
    219         };
    220       }
    221 
    222       // Listen to add language dialog ok button.
    223       $('add-language-overlay-ok-button').addEventListener(
    224           'click', this.handleAddLanguageOkButtonClick_.bind(this));
    225 
    226       if (!cr.isChromeOS) {
    227         // Show experimental features if enabled.
    228         if (loadTimeData.getBoolean('enableSpellingAutoCorrect'))
    229           $('auto-spell-correction-option').hidden = false;
    230 
    231         // Handle spell check enable/disable.
    232         if (!cr.isMac) {
    233           Preferences.getInstance().addEventListener(
    234               ENABLE_SPELL_CHECK_PREF,
    235               this.updateEnableSpellCheck_.bind(this));
    236         }
    237       }
    238 
    239       // Handle clicks on "Use this language for spell checking" button.
    240       if (!cr.isMac) {
    241         var spellCheckLanguageButton = getRequiredElement(
    242             'language-options-spell-check-language-button');
    243         spellCheckLanguageButton.addEventListener(
    244             'click',
    245             this.handleSpellCheckLanguageButtonClick_.bind(this));
    246       }
    247 
    248       if (cr.isChromeOS) {
    249         $('language-options-ui-restart-button').onclick = function() {
    250           chrome.send('uiLanguageRestart');
    251         };
    252       }
    253 
    254       $('language-confirm').onclick =
    255           PageManager.closeOverlay.bind(PageManager);
    256 
    257       // Public session users cannot change the locale.
    258       if (cr.isChromeOS && UIAccountTweaks.loggedInAsPublicAccount())
    259         $('language-options-ui-language-section').hidden = true;
    260           PageManager.closeOverlay.bind(PageManager);
    261     },
    262 
    263     /**
    264      * Initializes the input method list.
    265      */
    266     initializeInputMethodList_: function() {
    267       var inputMethodList = $('language-options-input-method-list');
    268       var inputMethodPrototype = $('language-options-input-method-template');
    269 
    270       // Add all input methods, but make all of them invisible here. We'll
    271       // change the visibility in handleLanguageOptionsListChange_() based
    272       // on the selected language. Note that we only have less than 100
    273       // input methods, so creating DOM nodes at once here should be ok.
    274       this.appendInputMethodElement_(loadTimeData.getValue('inputMethodList'));
    275       this.appendInputMethodElement_(loadTimeData.getValue('extensionImeList'));
    276       this.appendComponentExtensionIme_(
    277           loadTimeData.getValue('componentExtensionImeList'));
    278 
    279       // Listen to pref change once the input method list is initialized.
    280       Preferences.getInstance().addEventListener(
    281           PRELOAD_ENGINES_PREF,
    282           this.handlePreloadEnginesPrefChange_.bind(this));
    283       Preferences.getInstance().addEventListener(
    284           ENABLED_EXTENSION_IME_PREF,
    285           this.handleEnabledExtensionsPrefChange_.bind(this));
    286     },
    287 
    288     /**
    289      * Appends input method lists based on component extension ime list.
    290      * @param {!Array} componentExtensionImeList A list of input method
    291      *     descriptors.
    292      * @private
    293      */
    294     appendComponentExtensionIme_: function(componentExtensionImeList) {
    295       this.appendInputMethodElement_(componentExtensionImeList);
    296 
    297       for (var i = 0; i < componentExtensionImeList.length; i++) {
    298         var inputMethod = componentExtensionImeList[i];
    299         for (var languageCode in inputMethod.languageCodeSet) {
    300           if (languageCode in this.languageCodeToInputMethodIdsMap_) {
    301             this.languageCodeToInputMethodIdsMap_[languageCode].push(
    302                 inputMethod.id);
    303           } else {
    304             this.languageCodeToInputMethodIdsMap_[languageCode] =
    305                 [inputMethod.id];
    306           }
    307         }
    308       }
    309     },
    310 
    311     /**
    312      * Appends input methods into input method list.
    313      * @param {!Array} inputMethods A list of input method descriptors.
    314      * @private
    315      */
    316     appendInputMethodElement_: function(inputMethods) {
    317       var inputMethodList = $('language-options-input-method-list');
    318       var inputMethodTemplate = $('language-options-input-method-template');
    319 
    320       for (var i = 0; i < inputMethods.length; i++) {
    321         var inputMethod = inputMethods[i];
    322         var element = inputMethodTemplate.cloneNode(true);
    323         element.id = '';
    324         element.languageCodeSet = inputMethod.languageCodeSet;
    325 
    326         var input = element.querySelector('input');
    327         input.inputMethodId = inputMethod.id;
    328         input.imeProvider = inputMethod.extensionName;
    329         var span = element.querySelector('span');
    330         span.textContent = inputMethod.displayName;
    331 
    332         if (inputMethod.optionsPage) {
    333           var button = document.createElement('button');
    334           button.textContent = loadTimeData.getString('configure');
    335           button.inputMethodId = inputMethod.id;
    336           button.onclick = function(inputMethodId, e) {
    337             chrome.send('inputMethodOptionsOpen', [inputMethodId]);
    338           }.bind(this, inputMethod.id);
    339           element.appendChild(button);
    340         }
    341 
    342         // Listen to user clicks.
    343         input.addEventListener('click',
    344                                this.handleCheckboxClick_.bind(this));
    345         inputMethodList.appendChild(element);
    346       }
    347     },
    348 
    349     /**
    350      * Adds a language to the preference 'translate_blocked_languages'. If
    351      * |langCode| is already added, nothing happens. |langCode| is converted
    352      * to a Translate language synonym before added.
    353      * @param {string} langCode A language code like 'en'
    354      * @private
    355      */
    356     addBlockedLanguage_: function(langCode) {
    357       langCode = this.convertLangCodeForTranslation_(langCode);
    358       if (this.translateBlockedLanguages_.indexOf(langCode) == -1) {
    359         this.translateBlockedLanguages_.push(langCode);
    360         Preferences.setListPref(TRANSLATE_BLOCKED_LANGUAGES_PREF,
    361                                 this.translateBlockedLanguages_, true);
    362       }
    363     },
    364 
    365     /**
    366      * Removes a language from the preference 'translate_blocked_languages'.
    367      * If |langCode| doesn't exist in the preference, nothing happens.
    368      * |langCode| is converted to a Translate language synonym before removed.
    369      * @param {string} langCode A language code like 'en'
    370      * @private
    371      */
    372     removeBlockedLanguage_: function(langCode) {
    373       langCode = this.convertLangCodeForTranslation_(langCode);
    374       if (this.translateBlockedLanguages_.indexOf(langCode) != -1) {
    375         this.translateBlockedLanguages_ =
    376             this.translateBlockedLanguages_.filter(
    377                 function(langCodeNotTranslated) {
    378                   return langCodeNotTranslated != langCode;
    379                 });
    380         Preferences.setListPref(TRANSLATE_BLOCKED_LANGUAGES_PREF,
    381                                 this.translateBlockedLanguages_, true);
    382       }
    383     },
    384 
    385     /**
    386      * Handles Page's visible property change event.
    387      * @param {Event} e Property change event.
    388      * @private
    389      */
    390     handleVisibleChange_: function(e) {
    391       if (this.visible) {
    392         $('language-options-list').redraw();
    393         chrome.send('languageOptionsOpen');
    394       }
    395     },
    396 
    397     /**
    398      * Handles languageOptionsList's change event.
    399      * @param {Event} e Change event.
    400      * @private
    401      */
    402     handleLanguageOptionsListChange_: function(e) {
    403       var languageOptionsList = $('language-options-list');
    404       var languageCode = languageOptionsList.getSelectedLanguageCode();
    405 
    406       // If there's no selection, just return.
    407       if (!languageCode)
    408         return;
    409 
    410       // Select the language if it's specified in the URL hash (ex. lang=ja).
    411       // Used for automated testing.
    412       var match = document.location.hash.match(/\blang=([\w-]+)/);
    413       if (match) {
    414         var specifiedLanguageCode = match[1];
    415         if (languageOptionsList.selectLanguageByCode(specifiedLanguageCode)) {
    416           languageCode = specifiedLanguageCode;
    417         }
    418       }
    419 
    420       this.updateOfferToTranslateCheckbox_(languageCode);
    421 
    422       if (cr.isWindows || cr.isChromeOS)
    423         this.updateUiLanguageButton_(languageCode);
    424 
    425       this.updateSelectedLanguageName_(languageCode);
    426 
    427       if (!cr.isMac)
    428         this.updateSpellCheckLanguageButton_(languageCode);
    429 
    430       if (cr.isChromeOS)
    431         this.updateInputMethodList_(languageCode);
    432 
    433       this.updateLanguageListInAddLanguageOverlay_();
    434     },
    435 
    436     /**
    437      * Handles languageOptionsList's save event.
    438      * @param {Event} e Save event.
    439      * @private
    440      */
    441     handleLanguageOptionsListSave_: function(e) {
    442       if (cr.isChromeOS) {
    443         // Sort the preload engines per the saved languages before save.
    444         this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_);
    445         this.savePreloadEnginesPref_();
    446       }
    447     },
    448 
    449     /**
    450      * Sorts preloadEngines_ by languageOptionsList's order.
    451      * @param {Array} preloadEngines List of preload engines.
    452      * @return {Array} Returns sorted preloadEngines.
    453      * @private
    454      */
    455     sortPreloadEngines_: function(preloadEngines) {
    456       // For instance, suppose we have two languages and associated input
    457       // methods:
    458       //
    459       // - Korean: hangul
    460       // - Chinese: pinyin
    461       //
    462       // The preloadEngines preference should look like "hangul,pinyin".
    463       // If the user reverse the order, the preference should be reorderd
    464       // to "pinyin,hangul".
    465       var languageOptionsList = $('language-options-list');
    466       var languageCodes = languageOptionsList.getLanguageCodes();
    467 
    468       // Convert the list into a dictonary for simpler lookup.
    469       var preloadEngineSet = {};
    470       for (var i = 0; i < preloadEngines.length; i++) {
    471         preloadEngineSet[preloadEngines[i]] = true;
    472       }
    473 
    474       // Create the new preload engine list per the language codes.
    475       var newPreloadEngines = [];
    476       for (var i = 0; i < languageCodes.length; i++) {
    477         var languageCode = languageCodes[i];
    478         var inputMethodIds = this.languageCodeToInputMethodIdsMap_[
    479             languageCode];
    480         if (!inputMethodIds)
    481           continue;
    482 
    483         // Check if we have active input methods associated with the language.
    484         for (var j = 0; j < inputMethodIds.length; j++) {
    485           var inputMethodId = inputMethodIds[j];
    486           if (inputMethodId in preloadEngineSet) {
    487             // If we have, add it to the new engine list.
    488             newPreloadEngines.push(inputMethodId);
    489             // And delete it from the set. This is necessary as one input
    490             // method can be associated with more than one language thus
    491             // we should avoid having duplicates in the new list.
    492             delete preloadEngineSet[inputMethodId];
    493           }
    494         }
    495       }
    496 
    497       return newPreloadEngines;
    498     },
    499 
    500     /**
    501      * Initializes the map of language code to input method IDs.
    502      * @private
    503      */
    504     initializeLanguageCodeToInputMethodIdsMap_: function() {
    505       var inputMethodList = loadTimeData.getValue('inputMethodList');
    506       for (var i = 0; i < inputMethodList.length; i++) {
    507         var inputMethod = inputMethodList[i];
    508         for (var languageCode in inputMethod.languageCodeSet) {
    509           if (languageCode in this.languageCodeToInputMethodIdsMap_) {
    510             this.languageCodeToInputMethodIdsMap_[languageCode].push(
    511                 inputMethod.id);
    512           } else {
    513             this.languageCodeToInputMethodIdsMap_[languageCode] =
    514                 [inputMethod.id];
    515           }
    516         }
    517       }
    518     },
    519 
    520     /**
    521      * Updates the currently selected language name.
    522      * @param {string} languageCode Language code (ex. "fr").
    523      * @private
    524      */
    525     updateSelectedLanguageName_: function(languageCode) {
    526       var languageInfo = LanguageList.getLanguageInfoFromLanguageCode(
    527           languageCode);
    528       var languageDisplayName = languageInfo.displayName;
    529       var languageNativeDisplayName = languageInfo.nativeDisplayName;
    530       var textDirection = languageInfo.textDirection;
    531 
    532       // If the native name is different, add it.
    533       if (languageDisplayName != languageNativeDisplayName) {
    534         languageDisplayName += ' - ' + languageNativeDisplayName;
    535       }
    536 
    537       // Update the currently selected language name.
    538       var languageName = $('language-options-language-name');
    539       languageName.textContent = languageDisplayName;
    540       languageName.dir = textDirection;
    541     },
    542 
    543     /**
    544      * Updates the UI language button.
    545      * @param {string} languageCode Language code (ex. "fr").
    546      * @private
    547      */
    548     updateUiLanguageButton_: function(languageCode) {
    549       var uiLanguageButton = $('language-options-ui-language-button');
    550       var uiLanguageMessage = $('language-options-ui-language-message');
    551       var uiLanguageNotification = $('language-options-ui-notification-bar');
    552 
    553       // Remove the event listener and add it back if useful.
    554       uiLanguageButton.onclick = null;
    555 
    556       // Unhide the language button every time, as it could've been previously
    557       // hidden by a language change.
    558       uiLanguageButton.hidden = false;
    559 
    560       // Hide the controlled setting indicator.
    561       var uiLanguageIndicator = document.querySelector(
    562           '.language-options-contents .controlled-setting-indicator');
    563       uiLanguageIndicator.removeAttribute('controlled-by');
    564 
    565       if (languageCode == this.prospectiveUiLanguageCode_) {
    566         uiLanguageMessage.textContent =
    567             loadTimeData.getString('isDisplayedInThisLanguage');
    568         showMutuallyExclusiveNodes(
    569             [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 1);
    570       } else if (languageCode in loadTimeData.getValue('uiLanguageCodeSet')) {
    571         if (cr.isChromeOS && UIAccountTweaks.loggedInAsGuest()) {
    572           // In the guest mode for ChromeOS, changing UI language does not make
    573           // sense because it does not take effect after browser restart.
    574           uiLanguageButton.hidden = true;
    575           uiLanguageMessage.hidden = true;
    576         } else {
    577           uiLanguageButton.textContent =
    578               loadTimeData.getString('displayInThisLanguage');
    579 
    580           if (loadTimeData.valueExists('secondaryUser') &&
    581               loadTimeData.getBoolean('secondaryUser')) {
    582             uiLanguageButton.disabled = true;
    583             uiLanguageIndicator.setAttribute('controlled-by', 'shared');
    584           } else {
    585             uiLanguageButton.onclick = function(e) {
    586               chrome.send('uiLanguageChange', [languageCode]);
    587             };
    588           }
    589           showMutuallyExclusiveNodes(
    590               [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 0);
    591         }
    592       } else {
    593         uiLanguageMessage.textContent =
    594             loadTimeData.getString('cannotBeDisplayedInThisLanguage');
    595         showMutuallyExclusiveNodes(
    596             [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 1);
    597       }
    598     },
    599 
    600     /**
    601      * Updates the spell check language button.
    602      * @param {string} languageCode Language code (ex. "fr").
    603      * @private
    604      */
    605     updateSpellCheckLanguageButton_: function(languageCode) {
    606       var spellCheckLanguageSection = $('language-options-spellcheck');
    607       var spellCheckLanguageButton =
    608           $('language-options-spell-check-language-button');
    609       var spellCheckLanguageMessage =
    610           $('language-options-spell-check-language-message');
    611       var dictionaryDownloadInProgress =
    612           $('language-options-dictionary-downloading-message');
    613       var dictionaryDownloadFailed =
    614           $('language-options-dictionary-download-failed-message');
    615       var dictionaryDownloadFailHelp =
    616           $('language-options-dictionary-download-fail-help-message');
    617       spellCheckLanguageSection.hidden = false;
    618       spellCheckLanguageMessage.hidden = true;
    619       spellCheckLanguageButton.hidden = true;
    620       dictionaryDownloadInProgress.hidden = true;
    621       dictionaryDownloadFailed.hidden = true;
    622       dictionaryDownloadFailHelp.hidden = true;
    623 
    624       if (languageCode == this.spellCheckDictionary_) {
    625         if (!(languageCode in this.spellcheckDictionaryDownloadStatus_)) {
    626           spellCheckLanguageMessage.textContent =
    627               loadTimeData.getString('isUsedForSpellChecking');
    628           showMutuallyExclusiveNodes(
    629               [spellCheckLanguageButton, spellCheckLanguageMessage], 1);
    630         } else if (this.spellcheckDictionaryDownloadStatus_[languageCode] ==
    631                        DOWNLOAD_STATUS.IN_PROGRESS) {
    632           dictionaryDownloadInProgress.hidden = false;
    633         } else if (this.spellcheckDictionaryDownloadStatus_[languageCode] ==
    634                        DOWNLOAD_STATUS.FAILED) {
    635           spellCheckLanguageSection.hidden = true;
    636           dictionaryDownloadFailed.hidden = false;
    637           if (this.spellcheckDictionaryDownloadFailures_ > 1)
    638             dictionaryDownloadFailHelp.hidden = false;
    639         }
    640       } else if (languageCode in
    641           loadTimeData.getValue('spellCheckLanguageCodeSet')) {
    642         spellCheckLanguageButton.textContent =
    643             loadTimeData.getString('useThisForSpellChecking');
    644         showMutuallyExclusiveNodes(
    645             [spellCheckLanguageButton, spellCheckLanguageMessage], 0);
    646         spellCheckLanguageButton.languageCode = languageCode;
    647       } else if (!languageCode) {
    648         spellCheckLanguageButton.hidden = true;
    649         spellCheckLanguageMessage.hidden = true;
    650       } else {
    651         spellCheckLanguageMessage.textContent =
    652             loadTimeData.getString('cannotBeUsedForSpellChecking');
    653         showMutuallyExclusiveNodes(
    654             [spellCheckLanguageButton, spellCheckLanguageMessage], 1);
    655       }
    656     },
    657 
    658     /**
    659      * Updates the checkbox for stopping translation.
    660      * @param {string} languageCode Language code (ex. "fr").
    661      * @private
    662      */
    663     updateOfferToTranslateCheckbox_: function(languageCode) {
    664       var div = $('language-options-offer-to-translate');
    665 
    666       // Translation server supports Chinese (Transitional) and Chinese
    667       // (Simplified) but not 'general' Chinese. To avoid ambiguity, we don't
    668       // show this preference when general Chinese is selected.
    669       if (languageCode != 'zh') {
    670         div.hidden = false;
    671       } else {
    672         div.hidden = true;
    673         return;
    674       }
    675 
    676       var offerToTranslate = div.querySelector('div');
    677       var cannotTranslate = $('cannot-translate-in-this-language');
    678       var nodes = [offerToTranslate, cannotTranslate];
    679 
    680       var convertedLangCode = this.convertLangCodeForTranslation_(languageCode);
    681       if (this.translateSupportedLanguages_.indexOf(convertedLangCode) != -1) {
    682         showMutuallyExclusiveNodes(nodes, 0);
    683       } else {
    684         showMutuallyExclusiveNodes(nodes, 1);
    685         return;
    686       }
    687 
    688       var checkbox = $('offer-to-translate-in-this-language');
    689 
    690       if (!this.enableTranslate_) {
    691         checkbox.disabled = true;
    692         checkbox.checked = false;
    693         return;
    694       }
    695 
    696       // If the language corresponds to the default target language (in most
    697       // cases, the user's locale language), "Offer to translate" checkbox
    698       // should be always unchecked.
    699       var defaultTargetLanguage =
    700           loadTimeData.getString('defaultTargetLanguage');
    701       if (convertedLangCode == defaultTargetLanguage) {
    702         checkbox.disabled = true;
    703         checkbox.checked = false;
    704         return;
    705       }
    706 
    707       checkbox.disabled = false;
    708 
    709       var blockedLanguages = this.translateBlockedLanguages_;
    710       var checked = blockedLanguages.indexOf(convertedLangCode) == -1;
    711       checkbox.checked = checked;
    712     },
    713 
    714     /**
    715      * Updates the input method list.
    716      * @param {string} languageCode Language code (ex. "fr").
    717      * @private
    718      */
    719     updateInputMethodList_: function(languageCode) {
    720       // Give one of the checkboxes or buttons focus, if it's specified in the
    721       // URL hash (ex. focus=mozc). Used for automated testing.
    722       var focusInputMethodId = -1;
    723       var match = document.location.hash.match(/\bfocus=([\w:-]+)\b/);
    724       if (match) {
    725         focusInputMethodId = match[1];
    726       }
    727       // Change the visibility of the input method list. Input methods that
    728       // matches |languageCode| will become visible.
    729       var inputMethodList = $('language-options-input-method-list');
    730       var methods = inputMethodList.querySelectorAll('.input-method');
    731       for (var i = 0; i < methods.length; i++) {
    732         var method = methods[i];
    733         if (languageCode in method.languageCodeSet) {
    734           method.hidden = false;
    735           var input = method.querySelector('input');
    736           // Give it focus if the ID matches.
    737           if (input.inputMethodId == focusInputMethodId) {
    738             input.focus();
    739           }
    740         } else {
    741           method.hidden = true;
    742         }
    743       }
    744 
    745       $('language-options-input-method-none').hidden =
    746           (languageCode in this.languageCodeToInputMethodIdsMap_);
    747 
    748       if (focusInputMethodId == 'add') {
    749         $('language-options-add-button').focus();
    750       }
    751     },
    752 
    753     /**
    754      * Updates the language list in the add language overlay.
    755      * @private
    756      */
    757     updateLanguageListInAddLanguageOverlay_: function() {
    758       // Change the visibility of the language list in the add language
    759       // overlay. Languages that are already active will become invisible,
    760       // so that users don't add the same language twice.
    761       var languageOptionsList = $('language-options-list');
    762       var languageCodes = languageOptionsList.getLanguageCodes();
    763       var languageCodeSet = {};
    764       for (var i = 0; i < languageCodes.length; i++) {
    765         languageCodeSet[languageCodes[i]] = true;
    766       }
    767 
    768       var addLanguageList = $('add-language-overlay-language-list');
    769       var options = addLanguageList.querySelectorAll('option');
    770       assert(options.length > 0);
    771       var selectedFirstItem = false;
    772       for (var i = 0; i < options.length; i++) {
    773         var option = options[i];
    774         option.hidden = option.value in languageCodeSet;
    775         if (!option.hidden && !selectedFirstItem) {
    776           // Select first visible item, otherwise previously selected hidden
    777           // item will be selected by default at the next time.
    778           option.selected = true;
    779           selectedFirstItem = true;
    780         }
    781       }
    782     },
    783 
    784     /**
    785      * Handles preloadEnginesPref change.
    786      * @param {Event} e Change event.
    787      * @private
    788      */
    789     handlePreloadEnginesPrefChange_: function(e) {
    790       var value = e.value.value;
    791       this.preloadEngines_ = this.filterBadPreloadEngines_(value.split(','));
    792       this.updateCheckboxesFromPreloadEngines_();
    793       $('language-options-list').updateDeletable();
    794     },
    795 
    796     /**
    797      * Handles enabledExtensionImePref change.
    798      * @param {Event} e Change event.
    799      * @private
    800      */
    801     handleEnabledExtensionsPrefChange_: function(e) {
    802       var value = e.value.value;
    803       this.enabledExtensionImes_ = value.split(',');
    804       this.updateCheckboxesFromEnabledExtensions_();
    805     },
    806 
    807     /**
    808      * Handles offer-to-translate checkbox's click event.
    809      * @param {Event} e Click event.
    810      * @private
    811      */
    812     handleOfferToTranslateCheckboxClick_: function(e) {
    813       var checkbox = e.target;
    814       var checked = checkbox.checked;
    815 
    816       var languageOptionsList = $('language-options-list');
    817       var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode();
    818 
    819       if (checked)
    820         this.removeBlockedLanguage_(selectedLanguageCode);
    821       else
    822         this.addBlockedLanguage_(selectedLanguageCode);
    823     },
    824 
    825     /**
    826      * Handles input method checkbox's click event.
    827      * @param {Event} e Click event.
    828      * @private
    829      */
    830     handleCheckboxClick_: function(e) {
    831       var checkbox = assertInstanceof(e.target, Element);
    832 
    833       // Third party IMEs require additional confirmation prior to enabling due
    834       // to privacy risk.
    835       if (/^_ext_ime_/.test(checkbox.inputMethodId) && checkbox.checked) {
    836         var confirmationCallback = this.handleCheckboxUpdate_.bind(this,
    837                                                                    checkbox);
    838         var cancellationCallback = function() {
    839           checkbox.checked = false;
    840         };
    841         ThirdPartyImeConfirmOverlay.showConfirmationDialog({
    842           extension: checkbox.imeProvider,
    843           confirm: confirmationCallback,
    844           cancel: cancellationCallback
    845         });
    846       } else {
    847         this.handleCheckboxUpdate_(checkbox);
    848       }
    849     },
    850 
    851     /**
    852      * Updates active IMEs based on change in state of a checkbox for an input
    853      * method.
    854      * @param {!Element} checkbox Updated checkbox element.
    855      * @private
    856      */
    857     handleCheckboxUpdate_: function(checkbox) {
    858       if (checkbox.inputMethodId.match(/^_ext_ime_/)) {
    859         this.updateEnabledExtensionsFromCheckboxes_();
    860         this.saveEnabledExtensionPref_();
    861         return;
    862       }
    863       if (this.preloadEngines_.length == 1 && !checkbox.checked) {
    864         // Don't allow disabling the last input method.
    865         this.showNotification_(
    866             loadTimeData.getString('pleaseAddAnotherInputMethod'),
    867             loadTimeData.getString('okButton'));
    868         checkbox.checked = true;
    869         return;
    870       }
    871       if (checkbox.checked) {
    872         chrome.send('inputMethodEnable', [checkbox.inputMethodId]);
    873       } else {
    874         chrome.send('inputMethodDisable', [checkbox.inputMethodId]);
    875       }
    876       this.updatePreloadEnginesFromCheckboxes_();
    877       this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_);
    878       this.savePreloadEnginesPref_();
    879     },
    880 
    881     /**
    882      * Handles clicks on the "OK" button of the "Add language" dialog.
    883      * @param {Event} e Click event.
    884      * @private
    885      */
    886     handleAddLanguageOkButtonClick_: function(e) {
    887       var languagesSelect = $('add-language-overlay-language-list');
    888       var selectedIndex = languagesSelect.selectedIndex;
    889       if (selectedIndex >= 0) {
    890         var selection = languagesSelect.options[selectedIndex];
    891         var langCode = String(selection.value);
    892         $('language-options-list').addLanguage(langCode);
    893         this.addBlockedLanguage_(langCode);
    894         PageManager.closeOverlay();
    895       }
    896     },
    897 
    898     /**
    899      * Checks if languageCode is deletable or not.
    900      * @param {string} languageCode the languageCode to check for deletability.
    901      */
    902     languageIsDeletable: function(languageCode) {
    903       // Don't allow removing the language if it's a UI language.
    904       if (languageCode == this.prospectiveUiLanguageCode_)
    905         return false;
    906       return (!cr.isChromeOS ||
    907               this.canDeleteLanguage_(languageCode));
    908     },
    909 
    910     /**
    911      * Handles browse.enable_spellchecking change.
    912      * @param {Event} e Change event.
    913      * @private
    914      */
    915     updateEnableSpellCheck_: function(e) {
    916        var value = !$('enable-spell-check').checked;
    917        $('language-options-spell-check-language-button').disabled = value;
    918        if (!cr.isMac)
    919          $('edit-dictionary-button').hidden = value;
    920      },
    921 
    922     /**
    923      * Handles translateBlockedLanguagesPref change.
    924      * @param {Event} e Change event.
    925      * @private
    926      */
    927     handleTranslateBlockedLanguagesPrefChange_: function(e) {
    928       this.translateBlockedLanguages_ = e.value.value;
    929       this.updateOfferToTranslateCheckbox_(
    930           $('language-options-list').getSelectedLanguageCode());
    931     },
    932 
    933     /**
    934      * Handles spellCheckDictionaryPref change.
    935      * @param {Event} e Change event.
    936      * @private
    937      */
    938     handleSpellCheckDictionaryPrefChange_: function(e) {
    939       var languageCode = e.value.value;
    940       this.spellCheckDictionary_ = languageCode;
    941       if (!cr.isMac) {
    942         this.updateSpellCheckLanguageButton_(
    943             $('language-options-list').getSelectedLanguageCode());
    944       }
    945     },
    946 
    947     /**
    948      * Handles translate.enabled change.
    949      * @param {Event} e Change event.
    950      * @private
    951      */
    952     handleEnableTranslatePrefChange_: function(e) {
    953       var enabled = e.value.value;
    954       this.enableTranslate_ = enabled;
    955       this.updateOfferToTranslateCheckbox_(
    956           $('language-options-list').getSelectedLanguageCode());
    957     },
    958 
    959     /**
    960      * Handles spellCheckLanguageButton click.
    961      * @param {Event} e Click event.
    962      * @private
    963      */
    964     handleSpellCheckLanguageButtonClick_: function(e) {
    965       var languageCode = e.target.languageCode;
    966       // Save the preference.
    967       Preferences.setStringPref(SPELL_CHECK_DICTIONARY_PREF,
    968                                 languageCode, true);
    969       chrome.send('spellCheckLanguageChange', [languageCode]);
    970     },
    971 
    972     /**
    973      * Checks whether it's possible to remove the language specified by
    974      * languageCode and returns true if possible. This function returns false
    975      * if the removal causes the number of preload engines to be zero.
    976      *
    977      * @param {string} languageCode Language code (ex. "fr").
    978      * @return {boolean} Returns true on success.
    979      * @private
    980      */
    981     canDeleteLanguage_: function(languageCode) {
    982       // First create the set of engines to be removed from input methods
    983       // associated with the language code.
    984       var enginesToBeRemovedSet = {};
    985       var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode];
    986 
    987       // If this language doesn't have any input methods, it can be deleted.
    988       if (!inputMethodIds)
    989         return true;
    990 
    991       for (var i = 0; i < inputMethodIds.length; i++) {
    992         enginesToBeRemovedSet[inputMethodIds[i]] = true;
    993       }
    994 
    995       // Then eliminate engines that are also used for other active languages.
    996       // For instance, if "xkb:us::eng" is used for both English and Filipino.
    997       var languageCodes = $('language-options-list').getLanguageCodes();
    998       for (var i = 0; i < languageCodes.length; i++) {
    999         // Skip the target language code.
   1000         if (languageCodes[i] == languageCode) {
   1001           continue;
   1002         }
   1003         // Check if input methods used in this language are included in
   1004         // enginesToBeRemovedSet. If so, eliminate these from the set, so
   1005         // we don't remove this time.
   1006         var inputMethodIdsForAnotherLanguage =
   1007             this.languageCodeToInputMethodIdsMap_[languageCodes[i]];
   1008         if (!inputMethodIdsForAnotherLanguage)
   1009           continue;
   1010 
   1011         for (var j = 0; j < inputMethodIdsForAnotherLanguage.length; j++) {
   1012           var inputMethodId = inputMethodIdsForAnotherLanguage[j];
   1013           if (inputMethodId in enginesToBeRemovedSet) {
   1014             delete enginesToBeRemovedSet[inputMethodId];
   1015           }
   1016         }
   1017       }
   1018 
   1019       // Update the preload engine list with the to-be-removed set.
   1020       var newPreloadEngines = [];
   1021       for (var i = 0; i < this.preloadEngines_.length; i++) {
   1022         if (!(this.preloadEngines_[i] in enginesToBeRemovedSet)) {
   1023           newPreloadEngines.push(this.preloadEngines_[i]);
   1024         }
   1025       }
   1026       // Don't allow this operation if it causes the number of preload
   1027       // engines to be zero.
   1028       return (newPreloadEngines.length > 0);
   1029     },
   1030 
   1031     /**
   1032      * Saves the enabled extension preference.
   1033      * @private
   1034      */
   1035     saveEnabledExtensionPref_: function() {
   1036       Preferences.setStringPref(ENABLED_EXTENSION_IME_PREF,
   1037                                 this.enabledExtensionImes_.join(','), true);
   1038     },
   1039 
   1040     /**
   1041      * Updates the checkboxes in the input method list from the enabled
   1042      * extensions preference.
   1043      * @private
   1044      */
   1045     updateCheckboxesFromEnabledExtensions_: function() {
   1046       // Convert the list into a dictonary for simpler lookup.
   1047       var dictionary = {};
   1048       for (var i = 0; i < this.enabledExtensionImes_.length; i++)
   1049         dictionary[this.enabledExtensionImes_[i]] = true;
   1050 
   1051       var inputMethodList = $('language-options-input-method-list');
   1052       var checkboxes = inputMethodList.querySelectorAll('input');
   1053       for (var i = 0; i < checkboxes.length; i++) {
   1054         if (checkboxes[i].inputMethodId.match(/^_ext_ime_/))
   1055           checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary);
   1056       }
   1057       var configureButtons = inputMethodList.querySelectorAll('button');
   1058       for (var i = 0; i < configureButtons.length; i++) {
   1059         if (configureButtons[i].inputMethodId.match(/^_ext_ime_/)) {
   1060           configureButtons[i].hidden =
   1061               !(configureButtons[i].inputMethodId in dictionary);
   1062         }
   1063       }
   1064     },
   1065 
   1066     /**
   1067      * Updates the enabled extensions preference from the checkboxes in the
   1068      * input method list.
   1069      * @private
   1070      */
   1071     updateEnabledExtensionsFromCheckboxes_: function() {
   1072       this.enabledExtensionImes_ = [];
   1073       var inputMethodList = $('language-options-input-method-list');
   1074       var checkboxes = inputMethodList.querySelectorAll('input');
   1075       for (var i = 0; i < checkboxes.length; i++) {
   1076         if (checkboxes[i].inputMethodId.match(/^_ext_ime_/)) {
   1077           if (checkboxes[i].checked)
   1078             this.enabledExtensionImes_.push(checkboxes[i].inputMethodId);
   1079         }
   1080       }
   1081     },
   1082 
   1083     /**
   1084      * Saves the preload engines preference.
   1085      * @private
   1086      */
   1087     savePreloadEnginesPref_: function() {
   1088       Preferences.setStringPref(PRELOAD_ENGINES_PREF,
   1089                                 this.preloadEngines_.join(','), true);
   1090     },
   1091 
   1092     /**
   1093      * Updates the checkboxes in the input method list from the preload
   1094      * engines preference.
   1095      * @private
   1096      */
   1097     updateCheckboxesFromPreloadEngines_: function() {
   1098       // Convert the list into a dictonary for simpler lookup.
   1099       var dictionary = {};
   1100       for (var i = 0; i < this.preloadEngines_.length; i++) {
   1101         dictionary[this.preloadEngines_[i]] = true;
   1102       }
   1103 
   1104       var inputMethodList = $('language-options-input-method-list');
   1105       var checkboxes = inputMethodList.querySelectorAll('input');
   1106       for (var i = 0; i < checkboxes.length; i++) {
   1107         if (!checkboxes[i].inputMethodId.match(/^_ext_ime_/))
   1108           checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary);
   1109       }
   1110       var configureButtons = inputMethodList.querySelectorAll('button');
   1111       for (var i = 0; i < configureButtons.length; i++) {
   1112         if (!configureButtons[i].inputMethodId.match(/^_ext_ime_/)) {
   1113           configureButtons[i].hidden =
   1114               !(configureButtons[i].inputMethodId in dictionary);
   1115         }
   1116       }
   1117     },
   1118 
   1119     /**
   1120      * Updates the preload engines preference from the checkboxes in the
   1121      * input method list.
   1122      * @private
   1123      */
   1124     updatePreloadEnginesFromCheckboxes_: function() {
   1125       this.preloadEngines_ = [];
   1126       var inputMethodList = $('language-options-input-method-list');
   1127       var checkboxes = inputMethodList.querySelectorAll('input');
   1128       for (var i = 0; i < checkboxes.length; i++) {
   1129         if (!checkboxes[i].inputMethodId.match(/^_ext_ime_/)) {
   1130           if (checkboxes[i].checked)
   1131             this.preloadEngines_.push(checkboxes[i].inputMethodId);
   1132         }
   1133       }
   1134       var languageOptionsList = $('language-options-list');
   1135       languageOptionsList.updateDeletable();
   1136     },
   1137 
   1138     /**
   1139      * Filters bad preload engines in case bad preload engines are
   1140      * stored in the preference. Removes duplicates as well.
   1141      * @param {Array} preloadEngines List of preload engines.
   1142      * @private
   1143      */
   1144     filterBadPreloadEngines_: function(preloadEngines) {
   1145       // Convert the list into a dictonary for simpler lookup.
   1146       var dictionary = {};
   1147       var list = loadTimeData.getValue('inputMethodList');
   1148       for (var i = 0; i < list.length; i++) {
   1149         dictionary[list[i].id] = true;
   1150       }
   1151 
   1152       var enabledPreloadEngines = [];
   1153       var seen = {};
   1154       for (var i = 0; i < preloadEngines.length; i++) {
   1155         // Check if the preload engine is present in the
   1156         // dictionary, and not duplicate. Otherwise, skip it.
   1157         // Component Extension IME should be handled same as preloadEngines and
   1158         // "_comp_" is the special prefix of its ID.
   1159         if ((preloadEngines[i] in dictionary && !(preloadEngines[i] in seen)) ||
   1160             /^_comp_/.test(preloadEngines[i])) {
   1161           enabledPreloadEngines.push(preloadEngines[i]);
   1162           seen[preloadEngines[i]] = true;
   1163         }
   1164       }
   1165       return enabledPreloadEngines;
   1166     },
   1167 
   1168     // TODO(kochi): This is an adapted copy from new_tab.js.
   1169     // If this will go as final UI, refactor this to share the component with
   1170     // new new tab page.
   1171     /**
   1172      * @private
   1173      */
   1174     notificationTimeout_: null,
   1175 
   1176     /**
   1177      * Shows notification.
   1178      * @param {string} text
   1179      * @param {string} actionText
   1180      * @param {number=} opt_delay
   1181      * @private
   1182      */
   1183     showNotification_: function(text, actionText, opt_delay) {
   1184       var notificationElement = $('notification');
   1185       var actionLink = notificationElement.querySelector('.link-color');
   1186       var delay = opt_delay || 10000;
   1187 
   1188       function show() {
   1189         window.clearTimeout(this.notificationTimeout_);
   1190         notificationElement.classList.add('show');
   1191         document.body.classList.add('notification-shown');
   1192       }
   1193 
   1194       function hide() {
   1195         window.clearTimeout(this.notificationTimeout_);
   1196         notificationElement.classList.remove('show');
   1197         document.body.classList.remove('notification-shown');
   1198         // Prevent tabbing to the hidden link.
   1199         actionLink.tabIndex = -1;
   1200         // Setting tabIndex to -1 only prevents future tabbing to it. If,
   1201         // however, the user switches window or a tab and then moves back to
   1202         // this tab the element may gain focus. We therefore make sure that we
   1203         // blur the element so that the element focus is not restored when
   1204         // coming back to this window.
   1205         actionLink.blur();
   1206       }
   1207 
   1208       function delayedHide() {
   1209         this.notificationTimeout_ = window.setTimeout(hide, delay);
   1210       }
   1211 
   1212       notificationElement.firstElementChild.textContent = text;
   1213       actionLink.textContent = actionText;
   1214 
   1215       actionLink.onclick = hide;
   1216       actionLink.onkeydown = function(e) {
   1217         if (e.keyIdentifier == 'Enter') {
   1218           hide();
   1219         }
   1220       };
   1221       notificationElement.onmouseover = show;
   1222       notificationElement.onmouseout = delayedHide;
   1223       actionLink.onfocus = show;
   1224       actionLink.onblur = delayedHide;
   1225       // Enable tabbing to the link now that it is shown.
   1226       actionLink.tabIndex = 0;
   1227 
   1228       show();
   1229       delayedHide();
   1230     },
   1231 
   1232     /**
   1233      * Chrome callback for when the UI language preference is saved.
   1234      * @param {string} languageCode The newly selected language to use.
   1235      * @private
   1236      */
   1237     uiLanguageSaved_: function(languageCode) {
   1238       this.prospectiveUiLanguageCode_ = languageCode;
   1239 
   1240       // If the user is no longer on the same language code, ignore.
   1241       if ($('language-options-list').getSelectedLanguageCode() != languageCode)
   1242         return;
   1243 
   1244       // Special case for when a user changes to a different language, and
   1245       // changes back to the same language without having restarted Chrome or
   1246       // logged in/out of ChromeOS.
   1247       if (languageCode == loadTimeData.getString('currentUiLanguageCode')) {
   1248         this.updateUiLanguageButton_(languageCode);
   1249         return;
   1250       }
   1251 
   1252       // Otherwise, show a notification telling the user that their changes will
   1253       // only take effect after restart.
   1254       showMutuallyExclusiveNodes([$('language-options-ui-language-button'),
   1255                                   $('language-options-ui-notification-bar')],
   1256                                  1);
   1257     },
   1258 
   1259     /**
   1260      * A handler for when dictionary for |languageCode| begins downloading.
   1261      * @param {string} languageCode The language of the dictionary that just
   1262      *     began downloading.
   1263      * @private
   1264      */
   1265     onDictionaryDownloadBegin_: function(languageCode) {
   1266       this.spellcheckDictionaryDownloadStatus_[languageCode] =
   1267           DOWNLOAD_STATUS.IN_PROGRESS;
   1268       if (!cr.isMac &&
   1269           languageCode ==
   1270               $('language-options-list').getSelectedLanguageCode()) {
   1271         this.updateSpellCheckLanguageButton_(languageCode);
   1272       }
   1273     },
   1274 
   1275     /**
   1276      * A handler for when dictionary for |languageCode| successfully downloaded.
   1277      * @param {string} languageCode The language of the dictionary that
   1278      *     succeeded downloading.
   1279      * @private
   1280      */
   1281     onDictionaryDownloadSuccess_: function(languageCode) {
   1282       delete this.spellcheckDictionaryDownloadStatus_[languageCode];
   1283       this.spellcheckDictionaryDownloadFailures_ = 0;
   1284       if (!cr.isMac &&
   1285           languageCode ==
   1286               $('language-options-list').getSelectedLanguageCode()) {
   1287         this.updateSpellCheckLanguageButton_(languageCode);
   1288       }
   1289     },
   1290 
   1291     /**
   1292      * A handler for when dictionary for |languageCode| fails to download.
   1293      * @param {string} languageCode The language of the dictionary that failed
   1294      *     to download.
   1295      * @private
   1296      */
   1297     onDictionaryDownloadFailure_: function(languageCode) {
   1298       this.spellcheckDictionaryDownloadStatus_[languageCode] =
   1299           DOWNLOAD_STATUS.FAILED;
   1300       this.spellcheckDictionaryDownloadFailures_++;
   1301       if (!cr.isMac &&
   1302           languageCode ==
   1303               $('language-options-list').getSelectedLanguageCode()) {
   1304         this.updateSpellCheckLanguageButton_(languageCode);
   1305       }
   1306     },
   1307 
   1308     /**
   1309      * Converts the language code for Translation. There are some differences
   1310      * between the language set for Translation and that for Accept-Language.
   1311      * @param {string} languageCode The language code like 'fr'.
   1312      * @return {string} The converted language code.
   1313      * @private
   1314      */
   1315     convertLangCodeForTranslation_: function(languageCode) {
   1316       var tokens = languageCode.split('-');
   1317       var main = tokens[0];
   1318 
   1319       // See also: chrome/renderer/translate/translate_helper.cc.
   1320       var synonyms = {
   1321         'nb': 'no',
   1322         'he': 'iw',
   1323         'jv': 'jw',
   1324         'fil': 'tl',
   1325       };
   1326 
   1327       if (main in synonyms) {
   1328         return synonyms[main];
   1329       } else if (main == 'zh') {
   1330         // In Translation, general Chinese is not used, and the sub code is
   1331         // necessary as a language code for Translate server.
   1332         return languageCode;
   1333       }
   1334 
   1335       return main;
   1336     },
   1337   };
   1338 
   1339   /**
   1340    * Shows the node at |index| in |nodes|, hides all others.
   1341    * @param {Array.<HTMLElement>} nodes The nodes to be shown or hidden.
   1342    * @param {number} index The index of |nodes| to show.
   1343    */
   1344   function showMutuallyExclusiveNodes(nodes, index) {
   1345     assert(index >= 0 && index < nodes.length);
   1346     for (var i = 0; i < nodes.length; ++i) {
   1347       assert(nodes[i] instanceof HTMLElement);  // TODO(dbeam): Ignore null?
   1348       nodes[i].hidden = i != index;
   1349     }
   1350   }
   1351 
   1352   LanguageOptions.uiLanguageSaved = function(languageCode) {
   1353     LanguageOptions.getInstance().uiLanguageSaved_(languageCode);
   1354   };
   1355 
   1356   LanguageOptions.onDictionaryDownloadBegin = function(languageCode) {
   1357     LanguageOptions.getInstance().onDictionaryDownloadBegin_(languageCode);
   1358   };
   1359 
   1360   LanguageOptions.onDictionaryDownloadSuccess = function(languageCode) {
   1361     LanguageOptions.getInstance().onDictionaryDownloadSuccess_(languageCode);
   1362   };
   1363 
   1364   LanguageOptions.onDictionaryDownloadFailure = function(languageCode) {
   1365     LanguageOptions.getInstance().onDictionaryDownloadFailure_(languageCode);
   1366   };
   1367 
   1368   // Export
   1369   return {
   1370     LanguageOptions: LanguageOptions
   1371   };
   1372 });
   1373