diff --git a/src/js/select2/defaults.js b/src/js/select2/defaults.js index 575fc8a1..f694db6b 100644 --- a/src/js/select2/defaults.js +++ b/src/js/select2/defaults.js @@ -232,66 +232,29 @@ define([ ); } - if (typeof options.language === 'string') { - // Check if the language is specified with a region - if (options.language.indexOf('-') > 0) { - // Extract the region information if it is included - var languageParts = options.language.split('-'); - var baseLanguage = languageParts[0]; + // If the defaults were not previously applied from an element, it is + // possible for the language option to have not been resolved + options.language = this._resolveLanguage(options.language); - options.language = [options.language, baseLanguage]; - } else { - options.language = [options.language]; + // Always fall back to English since it will always be complete + options.language.push('en'); + + var uniqueLanguages = []; + + for (var l = 0; l < options.language.length; l++) { + var language = options.language[l]; + + if (uniqueLanguages.indexOf(language) === -1) { + uniqueLanguages.push(language); } } - if ($.isArray(options.language)) { - var languages = new Translation(); - options.language.push('en'); + options.language = uniqueLanguages; - var languageNames = options.language; - - for (var l = 0; l < languageNames.length; l++) { - var name = languageNames[l]; - var language = {}; - - try { - // Try to load it with the original name - language = Translation.loadPath(name); - } catch (e) { - try { - // If we couldn't load it, check if it wasn't the full path - name = this.defaults.amdLanguageBase + name; - language = Translation.loadPath(name); - } catch (ex) { - // The translation could not be loaded at all. Sometimes this is - // because of a configuration problem, other times this can be - // because of how Select2 helps load all possible translation files. - if (options.debug && window.console && console.warn) { - console.warn( - 'Select2: The language file for "' + name + '" could not be ' + - 'automatically loaded. A fallback will be used instead.' - ); - } - - continue; - } - } - - languages.extend(language); - } - - options.translations = languages; - } else { - var baseTranslation = Translation.loadPath( - this.defaults.amdLanguageBase + 'en' - ); - var customTranslation = new Translation(options.language); - - customTranslation.extend(baseTranslation); - - options.translations = customTranslation; - } + options.translations = this._processTranslations( + options.language, + options.debug + ); return options; }; @@ -358,7 +321,7 @@ define([ debug: false, dropdownAutoWidth: false, escapeMarkup: Utils.escapeMarkup, - language: EnglishTranslation, + language: {}, matcher: matcher, minimumInputLength: 0, maximumInputLength: 0, @@ -380,6 +343,103 @@ define([ }; }; + Defaults.prototype.applyFromElement = function (options, $element) { + var optionLanguage = options.language; + var defaultLanguage = this.defaults.language; + var elementLanguage = $element.prop('lang'); + var parentLanguage = $element.closest('[lang]').prop('lang'); + + var languages = Array.prototype.concat.call( + this._resolveLanguage(elementLanguage), + this._resolveLanguage(optionLanguage), + this._resolveLanguage(defaultLanguage), + this._resolveLanguage(parentLanguage) + ); + + options.language = languages; + + return options; + }; + + Defaults.prototype._resolveLanguage = function (language) { + if (!language) { + return []; + } + + if ($.isEmptyObject(language)) { + return []; + } + + if ($.isPlainObject(language)) { + return [language]; + } + + var languages; + + if (!$.isArray(language)) { + languages = [language]; + } else { + languages = language; + } + + var resolvedLanguages = []; + + for (var l = 0; l < languages.length; l++) { + resolvedLanguages.push(languages[l]); + + if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = languages[l].split('-'); + var baseLanguage = languageParts[0]; + + resolvedLanguages.push(baseLanguage); + } + } + + return resolvedLanguages; + }; + + Defaults.prototype._processTranslations = function (languages, debug) { + var translations = new Translation(); + + for (var l = 0; l < languages.length; l++) { + var languageData = new Translation(); + + var language = languages[l]; + + if (typeof language === 'string') { + try { + // Try to load it with the original name + languageData = Translation.loadPath(language); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + language = this.defaults.amdLanguageBase + language; + languageData = Translation.loadPath(language); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files + if (debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + language + '" could ' + + 'not be automatically loaded. A fallback will be used instead.' + ); + } + } + } + } else if ($.isPlainObject(language)) { + languageData = new Translation(language); + } else { + languageData = language; + } + + translations.extend(languageData); + } + + return translations; + }; + Defaults.prototype.set = function (key, value) { var camelKey = $.camelCase(key); diff --git a/src/js/select2/options.js b/src/js/select2/options.js index 8e0dc07e..b3d67cee 100644 --- a/src/js/select2/options.js +++ b/src/js/select2/options.js @@ -11,6 +11,10 @@ define([ this.fromElement($element); } + if ($element != null) { + this.options = Defaults.applyFromElement(this.options, $element); + } + this.options = Defaults.apply(this.options); if ($element && $element.is('input')) { @@ -34,14 +38,6 @@ define([ this.options.disabled = $e.prop('disabled'); } - if (this.options.language == null) { - if ($e.prop('lang')) { - this.options.language = $e.prop('lang').toLowerCase(); - } else if ($e.closest('[lang]').prop('lang')) { - this.options.language = $e.closest('[lang]').prop('lang'); - } - } - if (this.options.dir == null) { if ($e.prop('dir')) { this.options.dir = $e.prop('dir'); diff --git a/tests/options/translation-tests.js b/tests/options/translation-tests.js index ab433b61..34b86d33 100644 --- a/tests/options/translation-tests.js +++ b/tests/options/translation-tests.js @@ -1,16 +1,73 @@ -module('Options - Translations'); - var $ = require('jquery'); var Options = require('select2/options'); +var Defaults = require('select2/defaults'); + +module('Options - Translations', { + beforeEach: function () { + Defaults.reset(); + }, + afterEach: function () { + Defaults.reset(); + } +}); + +test('partial dictonaries are reset when default reset', function (assert) { + Defaults.set('language', { + test: 'testing' + }); + + Defaults.reset(); + + assert.ok( + !Defaults.defaults.language.test, + 'The partial dictionary should have been reset' + ); +}); + +test('default language chain is English', function (assert) { + var $element = $(''); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['en'] + ); +}); + +test( + 'default translation includes all of the required messages', + function (assert) { + var $element = $(''); + + var options = new Options({}, $element); + + assert.deepEqual( + Object.keys(options.get('translations').all()), + [ + 'errorLoading', + 'inputTooLong', + 'inputTooShort', + 'loadingMore', + 'maximumSelected', + 'noResults', + 'searching', + 'removeAllItems' + ] + ); + } +); test('partial dictionaries can be passed', function (assert) { + var $element = $(''); + var options = new Options({ language: { searching: function () { return 'Something'; } } - }); + }, $element); var translations = options.get('translations'); @@ -26,3 +83,206 @@ test('partial dictionaries can be passed', function (assert) { 'You can still get English translations for keys not passed in' ); }); + +test('partial dictionaries can be combined with defaults', function (assert) { + var $element = $(''); + + Defaults.set('language', { + test: function () { + return 'Testing'; + } + }); + + var options = new Options({ + language: { + searching: function () { + return 'Something'; + } + } + }, $element); + + var translations = options.get('translations'); + + assert.equal( + translations.get('searching')(), + 'Something', + 'The partial dictionary still overrides translations' + ); + + assert.equal( + translations.get('test')(), + 'Testing', + 'The defaults were included in the fallback chain' + ); + + assert.equal( + translations.get('noResults')(), + 'No results found', + 'You can still get English translations for keys not passed in' + ); +}); + +test('partial dictionaries can used in fallback chains', function (assert) { + var $element = $(''); + + var options = new Options({ + language: [ + { + searching: function () { + return 'Something'; + } + }, + { + test: function () { + return 'Testing'; + } + } + ] + }, $element); + + var translations = options.get('translations'); + + assert.equal( + translations.get('searching')(), + 'Something', + 'The partial dictionary still overrides translations' + ); + + assert.equal( + translations.get('test')(), + 'Testing', + 'The defaults were included in the fallback chain' + ); + + assert.equal( + translations.get('noResults')(), + 'No results found', + 'You can still get English translations for keys not passed in' + ); +}); + +test('language can be set via the options', function (assert) { + var $element = $(''); + + var options = new Options({ + language: 'es' + }, $element); + + assert.deepEqual( + options.get('language'), + ['es', 'en'] + ); +}); + +test('multi-part language is broken out', function (assert) { + var $element = $(''); + + var options = new Options({ + language: 'pt-BR' + }, $element); + + assert.deepEqual( + options.get('language'), + ['pt-BR', 'pt', 'en'] + ); +}); + +test('default language can be set', function (assert) { + var $element = $(''); + + Defaults.set('language', 'es'); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['es', 'en'] + ); +}); + +test('lanugage set via options adds to default chain', function (assert) { + var $element = $(''); + + Defaults.set('language', 'es'); + + var options = new Options({ + language: 'it' + }, $element); + + assert.deepEqual( + options.get('language'), + ['it', 'es', 'en'] + ); +}); + +test('default language chain can be set', function (assert) { + var $element = $(''); + + Defaults.set('language', ['es', 'it', 'en']); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['es', 'it', 'en'] + ); +}); + +test('language can be set by lang attr', function (assert) { + var $element = $(''); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['es', 'en'] + ); +}); + +test('language can be inherited by lang attr', function (assert) { + var $element = $('
').find('select'); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['es', 'en'] + ); +}); + +test('multi-part language can be inherited by lang attr', function (assert) { + var $element = $('
').find('select'); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['pt-BR', 'pt', 'en'] + ); +}); + +test('lang attr overrides default language', function (assert) { + var $element = $(''); + + Defaults.set('language', 'es'); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['it', 'es', 'en'] + ); +}); + +test('default language overrides inherited lang attr', function (assert) { + var $element = $('
').find('select'); + + Defaults.set('language', 'es'); + + var options = new Options({}, $element); + + assert.deepEqual( + options.get('language'), + ['es', 'it', 'en'] + ); +});