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