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