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