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